Inserte código en el contexto de la página usando un script de contenido

480

Estoy aprendiendo cómo crear extensiones de Chrome. Acabo de comenzar a desarrollar uno para ver los eventos de YouTube. Quiero usarlo con YouTube Flash Player (más adelante intentaré hacerlo compatible con 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!");

El problema es que la consola me da el "¡Iniciado!" , pero no hay "¡Estado cambiado!" cuando reproduzco / detengo videos de YouTube.

Cuando este código se coloca en la consola, funcionó. ¿Qué estoy haciendo mal?

André Alves
fuente
14
intente eliminar las comillas alrededor del nombre de su función:player.addEventListener("onStateChange", state);
Eduardo
2
También es notable que al escribir coincidencias, no olvide incluir https://o http://, esto www.youtube.com/*no le permitiría empacar la extensión y arrojaría un error de separador de esquema faltante
Nilay Vishwakarma

Respuestas:

875

Los scripts de contenido se ejecutan en un entorno de "mundo aislado" . Tienes que inyectar tu state()método en la página misma.

Cuando desee utilizar una de las chrome.*API en el script, debe implementar un controlador de eventos especial, como se describe en esta respuesta: Extensión de Chrome: recuperación del mensaje original de Gmail .

De lo contrario, si no tiene que usar chrome.*API, le recomiendo que inyecte todo su código JS en la página agregando una <script>etiqueta:

Tabla de contenido

  • Método 1: inyectar otro archivo
  • Método 2: inyectar código incrustado
  • Método 2b: uso de una función
  • Método 3: usar un evento en línea
  • Valores dinámicos en el código inyectado

Método 1: inyectar otro archivo

Este es el método más fácil / mejor cuando tienes mucho código. Incluya su código JS real en un archivo dentro de su extensión, digamos script.js. Luego, deje que su script de contenido sea el siguiente (explicado aquí: Google Chome "Acceso directo a la aplicación" Javascript personalizado ):

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: Si usa este método, el script.jsarchivo inyectado debe agregarse a la "web_accessible_resources"sección ( ejemplo ). Si no lo hace, Chrome se negará a cargar su script y mostrará el siguiente error en la consola:

Negar la carga de chrome-extension: // [EXTENSIONID] /script.js. Los recursos deben estar listados en la clave de manifiesto web_accessible_resources para que las páginas que se encuentren fuera de la extensión puedan cargarlos.

Método 2: inyectar código incrustado

Este método es útil cuando desea ejecutar rápidamente un pequeño fragmento de código. (Consulte también: ¿Cómo deshabilitar las teclas de acceso rápido de Facebook con la extensión de 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: los literales de plantilla solo son compatibles con Chrome 41 y versiones posteriores. Si desea que la extensión funcione en 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: uso de una función

Para una gran porción de código, no es factible citar la cadena. En lugar de usar una matriz, se puede usar una función y stringificar:

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 el +operador en cadenas y una función convierte todos los objetos en una cadena. Si tiene la intención de usar el código más de una vez, es aconsejable crear una función para evitar la repetición del código. Una implementación podría verse así:

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

Nota: ¡Dado que la función está serializada, el alcance original y todas las propiedades enlazadas se pierden!

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

Método 3: usar un evento en línea

A veces, desea ejecutar un código inmediatamente, por ejemplo, ejecutar un código antes de <head>crear el elemento. Esto se puede hacer insertando una <script>etiqueta con textContent(ver método 2 / 2b).

Una alternativa, pero no recomendada, es usar eventos en línea. No se recomienda porque si la página define una política de seguridad de contenido que prohíbe los scripts en línea, entonces los escuchas de eventos en línea están bloqueados. Los scripts en línea inyectados por la extensión, por otro lado, aún se ejecutan. Si aún desea utilizar eventos en línea, así es como:

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 supone que no hay otros escuchas de eventos globales que manejen el resetevento. Si lo hay, también puede elegir uno de los otros eventos globales. Simplemente abra la consola de JavaScript (F12), escriba document.documentElement.ony seleccione los eventos disponibles.

Valores dinámicos en el código inyectado

Ocasionalmente, debe pasar una variable arbitraria a la función inyectada. Por ejemplo:

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

Para inyectar este código, debe pasar las variables como argumentos a la función anónima. ¡Asegúrese de implementarlo correctamente! Lo siguiente no 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!

La solución es usar JSON.stringifyantes de pasar el argumento. Ejemplo:

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

Si tiene muchas variables, vale la pena usar JSON.stringifyuna vez, para mejorar la legibilidad, de la siguiente manera:

...
} + ')(' + JSON.stringify([arg1, arg2, arg3, arg4]) + ')';
Rob W
fuente
83
Esta respuesta debería ser parte de documentos oficiales. Los documentos oficiales deben enviarse con la forma recomendada -> 3 formas de hacer lo mismo ... ¿Incorrecto?
Mars Robertson
77
@ Qantas94Heavy El CSP de la extensión no afecta los scripts de contenido. Solo el CSP de la página es relevante. El método 1 se puede bloquear mediante el uso de una script-srcdirectiva que excluye el origen de la extensión, el método 2 se puede bloquear mediante el uso de un CSP que excluye la "línea insegura".
Rob W
3
Alguien preguntó por qué elimino la etiqueta de script usando script.parentNode.removeChild(script);. Mi razón para hacerlo es porque me gusta limpiar mi desorden. Cuando se inserta una secuencia de comandos en línea en el documento, se ejecuta inmediatamente y la <script>etiqueta se puede eliminar de forma segura.
Rob W
99
Otro método: utilizar location.href = "javascript: alert('yeah')";en cualquier parte de su secuencia de comandos de contenido. Es más fácil para fragmentos cortos de código, y también puede acceder a los objetos JS de la página.
Métoule
3
@ChrisP Tenga cuidado con el uso javascript:. El código que abarca varias líneas podría no funcionar como se esperaba. Una línea-comment ( //) truncará el resto, por lo que este se producirá un error: location.href = 'javascript:// Do something <newline> alert(0);';. Esto puede evitarse asegurándose de que utiliza comentarios de varias líneas. Otra cosa a tener cuidado es que el resultado de la expresión debe ser nulo. javascript:window.x = 'some variable';hará que el documento se descargue y se reemplace con la frase 'alguna variable'. Si se usa correctamente, es una alternativa atractiva a <script>.
Rob W
61

La única cosa desaparecido oculto de la excelente respuesta de Rob W está cómo comunicarse entre el guión de la página inyectada y el guión de contenido.

En el lado receptor (ya sea su secuencia de comandos de contenido o la secuencia de comandos de la página inyectada) agregue un detector de eventos:

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

En el lado del iniciador (secuencia de comandos de contenido o secuencia de comandos de página inyectada) envíe el 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:

  • La mensajería DOM utiliza un algoritmo de clonación estructurado, que puede transferir solo algunos tipos de datos además de los valores primitivos. No puede enviar instancias de clase o funciones o elementos DOM.
  • En Firefox, para enviar un objeto (es decir, no un valor primitivo) desde el script de contenido al contexto de la página, debe clonarlo explícitamente en el destino utilizando cloneInto(una función incorporada), de lo contrario fallará con un error de violación de seguridad .

    document.dispatchEvent(new CustomEvent('yourCustomEvent', {
      detail: cloneInto(data, document.defaultView),
    }));
    
laktak
fuente
De hecho, he vinculado el código y la explicación en la segunda línea de mi respuesta, a stackoverflow.com/questions/9602022/… .
Rob W
1
¿Tiene una referencia para su método actualizado (por ejemplo, un informe de error o un caso de prueba?) El CustomEventconstructor reemplaza la document.createEventAPI en desuso .
Rob W
Para mí, 'dispatchEvent (new CustomEvent ...' funcionó. Tengo Chrome 33. Tampoco funcionó antes porque escribí addEventListener después de inyectar el código js.
jscripter
Tenga mucho cuidado con lo que pasa como segundo parámetro al CustomEventconstructor. Experimenté 2 contratiempos muy confusos: 1. simplemente poner comillas simples alrededor de 'detalle' hizo que el valor fuera desconcertante nullcuando fue recibido por el oyente de mi Script de Contenido. 2. Más importante aún, por alguna razón tuve que hacerlo JSON.parse(JSON.stringify(myData))o de lo contrario también se convertiría null. Dado esto, me parece que la siguiente afirmación del desarrollador de Chromium, que el algoritmo de "clon estructurado" se usa automáticamente, no es cierto. bugs.chromium.org/p/chromium/issues/detail?id=260378#c18
jdunk
Creo que la forma oficial es usar window.postMessage: developer.chrome.com/extensions/…
Enrique
9

También me enfrenté al problema de ordenar los scripts cargados, que se resolvió mediante la carga secuencial de scripts. La carga se basa en la respuesta 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();
    }
}

El ejemplo de uso sería:

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")
]);

En realidad, soy un poco nuevo en JS, así que siéntete libre de enviarme un ping a las mejores formas.

Dmitry Ginzburg
fuente
3
Esta forma de insertar scripts no es agradable, porque estás contaminando el espacio de nombres de la página web. Si la página web usa una variable llamada formulaImageUrlo codeImageUrl, está destruyendo efectivamente la funcionalidad de la página. Si desea pasar una variable a la página web, sugiero adjuntar los datos al elemento del script ( e.g. script.dataset.formulaImageUrl = formulaImageUrl;) y usar, por ejemplo, (function() { var dataset = document.currentScript.dataset; alert(dataset.formulaImageUrl;) })();en el script para acceder a los datos.
Rob W
@RobW gracias por tu nota, aunque se trata más de la muestra. ¿Pueden aclarar, por qué debería usar IIFE en lugar de simplemente obtenerlo dataset?
Dmitry Ginzburg
44
document.currentScriptsolo apunta a la etiqueta del script mientras se está ejecutando. Si alguna vez desea acceder a la etiqueta del script y / o sus atributos / propiedades (por ejemplo dataset), debe almacenarla en una variable. Necesitamos un IIFE para obtener un cierre para almacenar esta variable sin contaminar el espacio de nombres global.
Rob W
@RobW excelente! Pero, ¿no podemos usar un nombre de variable que difícilmente se cruzaría con el existente? ¿Es simplemente no idiomático o podemos tener otros problemas?
Dmitry Ginzburg
2
Podría, pero el costo de usar un IIFE es insignificante, por lo que no veo una razón para preferir la contaminación del espacio de nombres sobre un IIFE. Valoro la certeza de que no romperé la página web de otros de alguna manera, y la capacidad de usar nombres de variables cortos. Otra ventaja de usar un IIFE es que puede salir del script antes si lo desea ( return;).
Rob W
6

en la secuencia de comandos de contenido, agrego una etiqueta de secuencia de comandos a la cabeza que une un controlador 'onmessage', dentro del controlador que uso, eval para ejecutar el código. En el script de contenido de la cabina, también uso el controlador onmessage, por lo que obtengo comunicación bidireccional. Documentos de 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 es un oyente de URL de mensaje de publicación

//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");

De esta manera, puedo tener comunicación de 2 vías entre CS a Real Dom. Es muy útil, por ejemplo, si necesita escuchar eventos de webcoket o cualquier evento o variable en memoria.

doron aviguy
fuente
1

Puede usar una función de utilidad que he creado con el fin de ejecutar código en el contexto de la página y recuperar el valor devuelto.

Esto se hace serializando una función en una cadena e inyectándola en la página web.

La utilidad está disponible aquí en GitHub .

Ejemplos 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
fuente
0

Si desea inyectar una función pura, en lugar de texto, puede 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');

Y puede pasar parámetros (desafortunadamente no se pueden clasificar en cadena objetos y matrices) a las funciones. Agréguelo a los baretheses, así:

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
fuente
Esto es bastante bueno ... pero la segunda versión, con una variable de color, no funciona para mí ... me 'no se reconoce' y el código arroja un error ... no lo ve como una variable.
11 de