Les scripts de contenu sont des fichiers exécutés dans le contexte de pages Web. À l'aide du Document Object Model (DOM) standard, ils peuvent lire les détails des pages Web visitées par le navigateur, les modifier et transmettre des informations à leur extension parente.
Comprendre les fonctionnalités du script de contenu
Les scripts de contenu peuvent accéder aux fichiers d'extension après les avoir déclarés comme ressources accessibles sur le Web. Ils peuvent accéder directement aux API d'extension suivantes:
dom
i18n
storage
runtime.connect()
runtime.getManifest()
runtime.getURL()
runtime.id
runtime.onConnect
runtime.onMessage
runtime.sendMessage()
Les scripts de contenu ne peuvent pas accéder directement aux autres API. Toutefois, ils peuvent y accéder indirectement en échangeant des messages avec d'autres parties de votre extension.
Travailler dans des environnements isolés
Les scripts de contenu résident dans un environnement isolé, ce qui leur permet de modifier leur environnement JavaScript sans entrer en conflit avec les scripts de contenu de la page ou d'autres extensions.
Une extension peut s'exécuter sur une page Web avec un code semblable à l'exemple suivant.
webPage.html
<html>
<button id="mybutton">click me</button>
<script>
var greeting = "hello, ";
var button = document.getElementById("mybutton");
button.person_name = "Bob";
button.addEventListener(
"click", () => alert(greeting + button.person_name + "."), false);
</script>
</html>
Cette extension pourrait injecter le script de contenu suivant en utilisant l'une des techniques décrites dans la section Injecter des scripts.
content-script.js
var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener(
"click", () => alert(greeting + button.person_name + "."), false);
Désormais, les deux alertes apparaissent l'une après l'autre lorsque l'utilisateur clique sur le bouton.
Injecter des scripts
Les scripts de contenu peuvent être déclarés de manière statique, déclarés de manière dynamique ou injectés de manière programmatique.
Injecter avec des déclarations statiques
Utilisez des déclarations de script de contenu statique dans le fichier manifest.json pour les scripts qui doivent être exécutés automatiquement sur un ensemble de pages bien connu.
Les scripts déclarés de manière statique sont enregistrés dans le fichier manifeste sous la clé "content_scripts"
.
Ils peuvent inclure des fichiers JavaScript, des fichiers CSS ou les deux. Tous les scripts de contenu à exécution automatique doivent spécifier des formats de correspondance.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"css": ["my-styles.css"],
"js": ["content-script.js"]
}
],
...
}
Nom | Type | Description |
---|---|---|
matches |
tableau de chaînes | Obligatoire. Spécifie les pages dans lesquelles ce script de contenu sera injecté. Consultez la section Formats de correspondance pour en savoir plus sur la syntaxe de ces chaînes et la section Formats de correspondance et schémas de correspondance pour en savoir plus sur l'exclusion d'URL. |
css |
tableau de chaînes | Facultatif. Liste des fichiers CSS à injecter dans les pages correspondantes. Ces éléments sont injectés dans l'ordre dans lequel ils apparaissent dans ce tableau, avant la construction ou l'affichage des DOM sur la page. |
js |
|
Facultatif. Liste des fichiers JavaScript à injecter dans les pages correspondantes. Les fichiers sont injectés dans l'ordre dans lequel ils apparaissent dans ce tableau. Chaque chaîne de cette liste doit contenir un chemin d'accès relatif à une ressource du répertoire racine de l'extension. Les barres obliques au début ("/") sont automatiquement coupées. |
run_at |
RunAt | Facultatif. Indique à quel moment le script doit être injecté dans la page. La valeur par défaut est document_idle . |
match_about_blank |
boolean | Facultatif. Indique si le script doit être injecté dans un frame about:blank où le frame parent ou d'ouverture correspond à l'un des modèles déclarés dans matches . Valeur par défaut : "false". |
match_origin_as_fallback |
boolean |
Facultatif. Indique si le script doit injecter des frames qui ont été créés par une origine correspondante, mais dont l'URL ou l'origine peuvent ne pas correspondre directement au format. Ceux-ci incluent des cadres avec différents schémas, tels que about: , data: , blob: et filesystem: . Consultez également la section Injecter dans des frames associés.
|
world |
ExecutionWorld |
Facultatif. Monde JavaScript dans lequel un script doit s'exécuter. La valeur par défaut est ISOLATED . Voir aussi Travailler dans des mondes isolés.
|
Injecter avec des déclarations dynamiques
Les scripts de contenu dynamique sont utiles lorsque les formats de correspondance des scripts de contenu ne sont pas bien connus ou lorsque les scripts de contenu ne doivent pas toujours être injectés sur des hôtes connus.
Introduites dans Chrome 96, les déclarations dynamiques sont semblables aux déclarations statiques, à la différence que l'objet de script de contenu est enregistré avec Chrome à l'aide de méthodes dans l'espace de noms chrome.scripting
plutôt que dans manifest.json. L'API de script permet également aux développeurs d'extensions de:
- Enregistrez des scripts de contenu.
- Obtenez la liste des scripts de contenu enregistrés.
- Mettez à jour la liste des scripts de contenu enregistrés.
- Supprimez des scripts de contenu enregistrés.
Comme les déclarations statiques, les déclarations dynamiques peuvent inclure des fichiers JavaScript, des fichiers CSS ou les deux.
service-worker.js
chrome.scripting
.registerContentScripts([{
id: "session-script",
js: ["content.js"],
persistAcrossSessions: false,
matches: ["*://example.com/*"],
runAt: "document_start",
}])
.then(() => console.log("registration complete"))
.catch((err) => console.warn("unexpected error", err))
service-worker.js
chrome.scripting
.updateContentScripts([{
id: "session-script",
excludeMatches: ["*://admin.example.com/*"],
}])
.then(() => console.log("registration updated"));
service-worker.js
chrome.scripting
.getRegisteredContentScripts()
.then(scripts => console.log("registered content scripts", scripts));
service-worker.js
chrome.scripting
.unregisterContentScripts({ ids: ["session-script"] })
.then(() => console.log("un-registration complete"));
Injecter par programmation
Utilisez l'injection programmatique pour les scripts de contenu qui doivent s'exécuter en réponse à des événements ou à des occasions spécifiques.
Pour injecter un script de contenu de manière automatisée, votre extension a besoin d'autorisations d'hôte pour la page dans laquelle elle tente d'injecter des scripts. Les autorisations d'hôte peuvent être accordées en les demandant dans le fichier manifeste de votre extension ou temporairement à l'aide de "activeTab"
.
Voici une version différente d'une extension basée sur ActiveTab.
manifest.json:
{
"name": "My extension",
...
"permissions": [
"activeTab",
"scripting"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "Action Button"
}
}
Les scripts de contenu peuvent être injectés en tant que fichiers.
content-script.js
document.body.style.backgroundColor = "orange";
service-worker.js :
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content-script.js"]
});
});
Le corps d'une fonction peut également être injecté et exécuté en tant que script de contenu.
service-worker.js :
function injectedFunction() {
document.body.style.backgroundColor = "orange";
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
});
});
Sachez que la fonction injectée est une copie de la fonction référencée dans l'appel chrome.scripting.executeScript()
, et non la fonction d'origine elle-même. Par conséquent, le corps de la fonction doit être autonome. Les références à des variables en dehors de la fonction entraînent la génération d'une ReferenceError
par le script de contenu.
Lors d'une injection en tant que fonction, vous pouvez également transmettre des arguments à la fonction.
service-worker.js
function injectedFunction(color) {
document.body.style.backgroundColor = color;
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
args : [ "orange" ],
});
});
Exclure des correspondances et des globs
Pour personnaliser la mise en correspondance des pages spécifiées, incluez les champs suivants dans un enregistrement déclaratif.
Nom | Type | Description |
---|---|---|
exclude_matches |
tableau de chaînes | Facultatif. Exclut les pages dans lesquelles ce script de contenu serait autrement injecté. Consultez Formats de correspondance pour en savoir plus sur la syntaxe de ces chaînes. |
include_globs |
tableau de chaînes | Facultatif. Appliqué après matches pour inclure uniquement les URL qui correspondent également à ce glob. Cela permet d'émuler le mot clé Greasemonkey @include . |
exclude_globs |
tableau de chaînes | Facultatif. Appliqué après matches pour exclure les URL qui correspondent à ce glob. Destiné à émuler le mot clé Greasemonkey @exclude . |
Le script de contenu est injecté dans une page si les deux conditions suivantes sont remplies:
- Son URL correspond à n'importe quel format
matches
etinclude_globs
. - L'URL ne correspond pas non plus à un format
exclude_matches
ouexclude_globs
. Étant donné que la propriétématches
est obligatoire,exclude_matches
,include_globs
etexclude_globs
ne peuvent être utilisés que pour limiter les pages affectées.
L'extension suivante injecte le script de contenu dans https://www.nytimes.com/health
, mais pas dans https://www.nytimes.com/business
.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"js": ["contentScript.js"]
}
],
...
}
service-worker.js
chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.nytimes.com/*" ],
excludeMatches : [ "*://*/*business*" ],
js : [ "contentScript.js" ],
}]);
Les propriétés glob suivent une syntaxe différente et plus flexible que les motifs de correspondance. Les chaînes glob acceptées sont des URL qui peuvent contenir des astérisques et des points d'interrogation "génériques". L'astérisque (*
) correspond à n'importe quelle chaîne de n'importe quelle longueur, y compris la chaîne vide, tandis que le point d'interrogation (?
) correspond à n'importe quel caractère.
Par exemple, le glob https://???.example.com/foo/\*
correspond à l'un des éléments suivants:
https://www.example.com/foo/bar
https://the.example.com/foo/
Toutefois, il ne correspond pas à ce qui suit:
https://my.example.com/foo/bar
https://example.com/foo/
https://www.example.com/foo
Cette extension injecte le script de contenu dans https://www.nytimes.com/arts/index.html
et https://www.nytimes.com/jobs/index.htm*
, mais pas dans https://www.nytimes.com/sports/index.html
:
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"include_globs": ["*nytimes.com/???s/*"],
"js": ["contentScript.js"]
}
],
...
}
Cette extension injecte le script de contenu dans https://history.nytimes.com
et https://.nytimes.com/history
, mais pas dans https://science.nytimes.com
ni https://www.nytimes.com/science
:
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
L'un, l'ensemble ou certains d'entre eux peuvent être inclus pour atteindre la portée correcte.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"exclude_matches": ["*://*/*business*"],
"include_globs": ["*nytimes.com/???s/*"],
"exclude_globs": ["*science*"],
"js": ["contentScript.js"]
}
],
...
}
Durée de diffusion
Le champ run_at
contrôle à quel moment les fichiers JavaScript sont injectés dans la page Web. La valeur par défaut est "document_idle"
. Consultez le type RunAt pour connaître d'autres valeurs possibles.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"run_at": "document_idle",
"js": ["contentScript.js"]
}
],
...
}
service-worker.js
chrome.scripting.registerContentScripts([{
id : "test",
matches : [ "https://*.nytimes.com/*" ],
runAt : "document_idle",
js : [ "contentScript.js" ],
}]);
Nom | Type | Description |
---|---|---|
document_idle |
chaîne | Préféré. Utilisez "document_idle" dans la mesure du possible.Le navigateur choisit une heure d'injection des scripts entre "document_end" et immédiatement après le déclenchement de l'événement window.onload . Le moment exact de l'injection dépend de la complexité du document et du temps de chargement. De plus, il est optimisé pour la vitesse de chargement des pages.Les scripts de contenu exécutés sur "document_idle" n'ont pas besoin d'écouter l'événement window.onload . Ils sont exécutés une fois le DOM terminé. Si un script doit impérativement s'exécuter après window.onload , l'extension peut vérifier si onload s'est déjà déclenché à l'aide de la propriété document.readyState . |
document_start |
chaîne | Les scripts sont injectés après les fichiers provenant de css , mais avant la construction de tout autre DOM ou l'exécution de tout autre script. |
document_end |
chaîne | Les scripts sont injectés immédiatement après la fin du DOM, mais avant le chargement des sous-ressources telles que les images et les cadres. |
Spécifier des cadres
Le champ "all_frames"
permet à l'extension de spécifier si les fichiers JavaScript et CSS doivent être injectés dans tous les frames correspondant aux exigences d'URL spécifiées ou uniquement dans le frame supérieur d'un onglet.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"all_frames": true,
"js": ["contentScript.js"]
}
],
...
}
service-worker.js
chrome.scripting.registerContentScripts([{
id: "test",
matches : [ "https://*.nytimes.com/*" ],
allFrames : true,
js : [ "contentScript.js" ],
}]);
Nom | Type | Description |
---|---|---|
all_frames |
boolean | Facultatif. La valeur par défaut est false , ce qui signifie que seul le frame supérieur est mis en correspondance.Si true est spécifié, tous les frames sont injectés, même s'ils ne sont pas le cadre supérieur de l'onglet. Chaque frame est vérifié indépendamment pour les exigences en termes d'URL. Elle ne sera pas injectée dans les frames enfants si les exigences d'URL ne sont pas remplies. |
Injecter dans les frames associés
Les extensions peuvent souhaiter exécuter des scripts dans des cadres liés à un cadre correspondant, mais qui ne correspondent pas. Dans ce cas, un scénario courant concerne les frames dont les URL ont été créées par un frame correspondant, mais dont les URL ne correspondent pas aux formats spécifiés par le script.
C'est le cas lorsqu'une extension souhaite injecter des frames dont les URL ont les schémas about:
, data:
, blob:
et filesystem:
. Dans ce cas, l'URL ne correspond pas au format du script de contenu (et, dans le cas de about:
et data:
, n'incluez même pas l'URL parente ni l'origine dans l'URL, comme dans about:blank
ou data:text/html,<html>Hello, World!</html>
). Toutefois, ces frames peuvent toujours être associés au frame en cours de création.
Pour injecter dans ces frames, les extensions peuvent spécifier la propriété "match_origin_as_fallback"
sur une spécification de script de contenu dans le fichier manifeste.
manifest.json
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.google.com/*"],
"match_origin_as_fallback": true,
"js": ["contentScript.js"]
}
],
...
}
Si spécifié et défini sur true
, Chrome examine l'origine de l'initiateur de la trame pour déterminer si celui-ci correspond, plutôt que l'URL du frame lui-même. Notez qu'elle peut également être différente de l'origine du frame cible (par exemple, data:
URL ont une origine nulle).
L'initiateur du frame est le frame qui a créé le frame cible ou qui l'a parcouru. Bien qu'il s'agisse généralement du parent ou de l'outil d'ouverture directs, ce n'est pas toujours le cas (comme c'est le cas lorsqu'un frame navigue dans un iFrame au sein d'un iFrame).
Comme cela permet de comparer l'origine de la trame de l'initiateur, celle-ci peut se trouver sur n'importe quel chemin à partir de cette origine. Pour que cette implication soit claire, Chrome exige que tous les scripts de contenu spécifiés avec "match_origin_as_fallback"
définis sur true
spécifient également le chemin d'accès *
.
Lorsque "match_origin_as_fallback"
et "match_about_blank"
sont spécifiés, "match_origin_as_fallback"
est prioritaire.
Communication avec la page d'intégration
Bien que les environnements d'exécution des scripts de contenu et les pages qui les hébergent soient isolés les uns des autres, ils partagent l'accès au DOM de la page. Si la page souhaite communiquer avec le script de contenu, ou avec l'extension via le script de contenu, elle doit le faire via le DOM partagé.
Un exemple peut être accompli en utilisant window.postMessage()
:
content-script.js
var port = chrome.runtime.connect();
window.addEventListener("message", (event) => {
// We only accept messages from ourselves
if (event.source !== window) {
return;
}
if (event.data.type && (event.data.type === "FROM_PAGE")) {
console.log("Content script received: " + event.data.text);
port.postMessage(event.data.text);
}
}, false);
example.js
document.getElementById("theButton").addEventListener("click", () => {
window.postMessage(
{type : "FROM_PAGE", text : "Hello from the webpage!"}, "*");
}, false);
La page sans extension, example.html, envoie des messages à elle-même. Ce message est intercepté et inspecté par le script de contenu, puis publié dans le processus d'extension. De cette manière, la page établit une ligne de communication avec le processus d'extension. L'inverse est possible par des moyens similaires.
Accéder aux fichiers d'extension
Pour accéder à un fichier d'extension à partir d'un script de contenu, vous pouvez appeler chrome.runtime.getURL()
afin d'obtenir l'URL absolue de votre composant d'extension, comme illustré dans l'exemple suivant (content.js
):
content-script.js
let image = chrome.runtime.getURL("images/my_image.png")
Pour utiliser des polices ou des images dans un fichier CSS, vous pouvez utiliser @@extension_id
afin de créer une URL, comme illustré dans l'exemple suivant (content.css
):
content.css
body {
background-image:url('chrome-extension://__MSG_@@extension_id__/background.png');
}
@font-face {
font-family: 'Stint Ultra Expanded';
font-style: normal;
font-weight: 400;
src: url('chrome-extension://__MSG_@@extension_id__/fonts/Stint Ultra Expanded.woff') format('woff');
}
Tous les éléments doivent être déclarés en tant que ressources accessibles sur le Web dans le fichier manifest.json
:
manifest.json
{
...
"web_accessible_resources": [
{
"resources": [ "images/*.png" ],
"matches": [ "https://example.com/*" ]
},
{
"resources": [ "fonts/*.woff" ],
"matches": [ "https://example.com/*" ]
}
],
...
}
Bénéficiez d'une sécurité optimale
Alors que les mondes isolés offrent un niveau de protection, l'utilisation de scripts de contenu peut créer des failles dans une extension et la page Web. Si le script de contenu reçoit du contenu d'un site Web distinct, par exemple en appelant fetch()
, veillez à le filtrer pour éviter les attaques par script intersites avant de l'injecter. Communiquez uniquement via HTTPS afin d'éviter les attaques "man-in-the-middle".
Veillez à filtrer les pages Web malveillantes. Par exemple, les formats suivants sont dangereux et interdits dans Manifest V3:
content-script.js
const data = document.getElementById("json-data"); // WARNING! Might be evaluating an evil script! const parsed = eval("(" + data + ")");
content-script.js
const elmt_id = ... // WARNING! elmt_id might be '); ... evil script ... //'! window.setTimeout("animate(" + elmt_id + ")", 200);
Optez plutôt pour des API plus sûres qui n'exécutent pas de scripts:
content-script.js
const data = document.getElementById("json-data") // JSON.parse does not evaluate the attacker's scripts. const parsed = JSON.parse(data);
content-script.js
const elmt_id = ... // The closure form of setTimeout does not evaluate scripts. window.setTimeout(() => animate(elmt_id), 200);