Secuencias de comandos de contenido

Las secuencias de comandos de contenido son archivos que se ejecutan en el contexto de páginas web. Con el Modelo de objetos del documento (DOM) estándar, pueden leer los detalles de las páginas web que visita el navegador, aplicar cambios y pasar información a su extensión superior.

Comprende las capacidades de las secuencias de comandos de contenido

Las secuencias de comandos de contenido pueden acceder a los archivos de la extensión después de declararlos como recursos accesibles a través de la Web. Pueden acceder a las siguientes APIs de extensión directamente:

Las secuencias de comandos de contenido no pueden acceder a otras API de forma directa. Sin embargo, pueden acceder a ellas de forma indirecta intercambiando mensajes con otras partes de la extensión.

Trabajar en mundos aislados

Las secuencias de comandos de contenido se encuentran en un mundo aislado, lo que permite que una secuencia de comandos de contenido realice cambios en su entorno de JavaScript sin entrar en conflicto con las secuencias de comandos del contenido de la página o de otras extensiones.

Se puede ejecutar una extensión en una página web con un código similar al siguiente ejemplo.

webPage.html

<html>
  <button id="mybutton">click me</button>
  <script>
    var greeting = "hello, ";
    var button = document.getElementById("mybutton");
    button.person_name = "Bob";
    button.addEventListener(
        "click", () => alert(greeting + button.person_name + "."), false);
  </script>
</html>

Esa extensión podría insertar la siguiente secuencia de comandos de contenido con una de las técnicas que se describen en la sección Cómo inyectar secuencias de comandos.

content-script.js

var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
    "click", () => alert(greeting + button.person_name + "."), false);

Con este cambio, ambas alertas aparecerán en secuencia cuando se haga clic en el botón.

Cómo inyectar secuencias de comandos

Las secuencias de comandos de contenido se pueden declarar de forma estática, de forma dinámica o inyectarse de manera programática.

Cómo realizar inyecciones con declaraciones estáticas

Usa declaraciones de secuencias de comandos de contenido estáticas en manifest.json para las secuencias de comandos que se deban ejecutar automáticamente en un conjunto conocido de páginas.

Las secuencias de comandos declaradas de forma estática se registran en el manifiesto con la clave "content_scripts". Pueden incluir archivos JavaScript, archivos CSS o ambos. Todas las secuencias de comandos de contenido de ejecución automática deben especificar patrones de coincidencia.

manifest.json

{
 "name": "My extension",
 ...
 "content_scripts": [
   {
     "matches": ["https://*.nytimes.com/*"],
     "css": ["my-styles.css"],
     "js": ["content-script.js"]
   }
 ],
 ...
}

Nombre Tipo Descripción
matches array de cadenas Obligatorio. Especifica en qué páginas se insertará esta secuencia de comandos de contenido. Consulta Patrones de coincidencia para obtener detalles sobre la sintaxis de estas strings y Patrones y globs para obtener información sobre cómo excluir URLs.
css array de cadenas Opcional. La lista de archivos CSS que se insertarán en las páginas coincidentes. Estos se insertan en el orden en que aparecen en este array, antes de que se construya cualquier DOM o se muestre para la página.
js array de cadenas Opcional. La lista de archivos JavaScript que se insertarán en las páginas coincidentes. Los archivos se insertan en el orden en que aparecen en este array. Cada string de esta lista debe contener una ruta de acceso relativa a un recurso en el directorio raíz de la extensión. Las barras iniciales (`/`) se cortan automáticamente.
run_at RunAt Opcional. Especifica cuándo se debe insertar la secuencia de comandos en la página. La configuración predeterminada es document_idle.
match_about_blank boolean Opcional. Indica si la secuencia de comandos debe insertarse en un marco about:blank en el que el marco superior o de apertura coincide con uno de los patrones declarados en matches. La configuración predeterminada es "false".
match_origin_as_fallback boolean Opcional. Indica si la secuencia de comandos debe insertarse en marcos creados por un origen coincidente, pero cuya URL u origen no coinciden directamente con el patrón. Entre ellos, se incluyen marcos con diferentes esquemas, como about:, data:, blob: y filesystem:. Consulta también Cómo insertar en marcos relacionados.
world ExecutionWorld Opcional. El mundo de JavaScript en el que se ejecuta una secuencia de comandos. La configuración predeterminada es ISOLATED. Consulta también Trabaja en mundos aislados.

Cómo insertar con declaraciones dinámicas

Las secuencias de comandos de contenido dinámicas son útiles cuando los patrones de coincidencia de las secuencias de comandos de contenido no son conocidos o cuando las secuencias de comandos de contenido no siempre deben insertarse en hosts conocidos.

Las declaraciones dinámicas, que se introdujeron en Chrome 96, son similares a las declaraciones estáticas, pero el objeto de secuencia de comandos de contenido se registra en Chrome mediante métodos en el espacio de nombres chrome.scripting en lugar de en manifest.json. La API de Scripting también permite a los desarrolladores de extensiones hacer lo siguiente:

  • Registra las secuencias de comandos del contenido.
  • Obtén una lista de secuencias de comandos de contenido registradas.
  • Actualiza la lista de secuencias de comandos del contenido registradas.
  • Quita secuencias de comandos de contenido registradas.

Al igual que las declaraciones estáticas, las declaraciones dinámicas pueden incluir archivos JavaScript, archivos CSS o ambos.

service-worker.js

chrome.scripting
  .registerContentScripts([{
    id: "session-script",
    js: ["content.js"],
    persistAcrossSessions: false,
    matches: ["*://example.com/*"],
    runAt: "document_start",
  }])
  .then(() => console.log("registration complete"))
  .catch((err) => console.warn("unexpected error", err))

service-worker.js

chrome.scripting
  .updateContentScripts([{
    id: "session-script",
    excludeMatches: ["*://admin.example.com/*"],
  }])
  .then(() => console.log("registration updated"));

service-worker.js

chrome.scripting
  .getRegisteredContentScripts()
  .then(scripts => console.log("registered content scripts", scripts));

service-worker.js

chrome.scripting
  .unregisterContentScripts({ ids: ["session-script"] })
  .then(() => console.log("un-registration complete"));

Cómo inyectar de manera programática

Usa la inserción programática para las secuencias de comandos de contenido que deban ejecutarse en respuesta a eventos o en ocasiones específicas.

Para insertar una secuencia de comandos de contenido de manera programática, la extensión necesita permisos de host para la página en la que intenta insertar secuencias de comandos. Para otorgar los permisos de host, puedes solicitarlos como parte del manifiesto de tu extensión o usar "activeTab" de forma temporal.

A continuación, se muestran versiones diferentes de una extensión basada en activeTab.

manifest.json:

{
  "name": "My extension",
  ...
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_title": "Action Button"
  }
}

Las secuencias de comandos de contenido se pueden insertar como archivos.

content-script.js


document.body.style.backgroundColor = "orange";

service-worker.js:

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ["content-script.js"]
  });
});

O bien, el cuerpo de una función se puede insertar y ejecutar como una secuencia de comandos de contenido.

service-worker.js:

function injectedFunction() {
  document.body.style.backgroundColor = "orange";
}

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
  });
});

Ten en cuenta que la función inyectada es una copia de la función a la que se hace referencia en la llamada a chrome.scripting.executeScript(), no la función original en sí. Como resultado, el cuerpo de la función debe ser independiente. Las referencias a variables fuera de la función harán que la secuencia de comandos de contenido arroje una ReferenceError.

Cuando realizas una inyección como una función, también puedes pasar argumentos a la función.

service-worker.js

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

chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target : {tabId : tab.id},
    func : injectedFunction,
    args : [ "orange" ],
  });
});

Excluye coincidencias y globs

Para personalizar la coincidencia de páginas especificadas, incluye los siguientes campos en un registro declarativo.

Nombre Tipo Descripción
exclude_matches array de cadenas Opcional. Excluye las páginas en las que se insertaría esta secuencia de comandos de contenido. Consulta Patrones de coincidencia para obtener detalles sobre la sintaxis de estas strings.
include_globs array de cadenas Opcional. Se aplica después de matches para incluir solo las URLs que también coinciden con este glob. El objetivo de esto es emular la palabra clave @include de Greasemonkey.
exclude_globs array de cadena Opcional. Se aplica después de matches para excluir las URLs que coinciden con este glob. Su objetivo es emular la palabra clave @exclude de Greasemonkey.

La secuencia de comandos del contenido se insertará en una página si se cumplen las siguientes condiciones:

  • Su URL coincide con cualquier patrón matches y include_globs.
  • La URL tampoco coincide con un patrón exclude_matches o exclude_globs. Dado que la propiedad matches es obligatoria, exclude_matches, include_globs y exclude_globs solo se pueden usar para limitar las páginas que se verán afectadas.

La siguiente extensión inserta la secuencia de comandos de contenido en https://www.nytimes.com/health, pero no en https://www.nytimes.com/business .

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  excludeMatches : [ "*://*/*business*" ],
  js : [ "contentScript.js" ],
}]);

Las propiedades glob siguen una sintaxis diferente y más flexible que los patrones de coincidencia. Las cadenas glob aceptables son las URLs que pueden contener asteriscos y signos de interrogación "comodines". El asterisco (*) coincide con cualquier cadena de cualquier longitud, incluida la cadena vacía, mientras que el signo de interrogación (?) coincide con cualquier carácter.

Por ejemplo, el glob https://???.example.com/foo/\* coincide con cualquiera de los siguientes elementos:

  • https://www.example.com/foo/bar
  • https://the.example.com/foo/

Sin embargo, no coincide con lo siguiente:

  • https://my.example.com/foo/bar
  • https://example.com/foo/
  • https://www.example.com/foo

Esta extensión inserta la secuencia de comandos de contenido en https://www.nytimes.com/arts/index.html y https://www.nytimes.com/jobs/index.htm*, pero no en https://www.nytimes.com/sports/index.html:

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

Esta extensión inserta la secuencia de comandos de contenido en https://history.nytimes.com y https://.nytimes.com/history, pero no en https://science.nytimes.com ni https://www.nytimes.com/science:

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

Se puede incluir uno, todos o algunos de estos para lograr el alcance correcto.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "exclude_matches": ["*://*/*business*"],
      "include_globs": ["*nytimes.com/???s/*"],
      "exclude_globs": ["*science*"],
      "js": ["contentScript.js"]
    }
  ],
  ...
}

Tiempo de ejecución

El campo run_at controla cuándo se insertan los archivos JavaScript en la página web. El valor preferido y predeterminado es "document_idle". Consulta el tipo RunAt para ver otros valores posibles.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "run_at": "document_idle",
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id : "test",
  matches : [ "https://*.nytimes.com/*" ],
  runAt : "document_idle",
  js : [ "contentScript.js" ],
}]);
Nombre Tipo Descripción
document_idle cadena Preferido. Usa "document_idle" siempre que sea posible.

El navegador elige un momento para insertar secuencias de comandos entre "document_end" e inmediatamente después de que se activa el evento window.onload. El momento exacto de la inyección depende de qué tan complejo sea el documento y cuánto tarde en cargarse, y está optimizado para la velocidad de carga de la página.

Las secuencias de comandos de contenido que se ejecutan en "document_idle" no necesitan detectar el evento window.onload; se garantiza que se ejecutarán una vez que se complete el DOM. Si sin dudas se debe ejecutar una secuencia de comandos después de window.onload, la extensión puede verificar si onload ya se activó con la propiedad document.readyState.
document_start cadena Las secuencias de comandos se insertan después de los archivos de css, pero antes de que se construya cualquier otro DOM o se ejecute cualquier otra secuencia de comandos.
document_end cadena Las secuencias de comandos se insertan inmediatamente después de que se completa el DOM, pero antes de que se carguen subrecursos, como las imágenes y los marcos.

Cómo especificar marcos

El campo "all_frames" permite que la extensión especifique si los archivos JavaScript y CSS se deben insertar en todos los marcos que coincidan con los requisitos de URL especificados o solo en el marco superior de una pestaña.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.nytimes.com/*"],
      "all_frames": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

service-worker.js

chrome.scripting.registerContentScripts([{
  id: "test",
  matches : [ "https://*.nytimes.com/*" ],
  allFrames : true,
  js : [ "contentScript.js" ],
}]);
Nombre Tipo Descripción
all_frames boolean Opcional. El valor predeterminado es false, lo que significa que solo coincide el fotograma superior.

Si se especifica true, se insertarán todos los fotogramas, incluso si no es el marco superior de la pestaña. Cada marco se comprueba de forma independiente para determinar los requisitos de URL. No se insertará en marcos secundarios si no se cumplen los requisitos de la URL.

Es posible que las extensiones quieran ejecutar secuencias de comandos en marcos relacionados con un marco coincidente, pero que no coinciden. Una situación común cuando este es el caso es para marcos con URLs que se crearon mediante un marco coincidente, pero cuyas URLs no coinciden con los patrones especificados de la secuencia de comandos.

Este es el caso cuando una extensión quiere insertar fotogramas con URLs que tienen los esquemas about:, data:, blob: y filesystem:. En esos casos, la URL no coincidirá con el patrón de la secuencia de comandos de contenido (y, en el caso de about: y data:, ni siquiera incluyas el origen ni la URL superior en la URL, como en about:blank o data:text/html,<html>Hello, World!</html>). Sin embargo, estos marcos aún pueden asociarse con el marco de creación.

Para insertarlos en estos fotogramas, las extensiones pueden especificar la propiedad "match_origin_as_fallback" en una especificación de secuencia de comandos de contenido en el manifiesto.

manifest.json

{
  "name": "My extension",
  ...
  "content_scripts": [
    {
      "matches": ["https://*.google.com/*"],
      "match_origin_as_fallback": true,
      "js": ["contentScript.js"]
    }
  ],
  ...
}

Si se especifica y se establece en true, Chrome analizará el origen del iniciador del marco para determinar si este coincide, en lugar de la URL del marco. Ten en cuenta que esto también puede ser diferente del origen del marco de destino (p.ej., Las URLs de data: tienen un origen nulo).

El iniciador del marco es el que creó o navegó por el fotograma de destino. Si bien suele ser el elemento superior directo o el iniciador, puede que no lo sea (como en el caso de un marco que navega por un iframe dentro de un iframe).

Como esto compara el origen del marco de iniciador, este puede estar en cualquier ruta desde ese origen. Para aclarar esta implicación, Chrome requiere que cualquier secuencia de comandos de contenido que se especifique con "match_origin_as_fallback" configurado como true también especifique una ruta de acceso de *.

Cuando se especifican "match_origin_as_fallback" y "match_about_blank", "match_origin_as_fallback" tiene prioridad.

Comunicación con la página de incorporación

Aunque los entornos de ejecución de las secuencias de comandos de contenido y las páginas que las alojan están aislados entre sí, comparten acceso al DOM de la página. Si la página desea comunicarse con la secuencia de comandos de contenido o con la extensión mediante la secuencia de comandos de contenido, debe hacerlo mediante el DOM compartido.

Se puede lograr un ejemplo usando window.postMessage():

content-script.js

var port = chrome.runtime.connect();

window.addEventListener("message", (event) => {
  // We only accept messages from ourselves
  if (event.source !== window) {
    return;
  }

  if (event.data.type && (event.data.type === "FROM_PAGE")) {
    console.log("Content script received: " + event.data.text);
    port.postMessage(event.data.text);
  }
}, false);

example.js

document.getElementById("theButton").addEventListener("click", () => {
  window.postMessage(
      {type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);

La página que no es de extensión, example.html, publica mensajes en sí misma. La secuencia de comandos de contenido intercepta e inspecciona este mensaje y, luego, se publica en el proceso de extensión. De esta manera, la página establece una línea de comunicación con el proceso de extensión. Lo contrario es posible con medios similares.

Cómo acceder a los archivos de la extensión

Para acceder a un archivo de extensión desde una secuencia de comandos de contenido, puedes llamar a chrome.runtime.getURL() para obtener la URL absoluta de tu recurso de extensión, como se muestra en el siguiente ejemplo (content.js):

content-script.js

let image = chrome.runtime.getURL("images/my_image.png")

Para usar fuentes o imágenes en un archivo CSS, puedes usar @@extension_id para construir una URL, como se muestra en el siguiente ejemplo (content.css):

content.css

body {
 background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
}

@font-face {
 font-family: 'Stint Ultra Expanded';
 font-style: normal;
 font-weight: 400;
 src: url('chrome-extension://__MSG_@@extension_id__/fonts/Stint Ultra Expanded.woff') format('woff');
}

Todos los elementos se deben declarar como recursos accesibles desde la Web en el archivo manifest.json:

manifest.json

{
 ...
 "web_accessible_resources": [
   {
     "resources": [ "images/*.png" ],
     "matches": [ "https://example.com/*" ]
   },
   {
     "resources": [ "fonts/*.woff" ],
     "matches": [ "https://example.com/*" ]
   }
 ],
 ...
}

Mantente protegido

Si bien los mundos aislados proporcionan una capa de protección, el uso de secuencias de comandos de contenido puede crear vulnerabilidades en una extensión y la página web. Si la secuencia de comandos del contenido recibe contenido de un sitio web independiente, por ejemplo, cuando llama a fetch(), asegúrate de filtrar el contenido en función de ataques de secuencia de comandos entre sitios antes de insertarlo. Comunícate solo a través de HTTPS para evitar los ataques de "man-in-the-middle".

Asegúrate de filtrar las páginas web maliciosas. Por ejemplo, los siguientes patrones son peligrosos y no están permitidos en Manifest V3:

Qué no debes hacer

content-script.js

const data = document.getElementById("json-data");
// WARNING! Might be evaluating an evil script!
const parsed = eval("(" + data + ")");
Qué no debes hacer

content-script.js

const elmt_id = ...
// WARNING! elmt_id might be '); ... evil script ... //'!
window.setTimeout("animate(" + elmt_id + ")", 200);

En su lugar, es preferible usar APIs más seguras que no ejecuten secuencias de comandos:

content-script.js

const data = document.getElementById("json-data")
// JSON.parse does not evaluate the attacker's scripts.
const parsed = JSON.parse(data);

content-script.js

const elmt_id = ...
// The closure form of setTimeout does not evaluate scripts.
window.setTimeout(() => animate(elmt_id), 200);