Transmissão de mensagens

Como os scripts de conteúdo são executados no contexto de uma página da Web, e não da extensão que os executa, muitas vezes eles precisam de maneiras de se comunicar com o restante da extensão. Por exemplo, uma extensão de leitor de RSS pode usar scripts de conteúdo para detectar a presença de um feed RSS em uma página e, em seguida, notificar o service worker para exibir um ícone de ação para essa página.

Essa comunicação usa a transmissão de mensagens, que permite que extensões e scripts de conteúdo escutem as mensagens umas das outras e respondam no mesmo canal. Uma mensagem pode conter qualquer objeto JSON válido (nulo, booleano, número, string, matriz ou objeto). Há duas APIs de transmissão de mensagens: uma para solicitações únicas e outra mais complexa para conexões de longa duração que permitem o envio de várias mensagens. Para mais informações sobre o envio de mensagens entre extensões, consulte a seção Mensagens entre extensões.

Solicitações únicas

Para enviar uma única mensagem para outra parte da extensão e, opcionalmente, receber uma resposta, chame runtime.sendMessage() ou tabs.sendMessage(). Esses métodos permitem enviar uma mensagem serializável única em JSON de um script de conteúdo para a extensão ou da extensão para um script de conteúdo. Para processar a resposta, use a promessa retornada. Para compatibilidade com extensões mais antigas, transmita um callback como o último argumento. Não é possível usar uma promessa e um callback na mesma chamada.

Para informações sobre como converter callbacks em promessas e usá-los em extensões, consulte o guia de migração do Manifest V3.

O envio de uma solicitação de um script de conteúdo é assim:

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

Se você quiser responder a uma mensagem de forma síncrona, chame sendResponse quando tiver a resposta e retorne false para indicar que ela foi concluída. Para responder de forma assíncrona, retorne true para manter o callback sendResponse ativo até que você esteja pronto para usá-lo. Não há suporte para funções assíncronas porque elas retornam uma promessa, que não é aceita.

Para enviar uma solicitação a um script de conteúdo, especifique a guia a que a solicitação se aplica, conforme mostrado abaixo. Esse exemplo funciona em service workers, pop-ups e páginas chrome-extension:// abertas como uma guia.

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

Para receber a mensagem, configure um listener de evento runtime.onMessage. Eles usam o mesmo código em extensões e scripts de conteúdo:

content-script.js ou service-worker.js:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting === "hello")
      sendResponse({farewell: "goodbye"});
  }
);

No exemplo anterior, sendResponse() foi chamado de forma síncrona. Para usar sendResponse() de forma assíncrona, adicione return true; ao manipulador de eventos onMessage.

Se várias páginas estiverem aguardando eventos onMessage, apenas a primeira a chamar sendResponse() para um evento específico vai conseguir enviar a resposta. Todas as outras respostas a esse evento serão ignoradas.

Conexões de longa duração

Para criar um canal de transmissão de mensagens de longa duração reutilizável, chame runtime.connect() para transmitir mensagens de um script de conteúdo para uma página de extensão ou tabs.connect() para transmitir mensagens de uma página de extensão para um script de conteúdo. Você pode nomear seu canal para distinguir diferentes tipos de conexões.

Um possível caso de uso para uma conexão de longa duração é uma extensão automática de preenchimento de formulários. O script de conteúdo pode abrir um canal para a página da extensão para um login específico e enviar uma mensagem para a extensão para cada elemento de entrada na página para solicitar os dados do formulário a serem preenchidos. A conexão compartilhada permite que a extensão compartilhe o estado entre componentes da extensão.

Ao estabelecer uma conexão, cada extremidade recebe um objeto runtime.Port para enviar e receber mensagens por essa conexão.

Use o código abaixo para abrir um canal em um script de conteúdo e enviar e detectar mensagens:

content-script.js:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question === "Madame who?")
    port.postMessage({answer: "Madame... Bovary"});
});

Para enviar uma solicitação da extensão para um script de conteúdo, substitua a chamada para runtime.connect() no exemplo anterior por tabs.connect().

Para processar conexões recebidas de um script de conteúdo ou de uma página de extensão, configure um listener de eventos runtime.onConnect. Quando outra parte da extensão chama connect(), ela ativa esse evento e o objeto runtime.Port. O código para responder às conexões de entrada é semelhante ao seguinte:

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer === "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer === "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

Duração da porta

As portas são projetadas como um método de comunicação bidirecional entre diferentes partes da extensão. Um frame de nível superior é a menor parte de uma extensão que pode usar uma porta. Quando parte de uma extensão chama tabs.connect(), runtime.connect() ou runtime.connectNative(), ela cria uma porta que pode enviar mensagens imediatamente usando postMessage().

Se houver vários frames em uma guia, chamar tabs.connect() invocará o evento runtime.onConnect uma vez para cada frame na guia. Da mesma forma, se runtime.connect() for chamado, o evento onConnect poderá ser disparado uma vez para cada frame no processo de extensão.

Talvez você queira descobrir quando uma conexão está fechada, por exemplo, se você está mantendo estados separados para cada porta aberta. Para fazer isso, detecte o evento runtime.Port.onDisconnect. Esse evento é acionado quando não há portas válidas na outra extremidade do canal, o que pode ter uma das seguintes causas:

  • Não há listeners para runtime.onConnect na outra extremidade.
  • A guia que contém a porta é descarregada (por exemplo, se a guia for navegada).
  • O frame em que connect() foi chamado foi descarregado.
  • Todos os frames que receberam a porta (via runtime.onConnect) foram descarregados.
  • runtime.Port.disconnect() é chamado por a outra extremidade. Se uma chamada connect() resultar em várias portas no lado do receptor e o disconnect() for chamado em qualquer uma dessas portas, o evento onDisconnect só será acionado na porta de envio, não nas outras.

Mensagens entre extensões

Além de enviar mensagens entre diferentes componentes na sua extensão, você pode usar a API de mensagens para se comunicar com outras extensões. Isso permite que você exponha uma API pública para outras extensões.

Para detectar solicitações e conexões recebidas de outras extensões, use os métodos runtime.onMessageExternal ou runtime.onConnectExternal. Confira um exemplo de cada um:

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

Para enviar uma mensagem a outra extensão, transmita o ID da extensão com a qual você quer se comunicar da seguinte maneira:

service-worker.js (link em inglês)

// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

Enviar mensagens de páginas da Web

As extensões também podem receber e responder a mensagens de outras páginas da Web, mas não podem enviar mensagens para páginas da Web. Para enviar mensagens de uma página da Web para uma extensão, especifique em manifest.json quais sites você quer usar para se comunicar usando a chave de manifesto "externally_connectable". Exemplo:

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

Isso expõe a API de mensagens a qualquer página que corresponda aos padrões de URL especificados. O padrão de URL precisa conter pelo menos um domínio de segundo nível. Ou seja, não é possível usar padrões de nome de host como "*", "*.com", "*.co.uk" e "*.appspot.com". A partir do Chrome 107, é possível usar <all_urls> para acessar todos os domínios. Como isso afeta todos os hosts, as avaliações da Chrome Web Store para extensões que a usam podem demorar mais.

Use as APIs runtime.sendMessage() ou runtime.connect() para enviar uma mensagem a um app ou extensão específico. Exemplo:

webpage.js

// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(response) {
    if (!response.success)
      handleError(url);
  });

Na sua extensão, ouça mensagens de páginas da Web usando as APIs runtime.onMessageExternal ou runtime.onConnectExternal, como em mensagens entre extensões. Veja um exemplo:

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

Envio de mensagens nativo

As extensões podem trocar mensagens com aplicativos nativos registrados como um host de mensagens nativo. Para saber mais sobre esse recurso, consulte Mensagens nativas.

Considerações sobre segurança

Confira algumas considerações de segurança relacionadas às mensagens.

Os scripts de conteúdo são menos confiáveis

Os scripts de conteúdo são menos confiáveis do que o service worker da extensão. Por exemplo, uma página da Web maliciosa pode comprometer o processo de renderização que executa os scripts de conteúdo. Suponha que as mensagens de um script de conteúdo possam ter sido criadas por um invasor e valide e limpe todas as entradas. Suponha que qualquer dado enviado para o script de conteúdo possa vazar para a página da Web. Limite o escopo das ações privilegiadas que podem ser acionadas por mensagens recebidas de scripts de conteúdo.

Scripting em vários locais

Proteja seus scripts contra cross-site scripting. Ao receber dados de uma fonte não confiável, como entradas de usuários, outros sites por meio de um script de conteúdo ou uma API, evite interpretá-los como HTML ou usá-los de uma forma que possa permitir a execução de um código inesperado.

Métodos mais seguros

Use APIs que não executam scripts sempre que possível:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  var resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
Métodos não seguros

Evite usar os seguintes métodos que deixem sua extensão vulnerável:

service-worker.js (link em inglês)

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  var resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});