Un script de contenu est une partie de votre extension qui s’exécute dans le contexte d’une page Web en particulier (par opposition aux scripts d’arrière-plan de l’extension, ou des 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 à toutes les APIs Javascript de WebExtension, mais ils ne peuvent pas accéder directement au contenu des pages Web. Donc, si votre extension doit le faire, vous avez besoin de scripts de contenu.

Tout comme les scripts chargés par des pages Web normales, 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 petit sous-ensemble des API WebExtension, mais ils peuvent communiquer avec des scripts en arrière-plan à l'aide d'un système de messagerie, et accéder ainsi indirectement aux APIs WebExtension.

Notez que les scripts de contenu sont actuellement bloqués sur addons.mozilla.org. Si vous essayez d'injecter un script de contenu dans une page dans ce domaine, il échouera et la page enregistrera une erreur CSP.

Chargement des scripts de contenus

Vous pouvez charger un script de contenu dans une page Web de deux façons :

  • Par déclaration : en utilisant la clé content_scripts de votre 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 modèle donné
  • Par programmation : en utilisant l'API tabs.executeScript(),  vous pouvez charger un script de contenu dans un onglet spécifique chaque fois que vous le souhaitez : par exemple, en réponse à un clic de l'utilisateur sur une  action de navigateur.

Il n'y a qu'une seule portée globale par environnement d'exécution par extension, de sorte que les variables d'un script de contenu sont directement accessibles par un autre script de contenu, indépendamment de la façon dont le script de contenu a été chargé.

Environnement des scripts de contenu

Accès au DOM

Les scripts de contenu peuvent accéder et modifier le DOM de la page, comme le font les scripts de page habituels. Ils peuvent également voir tout changement du DOM effectué par les scripts de page.

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

  • Les scripts de contenu ne peuvent pas voir les variables JavaScript définies par les scripts de 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.

Dans Gecko, on appelle cela 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 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 page.";
p.setAttribute("id", "page-script-para");
document.body.appendChild(p);

// définition d’une nouvelle propriété pour la fenêtre
window.foo = "Cette variable globale a été ajoutée par un script de 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 page
console.log(window.foo);  // non défini

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

L'inverse est également vrai : les scripts de 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"]
  }
]

APIs WebExtension

En supplément des APIs standard du DOM, les scripts de contenu peuvent utiliser les APIs WebExtension suivantes :

Depuis extension :

Depuis runtime :

Depuis i18n :

La totalité de storage.

Autorisation Cross-domain

Les scripts de contenu possèdent le même niveau d'autorisation sur tout un domaine que le reste de l'extension. Donc si une extension a indiqué l'accès au cross-domain pour un domaine en utilisant la clé permissions dans le manifest.json, alors le script de contenu aura également accès au domaine.

Communication avec les scripts en arrière-plan

Bien que les scripts de contenu ne puissent pas utiliser la totalité des APIs WebExtensions, 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 APIs 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 APIs 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 des messages un par un peut devenir lourd si vous en envoyez un nombre important entre un script d'arrière plan et un script de contenu.

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

Chaque côté possède un objet runtime.Port dont ils peuvent se servir pour échanger des messages.

Pour créer la connexion :

Une fois que chaque côté a 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 !"});
});

L'exemple inpage-toolbar-ui utilise les messages avec connexion continue.

Communication avec une 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 APIs 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 demo sur Github et suivez les instructions.

Notez que 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.

Partage d'objets avec des scripts de page

Les techniquies décrites dans cette section sont uniquement disponibles dans Firefox, et seulement à partir de Firefox 49.

En tant que développeur de modules complémentaires, vous devez considérer que les scripts s'exécutant sur des pages Web arbitraires sont des codes hostiles dont le but est de voler les informations personnelles de l'utilisateur, d'endommager leur ordinateur ou de les attaquer d'une autre manière.

L'isolation entre les scripts de contenu et les scripts chargés par les pages Web a pour but de rendre plus difficile la tâche des pages Web hostiles.

Puisque les techniques décrites dans cette section décomposent cet isolement, elles sont intrinsèquement dangereuses et devraient être utilisées avec beaucoup de soin.

Nous avons vu dans l'accès DOM que les scripts de contenu ne voient pas les modifications apportées au DOM par les scripts chargés par les pages Web. Cela signifie que, par exemple, si une page Web charge une bibliothèque comme jQuery, les scripts de contenu ne pourront pas l'utiliser et devront charger leur propre copie. À l'inverse, les scripts chargés par les pages Web ne peuvent pas voir les modifications apportées par les scripts de contenu.

Cependant, Firefox fournit des API qui permettent aux scripts de contenu de :

  • accéder aux objets JavaScript créés par les scripts de page
  • exposer leurs propres objets JavaScript aux scripts de pages.

Vision Xray dans Firefox

Dans Firefox, une partie de l'isolation entre les scripts de contenu et les scripts de pages est implémentée en utilisant une fonction appelée « Vision Xray ». Lorsqu'un script dans une portée plus privilégiée accède à un objet défini dans une portée moins privilégiée, il ne voit que la « version native » de l'objet. Toutes les propriétés expando sont invisibles, et si des propriétés de l'objet ont été redéfinies, il voit l'implémentation d'origine et non la version redéfinie.

Le but de cette fonctionnalité est de rendre plus difficile pour le script le moins privilégié de confondre le script le plus privilégié par redéfinition des propriétés natives des objets.

Par exemple, lorsqu'un script de contenu accède à la fenêtre de la page, il ne voit aucune propriété ajoutée par le script de la page, et si le script de la page a redéfini les propriétés de la fenêtre, le script de contenu verra la version originale.

Pour l'histoire complète de la vision Xray, voir les articles sur la vision Xray et la sécurité des scripts.

Accès aux objets des scripts de page depuis les scripts de contenu

Dans Firefox, les objets DOM dans les scripts de contenu obtiennent une propriété supplémentaire wrappedJSObject. C'est une version « déballée » de l'objet, qui inclut toutes les modifications apportées à cet objet par les scripts de page.

Prenons un exemple simple. Supposons qu'une page Web charge un script :

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <script type="text/javascript" src="main.js"></script>
  </body>
</html>

Le script ajoute une propriété expando à la window globale :

// main.js

var foo = "Je suis défini par un script de page !";

La vision Xray signifie que si un script de contenu tente d'accéder à foo, il sera indéfini

// content-script.js

console.log(window.foo); // non défini

Dans Firefox, les scripts de contenu peuvent utiliser window.wrappedJSObject pour voir la propriété expando :

// content-script.js

console.log(window.wrappedJSObject.foo); // "Je suis défini par un script de page !"

Notez qu'une fois cela fait, il est possible que les propriétés (ou les fonctions) de cet objet ne soient (ou ne fassent) pas ce que vous attendez. N'importe lequel d'entre eux, même les setters et les getters, aurait pu être redéfini par du code non fiable.

Notez également que le déballage est transitif : lorsque vous utilisez wrappedJSObject, toutes les propriétés de l'objet déplié sont elles-mêmes dépliées (et donc peu fiables). C'est donc une bonne pratique, une fois que vous avez l'objet dont vous avez besoin, de le réemballer, comme ceci :

XPCNativeWrapper(window.wrappedJSObject.foo);

Voir le document sur la vision Xray pour plus de détails à ce sujet.

Partage  d'objets de script de contenu avec des scripts de page

Firefox fournit également des API permettant aux scripts de contenu de rendre les objets disponibles pour les scripts de page. Il y a deux API principales ici :

exportFunction

Étant donné une fonction définie dans le script de contenu, exportFunction() l'exporte vers la portée du script de page, afin que le script de page puisse l'appeler.

Considérons par exemple une WebExtension avec le script d'arrière-plan suivant :

/*
Exécute le script de contenu dans l'onglet actif.
*/
function loadContentScript() {
  browser.tabs.executeScript({
    file: "/content_scripts/export.js"
  });
}

/*
Ajoute loadContentScript() comme écouteur de clics
sur l'action de navigateur.
*/
browser.browserAction.onClicked.addListener(loadContentScript);

/*
Afficher une notification à la réception de messages
depuis le script de contenu.
*/
browser.runtime.onMessage.addListener((message) => {
  browser.notifications.create({
    type: "basic",
    title: "Message de la page",
    message: message.content
  });
});

Il réalise deux choses :

  • exécuter un script de contenu dans l'onglet en cours, lorsque l'utilisateur clique sur une action du navigateur
  • écouter les messages du script de contenu et afficher une notification lorsque le message arrive.

Le script de contenu ressemble à ceci :

/*
Définit une fonction dans la portée du contenu de script, puis l'exporte
dans celle du script de page.
*/
function notify(message) {
  browser.runtime.sendMessage({content: "Appel de fonction : " + message});
}

exportFunction(notify, window, {defineAs:'notify'});

Cela définit une fonction notify(), qui envoie simplement son argument au script d'arrière-plan. Il exporte ensuite la fonction vers la portée du script de page. Maintenant, le script de la page peut appeler cette fonction :

window.notify("Message du script de page !");

Pour le récit complet, voir Components.utils.exportFunction.

cloneInto

Étant donné un objet défini dans le script de contenu, cloneInto crée un clone de l'objet dans la portée du script de page, rendant ainsi le clone accessible aux scripts de page. Par défaut, cela utilise l'algorithme clone structuré pour cloner l'objet, ce qui signifie que les fonctions de l'objet ne sont pas incluses dans le clone. Pour inclure des fonctions, passez l'option cloneFunctions.

Par exemple, voici un script de contenu qui définit un objet contenant une fonction, puis le clone dans la portée du script de page :

/*
Créer dans la portée du script de contenu
un objet contenant des fonctions, puis le
cloner dans la portée de script de page.

L'objet contenant des fonctions, il faut passer
l'option `cloneFunctions` à l'appel cloneInto.
*/
var messenger = {
  notify: function(message) {
    browser.runtime.sendMessage({
      content: "Appel de méthode d'objet : " + message
    });
  }
};

window.wrappedJSObject.messenger = cloneInto(
  messenger,
  window,
  {cloneFunctions: true});

Maintenant les scripts de page verront une nouvelle propriété sur la fenêtre, messenger, qui a une fonction notify() :

window.messenger.notify("Message du script de page !");

Pour le récit complet, voir Components.utils.cloneInto.

Utilisation de eval() dans les scripts de contenu

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

Dans Firefox :

  • Si vous appelez eval(), il exécute du code dans le contexte du script de contenu
  • Si vous appelez window.eval(), il exécute du code 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(`In content script, window.x: ${window.x}`);
console.log(`In content script, 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, puis 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(`In page script, window.x: ${window.x}`);
    console.log(`In page script, window.y: ${window.y}`);
  }
});

Dans Chrome, cela produira une sortie comme ceci :

In content script, window.x: 1
In content script, window.y: 2
In page script, window.x: undefined
In page script, window.y: undefined

Dans Firefox la sortie suivante est produite :

In content script, window.x: undefined
In content script, window.y: 2
In page script, window.x: 1
In page script, window.y: undefined

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

Lorsque vous exécutez du code dans le contexte de la page, l'avertissement de la section « Partage de script de contenu avec des scripts de page » s'applique : 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 redefines console.log

var original = console.log;

console.log = function() {
  original(true);
}
// content-script.js calls the redefined version

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

Étiquettes et contributeurs liés au document

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