É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 transmission 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 comme dernier argument. Vous ne pouvez pas utiliser une promesse et un rappel dans le même appel.
Lorsque vous envoyez un message, un troisième argument facultatif, sendResponse
, est transmis à l'écouteur d'événements qui le gère. Il s'agit d'une fonction qui utilise un objet sérialisable au format JSON qui est utilisé comme valeur renvoyée à la fonction qui a envoyé le message. Par défaut, le rappel sendResponse
doit être appelé de manière synchrone. Si vous souhaitez effectuer une tâche asynchrone pour obtenir la valeur transmise à sendResponse
, vous devez renvoyer un littéral true
(et pas seulement une valeur vraie) à partir de l'écouteur d'événements. Le canal de messages reste ainsi ouvert à l'autre extrémité jusqu'à l'appel de sendResponse
.
// Event listener
function handleMessages(message, sender, sendResponse) {
fetch(message.url)
.then((response) => sendResponse({statusCode: response.status}))
// Since `fetch` is asynchronous, must send an explicit `true`
return true;
}
// Message sender
const {statusCode} = await chrome.runtime.sendMessage({
url: 'https://example.com'
});
Pour savoir comment convertir des rappels en promesses et les utiliser dans des 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);
})();
Si vous souhaitez répondre de manière synchrone à un message, appelez simplement sendResponse
une fois que vous avez la réponse, puis renvoyez false
pour indiquer que c'est 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 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
. 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 parviendra à 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 les composants de l'extension.
Lors de l'établissement d'une connexion, un objet runtime.Port
est attribué à chaque extrémité pour l'envoi et la réception de 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 d'un script de contenu ou d'une page d'extension, configurez un écouteur d'événement 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 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 consulté).
- 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 appelconnect()
génère plusieurs ports du côté du destinataire et quedisconnect()
est appelé sur l'un de ces ports, l'événementonDisconnect
ne se déclenche que sur le port d'envoi, et non sur les autres ports.
Messagerie 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 minimal 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. Autrement dit, les formats 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. 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.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';
// Check if extension is installed
if (chrome && chrome.runtime) {
// Make a request:
chrome.runtime.sendMessage(
editorExtensionId,
{
openUrlInEditor: url
},
(response) => {
if (!response.success) handleError(url);
}
);
}
Dans votre extension, écoutez les messages provenant de pages Web à l'aide des API runtime.onMessageExternal
ou runtime.onConnectExternal
, comme pour la messagerie 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 fuiter 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 attaques de type script intersites. Lorsque vous recevez des données provenant d'une source non fiable, telle que l'entrée utilisateur, d'autres sites Web via un script de contenu ou une API, veillez à ne pas les interpréter comme du code HTML ni à les utiliser de manière à permettre l'exécution de code inattendu.
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; });
É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; });