Message transmis

Étant donné que les scripts de contenu s'exécutent dans le contexte d'une page Web, et non de l'extension qui les exécute, ils ont souvent besoin de moyens de communiquer avec le reste de l'extension. Par exemple, une extension de lecteur RSS peut utiliser des scripts de contenu pour détecter la présence d'un flux RSS sur une page, puis avertir le service worker pour qu'il affiche une icône d'action pour cette page.

Cette communication utilise la transmission de messages, ce qui permet aux extensions et aux scripts de contenu de s'écouter mutuellement et de répondre sur le même canal. Un message peut contenir n'importe quel objet JSON valide (valeur nulle, booléenne, numérique, chaîne, tableau ou objet). Il existe deux API de transfert de messages: une pour les requêtes ponctuelles et une plus complexe pour les connexions durables qui permettent d'envoyer plusieurs messages. Pour en savoir plus sur l'envoi de messages entre les extensions, consultez la section Messages inter-extensions.

Requêtes ponctuelles

Pour envoyer un seul message à une autre partie de votre extension et éventuellement obtenir une réponse, appelez runtime.sendMessage() ou tabs.sendMessage(). Ces méthodes vous permettent d'envoyer un message JSON sérialisable unique à partir d'un script de contenu vers l'extension ou de l'extension vers un script de contenu. Pour gérer la réponse, utilisez la promesse renvoyée. Pour assurer la rétrocompatibilité avec les anciennes extensions, vous pouvez transmettre un rappel en tant que dernier argument. Vous ne pouvez pas utiliser une promesse et un rappel dans le même appel.

Pour savoir comment convertir des rappels en promesses et les utiliser dans des extensions, consultez le guide de migration vers Manifest V3.

Voici à quoi ressemble l'envoi d'une requête à partir d'un script de contenu:

content-script.js:

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

Si vous souhaitez répondre de manière synchrone à un message, il vous suffit d'appeler sendResponse une fois que vous avez la réponse et de renvoyer false pour indiquer que vous avez terminé. Pour répondre de manière asynchrone, renvoyez true pour maintenir le rappel sendResponse actif jusqu'à ce que vous soyez prêt à l'utiliser. Les fonctions asynchrones ne sont pas acceptées, car elles renvoient une promesse, qui n'est pas acceptée.

Pour envoyer une requête à un script de contenu, spécifiez l'onglet auquel la requête s'applique, comme indiqué ci-dessous. Cet exemple fonctionne dans les service workers, les pop-ups et les pages chrome-extension:// ouvertes sous forme d'onglet.

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

Pour recevoir le message, configurez un écouteur d'événements runtime.onMessage. Ils utilisent le même code dans les extensions et les scripts de contenu:

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

Dans l'exemple précédent, sendResponse() a été appelé de manière synchrone. Pour utiliser sendResponse() de manière asynchrone, ajoutez return true; au gestionnaire d'événements onMessage.

Si plusieurs pages écoutent des événements onMessage, seule la première à appeler sendResponse() pour un événement particulier pourra envoyer la réponse. Toutes les autres réponses à cet événement seront ignorées.

Connexions de longue durée

Pour créer un canal de transmission de messages réutilisable et durable, appelez runtime.connect() pour transmettre des messages d'un script de contenu à une page d'extension, ou tabs.connect() pour transmettre des messages d'une page d'extension à un script de contenu. Vous pouvez nommer votre canal pour distinguer les différents types de connexions.

Une extension de saisie automatique de formulaires est un cas d'utilisation potentiel d'une connexion longue durée. Le script de contenu peut ouvrir un canal vers la page de l'extension pour une connexion spécifique et envoyer un message à l'extension pour chaque élément de saisie de la page afin de demander les données du formulaire à renseigner. La connexion partagée permet à l'extension de partager l'état entre ses composants.

Lors de l'établissement d'une connexion, un objet runtime.Port est attribué à chaque extrémité pour envoyer et recevoir des messages via cette connexion.

Utilisez le code suivant pour ouvrir un canal à partir d'un script de contenu, et envoyer et écouter des messages:

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

Pour envoyer une requête de l'extension à un script de contenu, remplacez l'appel à runtime.connect() dans l'exemple précédent par tabs.connect().

Pour gérer les connexions entrantes pour un script de contenu ou une page d'extension, configurez un écouteur d'événements runtime.onConnect. Lorsqu'une autre partie de votre extension appelle connect(), elle active cet événement et l'objet runtime.Port. Le code permettant de répondre aux connexions entrantes se présente comme suit:

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."});
  });
});

Durée de vie des ports

Les ports sont conçus comme une méthode de communication bidirectionnelle entre les différentes parties de l'extension. Un frame de niveau supérieur est la plus petite partie d'une extension pouvant utiliser un port. Lorsqu'une partie d'une extension appelle tabs.connect(), runtime.connect() ou runtime.connectNative(), elle crée un port pouvant envoyer immédiatement des messages à l'aide de postMessage().

Si un onglet contient plusieurs cadres, l'appel de tabs.connect() appelle l'événement runtime.onConnect une fois pour chaque cadre de l'onglet. De même, si runtime.connect() est appelé, l'événement onConnect peut se déclencher une fois pour chaque frame du processus d'extension.

Vous pouvez déterminer quand une connexion est fermée, par exemple si vous gérez des états distincts pour chaque port ouvert. Pour ce faire, écoutez l'événement runtime.Port.onDisconnect. Cet événement se déclenche lorsqu'il n'y a pas de ports valides à l'autre extrémité du canal, ce qui peut être dû à l'une des raisons suivantes:

  • Il n'y a pas d'écouteurs pour runtime.onConnect à l'autre extrémité.
  • L'onglet contenant le port est déchargé (par exemple, si l'onglet est parcouru).
  • Le frame dans lequel connect() a été appelé a été déchargé.
  • Toutes les trames ayant reçu le port (via runtime.onConnect) ont été déchargées.
  • runtime.Port.disconnect() est appelé par l'autre extrémité. Si un appel connect() génère plusieurs ports du côté du destinataire et que disconnect() est appelé sur l'un de ces ports, l'événement onDisconnect ne se déclenche que sur le port d'envoi, et non sur les autres ports.

Messages inter-extensions

En plus d'envoyer des messages entre les différents composants de votre extension, vous pouvez utiliser l'API de messagerie pour communiquer avec d'autres extensions. Vous pouvez ainsi exposer une API publique que d'autres extensions peuvent utiliser.

Pour écouter les requêtes entrantes et les connexions d'autres extensions, utilisez les méthodes runtime.onMessageExternal ou runtime.onConnectExternal. Voici un exemple de chacun:

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

Pour envoyer un message à une autre extension, transmettez l'ID de l'extension avec laquelle vous souhaitez communiquer comme suit:

service-worker.js

// 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(...);

Envoyer des messages depuis des pages Web

Les extensions peuvent également recevoir et répondre aux messages d'autres pages Web, mais elles ne peuvent pas envoyer de messages à des pages Web. Pour envoyer des messages d'une page Web à une extension, spécifiez dans votre manifest.json les sites Web avec lesquels vous souhaitez communiquer à l'aide de la clé manifeste "externally_connectable". Exemple :

manifest.json

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

L'API de messagerie est ainsi exposée à toutes les pages correspondant aux formats d'URL que vous spécifiez. Le format d'URL doit contenir au moins un domaine de deuxième niveau. En d'autres termes, les formats de nom d'hôte tels que "*", "*.com", "*.co.uk" et "*.appspot.com" ne sont pas acceptés. À partir de Chrome 107, vous pourrez utiliser <all_urls> pour accéder à tous les domaines. Notez que, comme il affecte tous les hôtes, l'examen des extensions qui l'utilisent sur le Chrome Web Store peut prendre plus de temps.

Utilisez les API runtime.sendMessage() ou runtime.connect() pour envoyer un message à une application ou une extension spécifique. Exemple :

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

À partir de votre extension, écoutez les messages des pages Web à l'aide des API runtime.onMessageExternal ou runtime.onConnectExternal, comme dans les messages inter-extensions. Exemple :

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

Messagerie native

Les extensions peuvent échanger des messages avec des applications natives enregistrées en tant qu'hôte de messagerie natif. Pour en savoir plus sur cette fonctionnalité, consultez la section Messagerie native.

Considérations de sécurité

Voici quelques considérations de sécurité concernant la messagerie.

Les scripts de contenu sont moins fiables

Les scripts de contenu sont moins fiables que le service worker de l'extension. Par exemple, une page Web malveillante peut compromettre le processus de rendu qui exécute les scripts de contenu. Partez du principe que les messages d'un script de contenu peuvent avoir été créés par un pirate informatique et veillez à valider et à nettoyer toutes les entrées. Supposons que toutes les données envoyées au script de contenu puissent fuir vers la page Web. Limitez la portée des actions privilégiées pouvant être déclenchées par les messages reçus à partir de scripts de contenu.

Script intersites

Veillez à protéger vos scripts contre les scripts intersites. Lorsque vous recevez des données provenant d'une source non fiable, telle qu'une entrée utilisateur, d'autres sites Web via un script de contenu ou une API, veillez à ne pas interpréter cela comme du code HTML ou l'utiliser d'une manière qui pourrait permettre l'exécution de code inattendu.

Méthodes plus sûres

Utilisez autant que possible des API qui n'exécutent pas de scripts:

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éthodes non sécurisées

Évitez d'utiliser les méthodes suivantes, qui rendent votre extension vulnérable:

service-worker.js

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