Inserir código no contexto da página usando um script de conteúdo

480

Estou aprendendo a criar extensões do Chrome. Comecei a desenvolver um para capturar eventos do YouTube. Quero usá-lo com o YouTube Flash Player (mais tarde tentarei torná-lo compatível com HTML5).

manifest.json:

{
    "name": "MyExtension",
    "version": "1.0",
    "description": "Gotta catch Youtube events!",
    "permissions": ["tabs", "http://*/*"],
    "content_scripts" : [{
        "matches" : [ "www.youtube.com/*"],
        "js" : ["myScript.js"]
    }]
}

myScript.js:

function state() { console.log("State Changed!"); }
var player = document.getElementById("movie_player");
player.addEventListener("onStateChange", "state");
console.log("Started!");

O problema é que o console me dá o "Iniciado!" , mas não há "Estado alterado!" quando reproduzo / pausa vídeos do YouTube.

Quando esse código é colocado no console, ele funcionou. O que estou fazendo errado?

André Alves
fonte
14
tentar remover as aspas em torno de seu nome da função:player.addEventListener("onStateChange", state);
Eduardo
2
Também é notável que, ao escrever correspondências, não se esqueça de incluir https://ou http://, isso www.youtube.com/*não deixaria você compactar a extensão e geraria um erro de separador de esquema ausente
Nilay Vishwakarma

Respostas:

874

Os scripts de conteúdo são executados em um ambiente de "mundo isolado" . Você precisa injetar seu state()método na própria página.

Quando você deseja usar uma das chrome.*APIs no script, precisa implementar um manipulador de eventos especial, conforme descrito nesta resposta: Extensão do Chrome - recuperando a mensagem original do Gmail .

Caso contrário, se você não precisar usar chrome.*APIs, recomendo injetar todo o seu código JS na página adicionando uma <script>tag:

Índice

  • Método 1: Injetar outro arquivo
  • Método 2: injetar código incorporado
  • Método 2b: usando uma função
  • Método 3: usando um evento embutido
  • Valores dinâmicos no código injetado

Método 1: Injetar outro arquivo

Este é o método mais fácil / melhor quando você tem muito código. Inclua seu código JS real em um arquivo na sua extensão, digamos script.js. Em seguida, deixe o seu script de conteúdo da seguinte maneira (explicado aqui: Javascript personalizado do "Google Shortcut" do Google Chome ):

var s = document.createElement('script');
// TODO: add "script.js" to web_accessible_resources in manifest.json
s.src = chrome.runtime.getURL('script.js');
s.onload = function() {
    this.remove();
};
(document.head || document.documentElement).appendChild(s);

Nota: Se você usar esse método, o script.jsarquivo injetado deverá ser adicionado à "web_accessible_resources"seção ( exemplo ). Caso contrário, o Chrome se recusará a carregar seu script e exibirá o seguinte erro no console:

Negando a carga da extensão chrome: // [EXTENSIONID] /script.js. Os recursos devem ser listados na chave do manifesto web_accessible_resources para serem carregados por páginas fora da extensão.

Método 2: injetar código incorporado

Esse método é útil quando você deseja executar rapidamente um pequeno pedaço de código. (Veja também: Como desativar as teclas de atalho do facebook com a extensão do Chrome? ).

var actualCode = `// Code here.
// If you want to use a variable, use $ and curly braces.
// For example, to use a fixed random number:
var someFixedRandomValue = ${ Math.random() };
// NOTE: Do not insert unsafe variables in this way, see below
// at "Dynamic values in the injected code"
`;

var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Nota: os literais de modelo são suportados apenas no Chrome 41 e superior. Se você deseja que a extensão funcione no Chrome 40-, use:

var actualCode = ['/* Code here. Example: */' + 'alert(0);',
                  '// Beware! This array have to be joined',
                  '// using a newline. Otherwise, missing semicolons',
                  '// or single-line comments (//) will mess up your',
                  '// code ----->'].join('\n');

Método 2b: usando uma função

Para um grande pedaço de código, não é possível citar a string. Em vez de usar uma matriz, uma função pode ser usada e stringificada:

var actualCode = '(' + function() {
    // All code is executed in a local scope.
    // For example, the following does NOT overwrite the global `alert` method
    var alert = null;
    // To overwrite a global variable, prefix `window`:
    window.alert = null;
} + ')();';
var script = document.createElement('script');
script.textContent = actualCode;
(document.head||document.documentElement).appendChild(script);
script.remove();

Este método funciona, porque o +operador em cadeias e uma função converte todos os objetos em uma cadeia. Se você pretende usar o código mais de uma vez, é aconselhável criar uma função para evitar a repetição do código. Uma implementação pode se parecer com:

function injectScript(func) {
    var actualCode = '(' + func + ')();'
    ...
}
injectScript(function() {
   alert("Injected script");
});

Nota: Como a função é serializada, o escopo original e todas as propriedades associadas são perdidas!

var scriptToInject = function() {
    console.log(typeof scriptToInject);
};
injectScript(scriptToInject);
// Console output:  "undefined"

Método 3: usando um evento embutido

Às vezes, você deseja executar algum código imediatamente, por exemplo, para executar um código antes que o <head>elemento seja criado. Isso pode ser feito inserindo uma <script>etiqueta com textContent(consulte o método 2 / 2b).

Uma alternativa, mas não recomendada, é usar eventos embutidos. Não é recomendado porque, se a página definir uma política de Segurança de Conteúdo que proíba scripts embutidos, os ouvintes de eventos embutidos serão bloqueados. Os scripts embutidos injetados pela extensão, por outro lado, ainda são executados. Se você ainda deseja usar eventos embutidos, é assim:

var actualCode = '// Some code example \n' + 
                 'console.log(document.documentElement.outerHTML);';

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

Nota: Este método assume que não há outros ouvintes de eventos globais que manipulam o resetevento. Se houver, você também pode escolher um dos outros eventos globais. Basta abrir o console do JavaScript (F12), digitar document.documentElement.one escolher os eventos disponíveis.

Valores dinâmicos no código injetado

Ocasionalmente, você precisa passar uma variável arbitrária para a função injetada. Por exemplo:

var GREETING = "Hi, I'm ";
var NAME = "Rob";
var scriptToInject = function() {
    alert(GREETING + NAME);
};

Para injetar esse código, você precisa passar as variáveis ​​como argumentos para a função anônima. Certifique-se de implementá-lo corretamente! O seguinte não funcionará:

var scriptToInject = function (GREETING, NAME) { ... };
var actualCode = '(' + scriptToInject + ')(' + GREETING + ',' + NAME + ')';
// The previous will work for numbers and booleans, but not strings.
// To see why, have a look at the resulting string:
var actualCode = "(function(GREETING, NAME) {...})(Hi, I'm ,Rob)";
//                                                 ^^^^^^^^ ^^^ No string literals!

A solução é usar JSON.stringifyantes de passar o argumento. Exemplo:

var actualCode = '(' + function(greeting, name) { ...
} + ')(' + JSON.stringify(GREETING) + ',' + JSON.stringify(NAME) + ')';

Se você tiver muitas variáveis, vale a pena usar JSON.stringifyuma vez, para melhorar a legibilidade, da seguinte maneira:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Rob W
fonte
82
Essa resposta deve fazer parte dos documentos oficiais. Os documentos oficiais devem ser enviados da maneira recomendada -> 3 maneiras de fazer a mesma coisa ... Errado?
Mars Robertson
7
@ Qantas94Heavy O CSP da extensão não afeta os scripts de conteúdo. Somente o CSP da página é relevante. O método 1 pode ser bloqueado usando uma script-srcdiretiva que exclua a origem da extensão, o método 2 pode ser bloqueado usando um CSP que exclui "linha não segura" `.
Rob W
3
Alguém perguntou por que eu removi a tag de script usando script.parentNode.removeChild(script);. Minha razão para fazer isso é porque eu gosto de limpar minha bagunça. Quando um script embutido é inserido no documento, ele é imediatamente executado e a <script>tag pode ser removida com segurança.
Rob W
9
Outro método: use location.href = "javascript: alert('yeah')";em qualquer lugar do seu script de conteúdo. É mais fácil para trechos curtos de código e também pode acessar os objetos JS da página.
Métoule 26/09/13
3
@ Chrishr Tenha cuidado ao usar javascript:. O código que abrange várias linhas pode não funcionar conforme o esperado. Uma linha-comment ( //) irá truncar o restante, de modo que este irá falhar: location.href = 'javascript:// Do something <newline> alert(0);';. Isso pode ser contornado, garantindo que você use comentários de várias linhas. Outra coisa a ter cuidado é que o resultado da expressão deve ser nulo. javascript:window.x = 'some variable';fará com que o documento seja descarregado e substituído pela frase 'alguma variável'. Se usado corretamente, é de fato uma alternativa atraente <script>.
Rob W
61

A única coisa ausência de Oculto da excelente resposta de Rob W é como se comunicar entre o script da página injetado e o script de conteúdo.

No lado do recebimento (seu script de conteúdo ou o script da página injetada), adicione um ouvinte de evento:

document.addEventListener('yourCustomEvent', function (e) {
  var data = e.detail;
  console.log('received', data);
});

No lado do iniciador (script de conteúdo ou script de página injetado), envie o evento:

var data = {
  allowedTypes: 'those supported by structured cloning, see the list below',
  inShort: 'no DOM elements or classes/functions',
};

document.dispatchEvent(new CustomEvent('yourCustomEvent', { detail: data }));

Notas:

  • O sistema de mensagens DOM usa o algoritmo de clonagem estruturado, que pode transferir apenas alguns tipos de dados , além dos valores primitivos. Ele não pode enviar instâncias de classe, funções ou elementos DOM.
  • No Firefox, para enviar um objeto (ou seja, não um valor primitivo) do script de conteúdo para o contexto da página, é necessário cloná-lo explicitamente no destino usando cloneInto(uma função interna), caso contrário, ocorrerá um erro de violação de segurança .

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
Laktak
fonte
Na verdade, vinculei o código e a explicação na segunda linha da minha resposta a stackoverflow.com/questions/9602022/… .
Rob W
1
Você tem uma referência para o seu método atualizado (por exemplo, um relatório de bug ou um caso de teste?) O CustomEventconstrutor substitui a document.createEventAPI reprovada .
22413 Robbie W
Para mim 'dispatchEvent (new CustomEvent ...' trabalhou tenho Chrome 33. Também não funcionou antes porque eu escrevi o addEventListener depois de injetar o código js..
jscripter
Tenha muito cuidado com o que você passa como seu segundo parâmetro para o CustomEventconstrutor. Eu experimentei dois contratempos muito confusos: 1. simplesmente colocar aspas simples em torno de 'detalhes' fez perplexamente o valor nullquando recebido pelo ouvinte do meu Script de conteúdo. 2. Mais importante, por algum motivo eu tive JSON.parse(JSON.stringify(myData))que fazê-lo, ou ele também se tornaria null. Diante disso, parece-me que a afirmação do desenvolvedor do Chromium a seguir - de que o algoritmo "clone estruturado" é usado automaticamente - não é verdadeira. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk 28/17
Eu acho que a maneira oficial é usar o window.postMessage: developer.chrome.com/extensions/…
Enrique
9

Também enfrentei o problema de ordenar scripts carregados, que foi resolvido através do carregamento sequencial de scripts. O carregamento é baseada na resposta de Rob W .

function scriptFromFile(file) {
    var script = document.createElement("script");
    script.src = chrome.extension.getURL(file);
    return script;
}

function scriptFromSource(source) {
    var script = document.createElement("script");
    script.textContent = source;
    return script;
}

function inject(scripts) {
    if (scripts.length === 0)
        return;
    var otherScripts = scripts.slice(1);
    var script = scripts[0];
    var onload = function() {
        script.parentNode.removeChild(script);
        inject(otherScripts);
    };
    if (script.src != "") {
        script.onload = onload;
        document.head.appendChild(script);
    } else {
        document.head.appendChild(script);
        onload();
    }
}

O exemplo de uso seria:

var formulaImageUrl = chrome.extension.getURL("formula.png");
var codeImageUrl = chrome.extension.getURL("code.png");

inject([
    scriptFromSource("var formulaImageUrl = '" + formulaImageUrl + "';"),
    scriptFromSource("var codeImageUrl = '" + codeImageUrl + "';"),
    scriptFromFile("EqEditor/eq_editor-lite-17.js"),
    scriptFromFile("EqEditor/eq_config.js"),
    scriptFromFile("highlight/highlight.pack.js"),
    scriptFromFile("injected.js")
]);

Na verdade, sou um pouco iniciante no JS, então fique à vontade para me dar um ping nas melhores maneiras.

Dmitry Ginzburg
fonte
3
Essa maneira de inserir scripts não é boa, porque você está poluindo o espaço para nome da página da web. Se a página da web usa uma variável chamada formulaImageUrlor codeImageUrl, você está destruindo efetivamente a funcionalidade da página. Se você deseja passar uma variável para a página da web, sugiro anexar os dados ao elemento de script ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) e usar, por exemplo, (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();no script para acessar os dados.
Rob W
@ RobW, obrigado pela sua observação, embora seja mais sobre a amostra. Você pode esclarecer, por que eu deveria usar o IIFE em vez de apenas obter dataset?
Dmitry Ginzburg
4
document.currentScriptapenas aponta para a tag de script enquanto está em execução. Se você quiser acessar a tag de script e / ou seus atributos / propriedades (por exemplo dataset), precisará armazená-la em uma variável. Precisamos de um IIFE para obter um fechamento para armazenar essa variável sem poluir o espaço para nome global.
Rob W
@RobW excellent! Mas não podemos simplesmente usar algum nome de variável, que dificilmente cruzaria com o existente. É apenas não-idiomático ou podemos ter outros problemas com isso?
Dmitry Ginzburg
2
Você poderia, mas o custo do uso de um IIFE é insignificante, por isso não vejo uma razão para preferir a poluição do namespace a um IIFE. Eu valorizo ​​a certeza de que não irei quebrar a página da web de outras pessoas e a capacidade de usar nomes curtos de variáveis. Outra vantagem do uso de um IIFE é que você pode sair do script mais cedo, se desejado ( return;).
Rob W
6

no script de conteúdo, adiciono uma tag de script à cabeça que liga um manipulador 'onmessage', dentro do manipulador que eu uso, eval para executar o código. No script de conteúdo do estande, também uso o manipulador onmessage, para obter uma comunicação bidirecional. Documentos do Chrome

//Content Script

var pmsgUrl = chrome.extension.getURL('pmListener.js');
$("head").first().append("<script src='"+pmsgUrl+"' type='text/javascript'></script>");


//Listening to messages from DOM
window.addEventListener("message", function(event) {
  console.log('CS :: message in from DOM', event);
  if(event.data.hasOwnProperty('cmdClient')) {
    var obj = JSON.parse(event.data.cmdClient);
    DoSomthingInContentScript(obj);
 }
});

pmListener.js é um ouvinte de URL de postagem

//pmListener.js

//Listen to messages from Content Script and Execute Them
window.addEventListener("message", function (msg) {
  console.log("im in REAL DOM");
  if (msg.data.cmnd) {
    eval(msg.data.cmnd);
  }
});

console.log("injected To Real Dom");

Dessa forma, posso ter uma comunicação bidirecional entre o CS e o Real Dom. É muito útil, por exemplo, se você precisar ouvir eventos do webscoket ou qualquer variável ou evento da memória.

Doron Aviguy
fonte
1

Você pode usar uma função de utilitário que eu criei com a finalidade de executar código no contexto da página e recuperar o valor retornado.

Isso é feito serializando uma função em uma string e injetando-a na página da web.

O utilitário está disponível aqui no GitHub .

Exemplos de uso -



// Some code that exists only in the page context -
window.someProperty = 'property';
function someFunction(name = 'test') {
    return new Promise(res => setTimeout(()=>res('resolved ' + name), 1200));
}
/////////////////

// Content script examples -

await runInPageContext(() => someProperty); // returns 'property'

await runInPageContext(() => someFunction()); // returns 'resolved test'

await runInPageContext(async (name) => someFunction(name), 'with name' ); // 'resolved with name'

await runInPageContext(async (...args) => someFunction(...args), 'with spread operator and rest parameters' ); // returns 'resolved with spread operator and rest parameters'

await runInPageContext({
    func: (name) => someFunction(name),
    args: ['with params object'],
    doc: document,
    timeout: 10000
} ); // returns 'resolved with params object'

Arik
fonte
0

Se você deseja injetar função pura, em vez de texto, você pode usar este método:

function inject(){
    document.body.style.backgroundColor = 'blue';
}

// this includes the function as text and the barentheses make it run itself.
var actualCode = "("+inject+")()"; 

document.documentElement.setAttribute('onreset', actualCode);
document.documentElement.dispatchEvent(new CustomEvent('reset'));
document.documentElement.removeAttribute('onreset');

E você pode passar parâmetros (infelizmente, nenhum objeto e matriz pode ser especificado) para as funções. Adicione-o nos baretheses, assim:

function inject(color){
    document.body.style.backgroundColor = color;
}

// this includes the function as text and the barentheses make it run itself.
var color = 'yellow';
var actualCode = "("+inject+")("+color+")"; 

Tarmo Saluste
fonte
Isso é bem legal ... mas a segunda versão, com uma variável de cor, não funciona para mim ... fico 'não reconhecida' e o código gera um erro ... não a vê como uma variável.
11teenth