Secuencias de comandos de contenido

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

Comprende las funciones de las secuencias de comandos de contenido

Las secuencias de comandos de contenido pueden acceder directamente a las siguientes APIs de extensión:

Las secuencias de comandos de contenido no pueden acceder a otras APIs directamente. Sin embargo, pueden acceder a ellos de forma indirecta intercambiando mensajes con otras partes de tu extensión.

También puedes acceder a otros archivos de tu extensión desde una secuencia de comandos de contenido con APIs como fetch(). Para ello, debes declararlos como recursos accesibles a través de la Web. Ten en cuenta que esto también expone los recursos a cualquier secuencia de comandos propia o de terceros que se ejecute en el mismo sitio.

Trabajar en mundos aislados

Las secuencias de comandos del contenido viven en un mundo aislado, lo que les permite realizar cambios en su entorno de JavaScript sin entrar en conflicto con la página o las secuencias de comandos del contenido de otras extensiones.

Una extensión puede ejecutarse 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 insertar 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 aparecen en secuencia cuando se hace clic en el botón.

Cómo insertar secuencias de comandos

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

Cómo insertar con declaraciones estáticas

Usa declaraciones de secuencias de comandos de contenido estático en manifest.json para las secuencias de comandos que se deben 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 cadenas y Patrones de coincidencia y glob para obtener información sobre cómo excluir URLs.
css array de cadenas Opcional. Es la lista de archivos CSS que se insertarán en las páginas coincidentes. Se insertan en el orden en que aparecen en este array, antes de que se construya o muestre ningún DOM para la página.
js array de cadenas Opcional. Es 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 cadena de esta lista debe contener una ruta de acceso relativa a un recurso en el directorio raíz de la extensión. Las barras diagonales 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 booleano Opcional. Indica si la secuencia de comandos debe insertarse en un marco about:blank en el que el marco superior o de apertura coincida con uno de los patrones declarados en matches. La configuración predeterminada es "false".
match_origin_as_fallback booleano Opcional. Indica si la secuencia de comandos debe insertarse en marcos que creó un origen coincidente, pero cuya URL o origen pueden no coincidir directamente con el patrón. Estos incluyen marcos con diferentes esquemas, como about:, data:, blob: y filesystem:. Consulta también Cómo insertar en marcos relacionados.
world ExecutionWorld Opcional. Es 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ámico son útiles cuando los patrones de coincidencia de las secuencias de comandos de contenido no son muy 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 la secuencia de comandos de contenido se registra en Chrome con 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 de contenido.
  • Obtén una lista de las secuencias de comandos de contenido registradas.
  • Actualiza la lista de secuencias de comandos de contenido registradas.
  • Quita las 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 insertar de manera programática

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

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

A continuación, se muestran diferentes versiones 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"]
  });
});

También se puede insertar y ejecutar un cuerpo de función 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 insertada 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. Como resultado, el cuerpo de la función debe ser autónomo. Las referencias a variables fuera de la función harán que la secuencia de comandos de contenido arroje un ReferenceError.

Cuando se inserta como una función, también puedes pasarle argumentos.

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 especificada, 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, de otro modo, se insertaría esta secuencia de comandos de contenido. Consulta Patrones de coincidencia para obtener detalles sobre la sintaxis de estas cadenas.
include_globs array de cadenas Opcional. Se aplica después de matches para incluir solo aquellas URLs que también coinciden con este glob. El objetivo es emular la palabra clave @include de Greasemonkey.
exclude_globs array de cadenas Opcional. Se aplica después de matches para excluir las URLs que coincidan con este glob. Su objetivo es emular la palabra clave @exclude de Greasemonkey.

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

  • Su URL coincide con cualquier patrón matches y cualquier patrón include_globs.
  • La URL tampoco coincide con un patrón exclude_matches o exclude_globs. Debido a que la propiedad matches es obligatoria, exclude_matches, include_globs y exclude_globs solo se pueden usar para limitar qué páginas 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 concordancia. Las cadenas de glob aceptables son URLs que pueden contener asteriscos y signos de interrogación comodín. 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:

  • 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 del 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 string Preferida. Usa "document_idle" siempre que sea posible.

El navegador elige un momento para insertar secuencias de comandos entre "document_end" y, de inmediato, después de que se activa el evento window.onload. El momento exacto de la inserción depende de la complejidad del documento y del tiempo que tarda 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 escuchar el evento window.onload, ya que se garantiza que se ejecutarán después de que se complete el DOM. Si una secuencia de comandos debe ejecutarse después de window.onload, la extensión puede verificar si onload ya se activó con la propiedad document.readyState.
document_start string Las secuencias de comandos se insertan después de cualquier archivo de css, pero antes de que se construya cualquier otro DOM o se ejecute cualquier otra secuencia de comandos.
document_end string Las secuencias de comandos se insertan inmediatamente después de que se completa el DOM, pero antes de que se carguen los subrecursos, como las imágenes y los marcos.

Especifica marcos

En el caso de las secuencias de comandos de contenido declarativo especificadas en el manifiesto, 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 la URL especificada 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"]
    }
  ],
  ...
}

Cuando se registran secuencias de comandos de contenido de forma programática con chrome.scripting.registerContentScripts(...), se puede usar el parámetro allFrames para especificar si la secuencia de comandos de contenido se debe insertar en todos los marcos que coincidan con los requisitos de la URL especificada o solo en el marco superior de una pestaña. Esto solo se puede usar con tabId y no se puede usar si se especifican frameIds o documentIds:

service-worker.js

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

Es posible que las extensiones deseen ejecutar secuencias de comandos en marcos que estén relacionados con un marco coincidente, pero que no coincidan. Una situación común cuando esto ocurre es para los marcos con URLs que creó 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 desea insertar en marcos con URLs que tienen esquemas about:, data:, blob: y filesystem:. En estos 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 incluye la URL superior o el origen en la URL, como en about:blank o data:text/html,<html>Hello, World!</html>). Sin embargo, estos marcos aún se pueden asociar con el marco de creación.

Para insertar en estos marcos, 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"]
    }
  ],
  ...
}

Cuando se especifique y se establezca en true, Chrome buscará el origen del iniciador del marco para determinar si coincide, en lugar de la URL del marco en sí. Ten en cuenta que esto también puede ser diferente del origen del fotograma de destino (p.ej., Las URLs de data: tienen un origen nulo).

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

Debido a que esto compara el origen del fotograma del iniciador, el fotograma del iniciador podría estar en cualquier ruta de acceso desde ese origen. Para que esta implicación quede clara, Chrome requiere que las secuencias de comandos de contenido especificadas con "match_origin_as_fallback" establecidas en true también especifiquen una ruta 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 el 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 a través de ella, debe hacerlo a través del DOM compartido.

Se puede obtener un ejemplo con 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 para sí misma. La secuencia de comandos de contenido intercepta y inspecciona este mensaje y, luego, lo publica en el proceso de la extensión. De esta manera, la página establece una línea de comunicación con el proceso de extensión. Lo inverso es posible a través de medios similares.

Cómo acceder a archivos de 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 recursos deben declararse como recursos accesibles a través de 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 en la página web. Si la secuencia de comandos de contenido recibe contenido de un sitio web independiente, por ejemplo, llamando a fetch(), ten cuidado de filtrar el contenido contra ataques de secuencia de comandos entre sitios antes de insertarlo. Comunícate solo a través de HTTPS para evitar ataques de "man-in-the-middle".

Asegúrate de filtrar las páginas web maliciosas. Por ejemplo, los siguientes patrones son peligrosos y no se permiten en el manifiesto 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, prefiere APIs más seguras que no ejecuten secuencias de comandos:

Qué debes hacer

content-script.js

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

content-script.js

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