Cette traduction est incomplète. Aidez à traduire cet article depuis l'anglais.

Un script de contenu fait partie de votre extension qui s'exécute dans le contexte d'une page Web particulière (par opposition aux scripts de fond qui font partie de l'extension, ou des scripts qui font partie du site Web lui-même, tels que ceux chargés à l'aide de la <script> element).

Les scripts d'arrière-plans peuvent accéder à toutes les APIs Javascript de WebExtension, mais ils ne peuvent pas accéder directement au contneu 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:

  • De la manière déclarative : en utilisant la clé de 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 de donnée
  • Par programme : 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 à l'utilisateur en cliquant sur une  action de navigateur.

Il n'y a qu'une seule portée globale par environnement d'execution par extension, de sorte que les variables d'un script de contenu peuvent être 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 tous les scripts de page le font. Ils peuvent également voir chaque changement effectué au DOM par chaque script 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 JavaScripts définies par les scripts de page.
  • Si un script de page redéfinit une propriété intrasèque au DOM, le script de contenu verra la version originale de cette propriété et non la version redéfinie.

Dans Gecko, on appelle cela Xray vision.

Par example, prenons 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" ci-dessous:

// page-script.js

// add a new element to the DOM
var p = document.createElement("p");
p.textContent = "This paragraph was added by a page script.";
p.setAttribute("id", "page-script-para");
document.body.appendChild(p);

// define a new property on the window
window.foo = "This global variable was added by a page script";

// redefine the built-in window.confirm() function
window.confirm = function() {
  alert("The page script has also redefined 'confirm'");
}

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

// content-script.js

// can access and modify the DOM
var pageScriptPara = document.getElementById("page-script-para");
pageScriptPara.style.backgroundColor = "blue";

// can't see page-script-added properties
console.log(window.foo);  // undefined

// sees the original form of redefined properties
window.confirm("Are you sure?"); // calls the original window.confirm()

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 librairies JavaScript chargées par la page. Par example, si la page inclut jQuery, le script de contenu ne pourra pas le voir.

Si un script de contenu veut utiliser une librairie JavaScript, alors la librairie 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"]
  }
]

WebExtension APIs

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ède le même niveau d'autorisation sur tout un domaine que le reste de l'extension. Donc si une extension à indiquer l'accès au cross-domain pour un domaine en utilisant la clé permissions dans le manifest.json, alors le script de contenu aura accès au domaine également.

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'extensions d'arrière-plan en utilisant l'API de messagerie et ont donc indirectement accès aux mêmes APIs que les scripts d'arrières plan.

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ènement de clic sur une page web. Si le clic était sur un lien, il envoit un message à la page d'arrière plan avec l'URL ciblée:

// 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": "You clicked a link!",
    "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 continus

L'envoi de messages un par un peut devenir lourd, si vous envoyer un nombre important de messages 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é à son propre port, les deux côtés peuvent échanger messages en utilisant runtime.Port.postMessage() pour envoyer des message et runtime.Port.onMessage pour recevoir des messages.

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 enregistres.
  • envoi des messages au script d'arrière plan, en utilisant myPort, quand l'utlisateur clique sur le document.
// content-script.js

var myPort = browser.runtime.connect({name:"port-from-cs"});
myPort.postMessage({greeting: "hello from content script"});

myPort.onMessage.addListener(function(m) {
  console.log("In content script, received message from background script: ");
  console.log(m.greeting);
});

document.body.addEventListener("click", function() {
  myPort.postMessage({greeting: "they clicked the page!"});
});

Le script d'arrière plan qui correspond.

  • Écoute les tentatives de depuis le script de contenu.
  • Quand il reçoit une tentative de connexion:
    • Enregistre le port dans une variable nommée portFromCS
    • Envoi au script de contenu un message en utilisant le port
    • Commence à écouter les messages reçus sur le port et les enregistre.
  • Envoi 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: "hi there content script!"});
  portFromCS.onMessage.addListener(function(m) {
    console.log("In background script, received message from content script")
    console.log(m.greeting);
  });
}

browser.runtime.onConnect.addListener(connected);

browser.browserAction.onClicked.addListener(function() {
  portFromCS.postMessage({greeting: "they clicked the button!"});
});

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

Communication avec une page web

Également les scripts de conenu ne peuvent pas (par défaut) accéder aux objets crées par les scripts de page, ils peuvent cependant communiquer avec les scripts de page en utilisant les API DOM  window.postMessage et window.addEventListener.

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 from the page"
  }, "*");
// content-script.js

window.addEventListener("message", function(event) {
  if (event.source == window &&
      event.data &&
      event.data.direction == "from-page-script") {
    alert("Content script received message: \"" + event.data.message + "\"");
  }
});

Pour un exemple complet et fonctionnel, visitez la page de demo sur Github et suiviez les instructions.

Note that any time you interact with untrusted web content on this way, you need to be very careful. WebExtensions are privileged code which can have powerful capabilities, and hostile web pages can easily trick them into accessing those capabilities.

To make a trivial example, suppose the content script code that receives the message does something like this:

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

Now the page script can run any code with all the privileges of the content script.

Sharing objects with page scripts

The techniques described in this section are only available in Firefox, and only from Firefox 49 onwards.

As an add-on developer you should consider that scripts running in arbitrary web pages are hostile code whose aim is to steal the user's personal information, damage their computer, or attack them in some other way.

The isolation between content scripts and scripts loaded by web pages is intended to make it more difficult for hostile web pages to do this.

Since the techniques described in this section break down that isolation, they are inherently dangerous and should be used with great care.

We saw in DOM access that content scripts don't see changes made to the DOM by scripts loaded by web pages. This means that, for example, if a web page loads a library like jQuery, content scripts won't be able to use it, and have to load their own copy. Conversely, scripts loaded by web pages can't see changes made by content scripts.

However, Firefox provides some APIs that enable content scripts to:

  • access JavaScript objects created by page scripts
  • expose their own JavaScript objects to page scripts.

Xray vision in Firefox

In Firefox, part of the isolation between content scripts and page scripts is implemented using a feature called "Xray vision". When a script in a more-privileged scope accesses an object that's defined in a less-privileged scope it sees only the "native version" of the object. Any expando properties are invisible, and if any properties of the object have been redefined, it sees the original implementation, not the redefined version.

The purpose of this feature is to make it harder for the less-privileged script to confuse the more-privileged script by redefining the native properties of objects.

So for example, when a content script accesses the page's window, it won't see any properties the page script added to the window, and if the page script has redefined any existing properties of the window, the content script will see the original version.

For the full story on Xray vision, see the articles on Xray vision and Script security.

Accessing page script objects from content scripts

In Firefox, DOM objects in content scripts get an extra property wrappedJSObject. This is an "unwrapped" version of the object, which includes any changes made to that object by any page scripts.

Let's take a simple example. Suppose a web page loads a script:

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

The script adds an expando property to the global window:

// main.js

var foo = "I'm defined in a page script!";

Xray vision means that if a content script tries to access foo, it will be undefined:

// content-script.js

console.log(window.foo); // undefined

In Firefox, content scripts can use window.wrappedJSObject to see the expando property:

// content-script.js

console.log(window.wrappedJSObject.foo); // "I'm defined in a page script!"

Note that once you do this, you can no longer rely on any of this object's properties or functions being, or doing, what you expect. Any of them, even setters and getters, could have been redefined by untrusted code.

Also note that unwrapping is transitive: when you use wrappedJSObject, any properties of the unwrapped object are themselves unwrapped (and therefore unreliable). So it's good practice, once you've got the object you need, to rewrap it, which you can do like this:

XPCNativeWrapper(window.wrappedJSObject.foo);

See the document on Xray vision for much more detail on this.

Sharing content script objects with page scripts

Firefox also provides APIs enabling content scripts to make objects available to page scripts. There are two main APIs here:

exportFunction

Given a function defined in the content script, exportFunction() exports it to the page script's scope, so the page script can call it.

For example, let's consider a WebExtension which has a background script like this:

/*
Execute content script in the active tab.
*/
function loadContentScript() {
  browser.tabs.executeScript({
    file: "/content_scripts/export.js"
  });
}

/*
Add loadContentScript() as a listener to clicks
on the browser action.
*/
browser.browserAction.onClicked.addListener(loadContentScript);

/*
Show a notification when we get messages from
the content script.
*/
browser.runtime.onMessage.addListener((message) => {
  browser.notifications.create({
    type: "basic",
    title: "Message from the page",
    message: message.content
  });
});

This does two things:

  • execute a content script in the current tab, when the user clicks a browser action
  • listen for messages from the content script, and display a notification when the message arrives.

The content script looks like this:

/*
Define a function in the content script's scope, then export it
into the page script's scope.
*/
function notify(message) {
  browser.runtime.sendMessage({content: "Function call: " + message});
}

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

This defines a function notify(), which just sends its argument to the background script. It then exports the function to the page script's scope. Now the page script can call this function:

window.notify("Message from the page script!");

For the full story, see Components.utils.exportFunction.

cloneInto

Given an object defined in the content script, this creates a clone of the object in the page script's scope, thereby making the clone accessible to page scripts. By default, this uses the structured clone algorithm to clone the object, meaning that functions in the object are not included in the clone. To include functions, pass the cloneFunctions option.

For example, here's a content script that defines an object that contains a function, then clones it into the page script's scope:

/*
Create an object that contains functions in
the content script's scope, then clone it
into the page script's scope.

Because the object contains functions,
the cloneInto call must include
the `cloneFunctions` option.
*/
var messenger = {
  notify: function(message) {
    browser.runtime.sendMessage({
      content: "Object method call: " + message
    });
  }
};

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

Now page scripts will see a new property on the window, messenger, which has a function notify():

window.messenger.notify("Message from the page script!");

For the full story, see Components.utils.cloneInto.

Using eval() in content scripts

In Chrome, eval() always runs code in the context of the content script, not in the context of the page.

In Firefox:

  • if you call eval(), it runs code in the context of the content script
  • if you call window.eval(), it runs code in the context of the page.

For example, consider a content script like this:

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

This code just creates some variables x and y using window.eval() and eval(), then logs their values, then messages the page.

On receiving the message, the page script logs the same 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}`);
  }
});

In Chrome, this will produce output like this:

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

In Firefox the following output is produced:

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

The same applies to setTimeout(), setInterval(), and Function().

When running code in the context of the page, the warning in the "Sharing content script objects with page scripts" section above applies: the page's environment is controlled by potentially malicious web pages, which can redefine objects you interact with to behave in unexpected ways:

// 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

 Contributeurs à cette page : Ostefanini, hellosct1
 Dernière mise à jour par : Ostefanini,