Un script de contenu (content script en anglais) est une partie de votre extension qui s’exécute dans le contexte d’une page web donnée (par opposition aux scripts d’arrière-plan de l’extension ou aux scripts qui font partie du site web lui-même, tels que ceux chargés par l’élément <script>).

Les scripts d'arrière-plan peuvent accéder à l'ensemble des API WebExtension mais ils ne peuvent pas accéder directement au contenu des pages web. Aussi, si votre extension doit manipuler le contenu des pages web, vous devrez utiliser les scripts de contenu.

Tout comme les scripts habituellement chargés par les pages web classiques, les scripts de contenu peuvent lire et modifier le contenu de leurs pages en utilisant les API DOM standard.

Les scripts de contenu ne peuvent accéder qu'à un sous-ensemble des API WebExtension, mais ils peuvent communiquer avec les scripts d'arrière-plan grâce à un systèmme de messages et ainsi accéder indirectement aux API WebExtension.

Note : On notera que les scripts de contenu sont actuellement bloqués sur le site https://addons.mozilla.org. Si vous essayez d'injecter un script de contenu dans une page de ce domaine, ceci échouera et la page enregistrera une erreur CSP.

Les valeurs ajoutées à la portée globale d'un script de contenu avec var foo ou window.foo = "bar" peuvent disparaître à cause du bogue 1408996.

Charger des scripts de contenu

Il est possible de charger un script de contenu dans une page web de trois manières différentes :

  1. Lors de la phase d'installation, pour les pages qui correspondent à certains motifs d'URL : en utilisant la clé content_scripts dans le fichier manifest.json, vous pouvez demander au navigateur de charger un script de contenu chaque fois que le navigateur charge une page dont l'URL correspond à un motif donné.
  2. Lors de l'exécution, pour les pages qui correspondent à certains motifs d'URL : en utilisant l'API contentScripts, vous pouvez demander au navigateur de charger un script de contenu chaque fois que le navigateur charge une page dont l'URL correspond à un motif donné. Cette méthode est la version dynamique de la première méthode.
  3. Lors de l'exécution, pour certains onglets spécifiques : en utilisant la méthode  tabs.executeScript(), vous pouvez charger un script de contenu dans un onglet spécifique quand vous le souhaitez (par exemple lorsqu'un utilisateur clique sur un bouton d'action du navigateur).

Il n'y a qu'une seule portée globale pour chaque frame et pour chaque extension. Ainsi, les variables d'un script de contenu A sont accessibles au script de contenu B de la même extension, quel que soit le mode de chargement du script de contenu.

Grâce aux deux premières méthodes, on peut uniquement charger des scripts pour les pages dont les URL peuvent être représentées à l'aide d'un motif. Grâce à la troisième méthode, on peut également charger des scripts dans les pages qui font partie de l'extension. En revanche, il ne sera pas possible de charger des scripts dans certaines pages spéciales du navigateur comme about:debugging ou about:addons.

Environnement des scripts de contenu

Accès au DOM

Les scripts de contenu peuvent accéder et modifier le DOM de la page à la manièrer des scripts classique. Ils peuvent également observer tout changement du DOM effectué par les scripts de la page.

Cependant, les scripts de contenu reçoivent une « vue propre du DOM ». Cela signifie que :

  • Les scripts de contenu ne peuvent pas accéder aux variables JavaScript définies par les scripts de la page.
  • Si un script de page redéfinit une propriété intrinsèque du DOM, le script de contenu verra la version originale de cette propriété et non la version redéfinie.

Pour le moteur Gecko (celui utilisé par Firefox), ce comportement est appelé Vision Xray.

Prenons par exemple la page web suivante :

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  </head>

  <body>
    <script src="page-scripts/page-script.js"></script>
  </body>
</html>

Le script « page-script.js » est écrit ci-dessous :

// page-script.js

// ajouter un élément au DOM
var p = document.createElement("p");
p.textContent = "Ce paragraphe a été ajouté par un script de la page.";
p.setAttribute("id", "page-script-para");
document.body.appendChild(p);

// définition d’une nouvelle propriété pour la fenêtre
window.toto = "Cette variable globale a été ajoutée par un script de la page.";

// redéfinition de la fonction intégrée window.confirm()
window.confirm = function() {
  alert("Ce script de page peut aussi redéfinir ’confirm’.");
}

Et maintenant une extension injecte ce script de contenu dans la page :

// content-script.js

// peut accéder au DOM et le modifier
var pageScriptPara = document.getElementById("page-script-para");
pageScriptPara.style.backgroundColor = "blue";

// ne peut pas voir les propriétés ajoutées par un script de la page
console.log(window.toto);  // non défini

// voit la forme originale des propriétés redéfinies
window.confirm("Êtes-vous sûr ?"); // appelle la méthode window.confirm() originale

L'inverse est également vrai : les scripts de la page ne peuvent pas voir les propriétés JavaScript ajoutées par les scripts de contenu.

Ceci signifie que le script de contenu peut compter sur un comportement prévisible des propriétés du DOM et n'a pas à se soucier d'un éventuel conflit entre les variables qu'il définit et celles des scripts de page.

Une des conséquences pratiques de ce comportement est que les scripts de contenu n’ont accès à aucunes des bibliothèques JavaScript chargées par la page. Par exemple, si la page inclut jQuery, le script de contenu ne pourra pas le voir.

Si un script de contenu veut utiliser une bibliothèque JavaScript, alors la bibliothèque doit être injectée en tant que script de contenu aux côtés du script de contenu qui veut l’utiliser.

"content_scripts": [
  {
    "matches": ["*://*.mozilla.org/*"],
    "js": ["jquery.js", "content-script.js"]
  }
]

On notera que Firefox fournit des API permettant aux scripts de contenu d'accéder aux objets JavaScript créés par les scripts de la page et d'exposer leurs propres objets JavaScript aux scripts de la page. Voir l'article Partage d'objets avec les scripts de la page pour plus d'informations.

Les API WebExtension accessibles

En plus des API standard du DOM, les scripts de contenu peuvent utiliser les API WebExtension suivantes :

Depuis l'API extension :

Depuis l'API runtime :

Depuis l'API i18n :

L'ensemble des propriétés et méthodes de l'API storage.

XHR et Fetch

Les scripts de contenu peuvent effectuer des requêtes en utilisant les API classiques  window.XMLHttpRequest et window.fetch().

Les scripts de contenu obtiennent les mêmes privilèges interdomaines que le reste de l'extension : si l'extension a demandé un accès interdomaine pour un domaine à l'aide de la clé permissions dans le fichier manifest.json, ses scripts de contenu auront également accès à ce domaine.

Ceci est accompli en exposant des instances XHR et fetch privilégiées dans le script de contenu. Cela a pour effet secondaire de ne pas définir les en-têtes Origin et Referer tels que la page elle-même l'aurait fait. Cela est souvent préférable afin d'éviter que la requête révèle la différence d'origine. À partir de Firefox 58, les extensions qui doivent exécuter des requêtes se comportant comme si elles étaient envoyées par le contenu lui-même peuvent utiliser content.XMLHttpRequest et content.fetch(). Pour les extensions visant une compatibilité entre les navigateurs, il est nécessaire de vérifier la présence de ces API avant de les utiliser.

Communication avec les scripts d'arrière-plan

Bien que les scripts de contenu ne puissent pas utiliser la totalité des API WebExtension, ils peuvent communiquer avec les scripts d'arrière-plan de l'extension via l'API de messagerie et ont donc indirectement accès aux mêmes API que ces derniers.

Par défault, il existe deux moyens de communication entre les scripts d'arrière-plan et les scripts de contenu : vous pouvez envoyer des messages un par un, avec des réponses optionelles, ou vous pouvez établir une connexion continue entre les scripts, et utiliser cette connexion pour échanger des messages.

Un message à la fois

Pour envoyer un message à la fois, vous pouvez utiliser les API suivantes :

  Dans le script de contenu Dans les scripts d'arrière-plan
Envoyer un message browser.runtime.sendMessage() browser.tabs.sendMessage()
Recevoir un message browser.runtime.onMessage browser.runtime.onMessage

Par exemple, voici un script de contenu qui écoute les évènements de clic sur une page web. Si le clic était sur un lien, il envoie un message à la page d'arrière-plan avec l'URL cible :

// content-script.js

window.addEventListener("click", notifyExtension);

function notifyExtension(e) {
  if (e.target.tagName != "A") {
    return;
  }
  browser.runtime.sendMessage({"url": e.target.href});
}

Le script d'arrière-plan écoute les messages et affiche une notification en utilisant l'API notification :

// background-script.js

browser.runtime.onMessage.addListener(notify);

function notify(message) {
  browser.notifications.create({
    "type": "basic",
    "iconUrl": browser.extension.getURL("link.png"),
    "title": "Vous avez cliqué sur un lien !",
    "message": message.url
  });
}

Ce code d'exemple est légèrement dérivé de l'exemple notify-link-clicks-i18n sur GitHub.

Les messages en flux continu

L'envoi de messages distincts peut vite devenir lourd si de nombreux messages sont envoyés entre les scripts d'arrière plan et les scripts de contenu.

L'une des alternatives possibles est d'établir une connexion longue durée entre les deux scripts et d'utiliser cette connexion afin d'échanger des messages.

De chaque côté (contenu d'une part, arrière-plan d'autre part), les scripts possèdent un objet runtime.Port dont ils peuvent se servir pour échanger des messages.

Pour créer la connexion :

Une fois que chaque côté possède son propre port, ils peuvent échanger en utilisant runtime.Port.postMessage() pour envoyer des message et runtime.Port.onMessage pour en recevoir.

Par exemple, dès le chargement, ce script de contenu :

  • se connecte au script d'arrière plan et stocke l'objet Port dans une variable myPort
  • écoute des messages sur myPort, et les enregistre.
  • envoie des messages au script d'arrière plan via myPort, quand l'utlisateur clique sur le document.
// content-script.js

var myPort = browser.runtime.connect({name:"port-from-cs"});
myPort.postMessage({greeting: "ici le script de contenu"});

myPort.onMessage.addListener(function(m) {
  console.log("Dans le script de contenu, réception d'un message du script d'arrière-plan : ");
  console.log(m.greeting);
});

document.body.addEventListener("click", function() {
  myPort.postMessage({greeting: "clic sur la page !"});
});

Le script d'arrière plan qui correspond ·

  • Écoute les tentatives de connexion depuis le script de contenu.
  • Quand il reçoit une tentative de connexion :
    • Enregistre le port dans une variable nommée portFromCS
    • Envoie un message au script de contenu en utilisant le port
    • Commence à écouter les messages reçus sur le port et les enregistre.
  • Envoie des messages au script de contenu en utilisant portFromCS, quand l'utilisateur clique sur l'action navigateur de l'add-on
// background-script.js

var portFromCS;

function connected(p) {
  portFromCS = p;
  portFromCS.postMessage({greeting: "salut, script de contenu !"});
  portFromCS.onMessage.addListener(function(m) {
    console.log("Dans le script d'arrière-plan, réception d'un message du script de contenu.")
    console.log(m.greeting);
  });
}

browser.runtime.onConnect.addListener(connected);

browser.browserAction.onClicked.addListener(function() {
  portFromCS.postMessage({greeting: "clic sur le bouton !"});
});

Scripts de contenu multiples

Si plusieurs scripts de contenu communiquent en même temps, vous pouvez stocker chaque connexion dans un tableau.

// background-script.js

var ports = [];

function connected(p) {
  ports[p.sender.tab.id] = p;
  //...
}

browser.runtime.onConnect.addListener(connected)

browser.browserAction.onClicked.addListener(function() {
  ports.forEach(p => {
      p.postMessage({greeting: "clic sur le bouton !"})
    })
});

 

Communiquer avec la page web

Bien que les scripts de contenu ne puissent (par défaut) accéder aux objets créés par les scripts de page, ils peuvent cependant communiquer avec les scripts de page en utilisant les API window.postMessage et window.addEventListener du DOM.

Par exemple :

// page-script.js

var messenger = document.getElementById("from-page-script");

messenger.addEventListener("click", messageContentScript);

function messageContentScript() {
  window.postMessage({
    direction: "from-page-script",
    message: "Message de la page"
  }, "*");
// content-script.js

window.addEventListener("message", function(event) {
  if (event.source == window &&
      event.data &&
      event.data.direction == "from-page-script") {
    alert("Le script de contenu a reçu ce message : \"" + event.data.message + "\"");
  }
});

Pour un exemple complet et fonctionnel, visitez la page de démo sur Github et suivez les instructions.

Attention ! Vous devez faire très attention chaque fois que vous interagissez avec du contenu web non approuvé de cette manière. Les extensions sont du code privilégié qui peut avoir des capacités puissantes et les pages web hostiles peuvent facilement les amener à accéder à ces fonctionnalités.

Un exemple trivial, supposons que le code du script de contenu qui reçoit le message ressemble à ceci :

// content-script.js

window.addEventListener("message", function(event) {
  if (event.source == window &&
      event.data.direction &&
      event.data.direction == "from-page-script") {
    eval(event.data.message);
  }
});

Maintenant, le script de page peut exécuter n'importe quel code avec tous les privilèges du script de contenu.

Utilisation de eval() dans les scripts de contenu

Dans Chrome, eval() exécute toujours le code dans le contexte du script de contenu et pas dans le contexte de la page.

Dans Firefox :

  • Si vous appelez eval(), le code est exécuté dans le contexte du script de contenu
  • Si vous appelez window.eval(), le code est exécute dans le contexte de la page.

Par exemple, considérons un script de contenu comme ceci  :

// content-script.js

window.eval('window.x = 1;');
eval('window.y = 2');

console.log(`Dans le script de contenu, window.x: ${window.x}`);
console.log(`Dans le script de contenu, window.y: ${window.y}`);

window.postMessage({
  message: "check"
}, "*");

Ce code crée simplement des variables x et y en utilisant window.eval() et eval() puis enregistre leurs valeurs et envoie un message à la page.

À la réception du message, le script de page enregistre les mêmes variables :

window.addEventListener("message", function(event) {
  if (event.source === window && event.data && event.data.message === "check") {
    console.log(`Dans le script de la page, window.x: ${window.x}`);
    console.log(`Dans le script de la page, window.y: ${window.y}`);
  }
});

Dans Chrome, cela produira le résultat suivant :

Dans le script de contenu, window.x: 1
Dans le script de contenu, window.y: 2
Dans le script de la page, window.x: undefined
Dans le script de la page, window.y: undefined

Dans Firefox, on aura le résultat suivant :

Dans le script de contenu, window.x: undefined
Dans le script de contenu, window.y: 2
Dans le script de la page, window.x: 1
Dans le script de la page, window.y: undefined

La même chose s'applique pour setTimeout(), setInterval(), et Function().

Lorsque vous exécutez du code dans le contexte de la page, l'avertissement précédent reste nécessaire : l'environnement de la page est contrôlé par des pages web potentiellement malveillantes qui peuvent redéfinir les objets avec lesquels vous interagissez:

// page.js redéfinit console.log

var original = console.log;

console.log = function() {
  original(true);
}
// content-script.js appelle la version redéfinie

window.eval('console.log(false)');

Étiquettes et contributeurs liés au document

Étiquettes : 
Contributeurs à cette page : hellosct1, SphinxKnight, Idlus, SuperTouch, Ostefanini
Dernière mise à jour par : hellosct1,