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 demander au service worker d'afficher 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 d'écouter mutuellement leurs messages et de répondre sur le même canal. Un message peut contenir tout objet JSON valide (null, booléen, nombre, chaîne, tableau ou objet). Il existe deux API de transmission de messages: une pour les requêtes ponctuelles et une plus complexe pour les connexions de longue durée qui permettent l'envoi de plusieurs messages. Pour en savoir plus sur l'envoi de messages entre les extensions, consultez la section Messages multi-extensions.

Requêtes uniques

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 sérialisable JSON 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 extensions plus anciennes, vous pouvez transmettre un rappel en tant que dernier argument. Vous ne pouvez pas utiliser de promesse et de rappel dans le même appel.

Pour plus d'informations sur la conversion des rappels en promesses et sur leur utilisation dans les extensions, consultez le guide de migration vers Manifest V3.

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

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 fenêtres pop-up et les pages chrome-extension:// ouvertes dans un 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. Celles-ci 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 réussira à 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éutilisables et à longue durée de vie, appelez runtime.connect() pour transmettre les messages d'un script de contenu vers une page d'extension, ou tabs.connect() pour transmettre les messages d'une page d'extension à un script de contenu. Vous pouvez nommer votre chaîne pour distinguer les différents types de connexions.

L'extension de remplissage automatique de formulaire 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 d'entrée de la page afin de demander les données de formulaire à remplir. La connexion partagée permet à l'extension de partager l'état entre les composants d'extension.

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, puis 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 du port

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

S'il y a plusieurs frames dans un onglet, l'appel de tabs.connect() appelle l'événement runtime.onConnect une fois pour chaque frame 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 voudrez peut-être savoir quand une connexion est fermée, par exemple si vous conservez 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 port valide à l'autre extrémité du canal. Cela peut avoir l'une des causes suivantes:

  • Il n'y a pas d'écouteur pour runtime.onConnect à l'autre extrémité.
  • L'onglet contenant le port est déchargé (par exemple, s'il est parcouru).
  • Le frame dans lequel connect() a été appelé a été déchargé.
  • Toutes les trames qui ont 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() entraîne plusieurs ports du côté du récepteur 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 sur les extensions multiples

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

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

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. Pour ce faire, procédez 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 à partir de pages Web

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

manifest.json

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

L'API de messagerie est ainsi exposée sur 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. Autrement dit, 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 pouvez utiliser <all_urls> pour accéder à tous les domaines. Étant donné qu'elle affecte tous les hôtes, l'examen des extensions qui utilisent le Chrome Web Store sur les extensions qui l'utilisent 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);
  });

Depuis votre extension, écoutez les messages des pages Web à l'aide des API runtime.onMessageExternal ou runtime.onConnectExternal, comme dans la messagerie multi-extension. 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 native. Pour en savoir plus sur cette fonctionnalité, consultez l'article Messages natifs.

Points à noter concernant la sécurité

Voici quelques points à prendre en compte concernant la sécurité des messages.

Les scripts de contenu sont moins fiables

Les scripts de contenu sont moins fiables que le service worker d'extension. Par exemple, une page Web malveillante peut compromettre le processus d'affichage qui exécute les scripts de contenu. Supposons que les messages d'un script de contenu aient été rédigés par un pirate informatique et assurez-vous de valider et nettoyer toutes les entrées. Supposons que des données envoyées au script de contenu puissent être transmises à la page Web. Limitez le champ d'application des actions privilégiées pouvant être déclenchées par des messages provenant de scripts de contenu.

Script intersites

Assurez-vous de 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, évitez de les interpréter comme du code HTML ou de l'utiliser d'une manière qui pourrait permettre l'exécution d'un code inattendu.

Méthodes plus sûres

Dans la mesure du possible, utilisez 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;
});