Content Scripts
Ein Content Script ist ein Teil Ihrer Erweiterung, der im Kontext einer Webseite ausgeführt wird. Es kann Seiteninhalte mithilfe der standardmäßigen Web-APIs lesen und ändern. Content Scripts verhalten sich ähnlich wie Skripte, die Teil einer Website sind, etwa solche, die über das Element <script> geladen werden. Content Scripts können jedoch nur dann auf Seiteninhalte zugreifen, wenn Host-Berechtigungen für den Ursprung der Webseite erteilt wurden.
Content Scripts können auf eine kleine Teilmenge der WebExtension-APIs zugreifen. Sie können aber über ein Nachrichtensystem mit Hintergrundskripten kommunizieren und so indirekt auf die WebExtension-APIs zugreifen. Hintergrundskripte können auf alle WebExtension-JavaScript-APIs zugreifen, jedoch nicht direkt auf den Inhalt von Webseiten.
Content Scripts laden
Sie können ein Content Script auf folgende Weise in eine Webseite laden:
- Bei der Installation, in Seiten, die URL-Mustern entsprechen.
- Mit dem Schlüssel
content_scriptsin Ihrermanifest.jsonkönnen Sie den Browser anweisen, ein Content Script zu laden, sobald er eine Seite lädt, deren URL einem bestimmten Muster entspricht.
- Mit dem Schlüssel
- Zur Laufzeit, in Seiten, die URL-Mustern entsprechen.
- Mit
scripting.registerContentScripts()oder (nur in Manifest V2 in Firefox)contentScriptskönnen Sie den Browser anweisen, ein Content Script zu laden, sobald er eine Seite lädt, deren URL einem bestimmten Muster entspricht. (Dies ähnelt Methode 1, mit dem Unterschied, dass Sie Content Scripts zur Laufzeit hinzufügen und entfernen können.)
- Mit
- Zur Laufzeit, in bestimmte Tabs.
- Mit
scripting.executeScript()oder (nur in Manifest V2)tabs.executeScript()können Sie jederzeit ein Content Script in einen bestimmten Tab laden. (Beispielsweise als Reaktion darauf, dass ein Benutzer auf eine Browser-Aktion klickt.)
- Mit
Es gibt nur einen globalen Gültigkeitsbereich pro Frame und pro Erweiterung. Das bedeutet, dass jedes andere Content Script auf die Variablen eines Content Scripts zugreifen kann, unabhängig davon, wie es geladen wurde.
Hinweis: Dynamische JS-Modulimporte funktionieren jetzt in Content Scripts. Weitere Einzelheiten finden Sie unter Firefox-Bug 1536094. Erlaubt sind nur URLs mit dem Schema moz-extension; Daten-URLs sind damit ausgeschlossen (Firefox-Bug 1587336).
Persistenz
Content Scripts, die mit scripting.executeScript() oder (nur in Manifest V2) tabs.executeScript() geladen werden, werden auf Anforderung ausgeführt und bleiben nicht bestehen.
Content Scripts, die über den Schlüssel content_scripts in der Manifestdatei oder mit der API scripting.registerContentScripts() beziehungsweise (nur in Manifest V2 in Firefox) contentScripts definiert werden, bleiben standardmäßig bestehen. Sie bleiben über Browserneustarts und -aktualisierungen sowie über Neustarts der Erweiterung hinweg registriert.
Mit der API scripting.registerContentScripts() lässt sich ein Skript jedoch auch als nicht persistent definieren. Das kann beispielsweise nützlich sein, wenn Ihre Erweiterung im Auftrag eines Benutzers ein Content Script nur für die aktuelle Browsersitzung aktivieren soll.
Berechtigungen, Einschränkungen und Grenzen
>Berechtigungen
Registrierte Content Scripts werden nur ausgeführt, wenn der Erweiterung Host-Berechtigungen für die Domain erteilt wurden.
Um Skripte programmgesteuert einzufügen, benötigt die Erweiterung entweder die activeTab-Berechtigung oder Host-Berechtigungen. Für Methoden der API scripting ist die Berechtigung scripting erforderlich.
Bei der Installation kann eine Erweiterung Host-Berechtigungen für Hosts anfordern, die in ihren matches-Listen unter dem Manifestschlüssel content_scripts stehen. Benutzer können Host-Berechtigungen nach der Installation der Erweiterung erteilen oder verweigern.
Eingeschränkte Domains
Sowohl für Host-Berechtigungen als auch für die activeTab-Berechtigung gelten Ausnahmen für bestimmte Domains. Auf diesen Domains wird die Ausführung von Content Scripts blockiert, beispielsweise um zu verhindern, dass eine Erweiterung über spezielle Seiten ihre Berechtigungen ausweitet.
In Firefox gehören dazu folgende Domains:
- accounts-static.cdn.mozilla.net
- accounts.firefox.com
- addons.cdn.mozilla.net
- addons.mozilla.org
- api.accounts.firefox.com
- content.cdn.mozilla.net
- discovery.addons.mozilla.org
- install.mozilla.org
- oauth.accounts.firefox.com
- profile.accounts.firefox.com
- support.mozilla.org
- sync.services.mozilla.com
Andere Browser schränken den Zugriff auf Websites, von denen Erweiterungen installiert werden können, in ähnlicher Weise ein. In Chrome ist beispielsweise der Zugriff auf chrome.google.com eingeschränkt.
Hinweis:
Da diese Einschränkungen auch addons.mozilla.org betreffen, stellen Benutzer, die Ihre Erweiterung unmittelbar nach der Installation verwenden möchten, möglicherweise fest, dass sie nicht funktioniert. Um das zu vermeiden, sollten Sie einen entsprechenden Hinweis oder eine Einführungsseite hinzufügen, die Benutzer von addons.mozilla.org wegführt.
Die Menge der Domains kann durch Unternehmensrichtlinien weiter eingeschränkt werden: Firefox erkennt die Richtlinie restricted_domains, wie unter ExtensionSettings in mozilla/policy-templates dokumentiert. Die Chrome-Richtlinie runtime_blocked_hosts ist unter ExtensionSettings-Richtlinie konfigurieren dokumentiert.
Grenzen
Standardmäßig werden Content Scripts nicht auf Seiten mit den Schemas about:blank, about:srcdoc, data: und blob: ausgeführt. Um ihre Ausführung dort zu ermöglichen, verwenden Sie die Option match_origin_as_fallback unter dem Manifestschlüssel content_scripts oder die Option matchOriginAsFallback der API scripting.
Erweiterungen können keine Content Scripts in privilegierte Seiten der Browseroberfläche (etwa about:debugging, about:addons, die Leseansicht, die Quelltextansicht oder den PDF-Viewer) oder in Erweiterungsseiten einfügen.
Wenn eine Erweiterung Code dynamisch auf einer Erweiterungsseite ausführen möchte, kann sie ein Skript in die Seite einbinden. Dieses Skript enthält den auszuführenden Code und registriert einen Listener für runtime.onMessage, der eine Möglichkeit zur Ausführung des Codes bereitstellt. Die Erweiterung kann dann eine Nachricht an den Listener senden, um die Ausführung auszulösen.
Umgebung von Content Scripts
>DOM-Zugriff
Content Scripts können ebenso wie normale Seitenskripte auf das DOM einer Seite zugreifen und es ändern. Sie können auch Änderungen sehen, die Seitenskripte am DOM vorgenommen haben.
Content Scripts erhalten jedoch eine „saubere“ Sicht auf das DOM. Das bedeutet:
- Content Scripts können JavaScript-Variablen, die von Seitenskripten definiert wurden, nicht sehen.
- Wenn ein Seitenskript eine integrierte DOM-Eigenschaft neu definiert, sieht das Content Script die ursprüngliche Version der Eigenschaft statt der neu definierten.
Wie unter „Content script environment“ bei Chrome-Inkompatibilitäten beschrieben, unterscheidet sich dieses Verhalten je nach Browser:
-
In Firefox heißt dieses Verhalten Xray Vision. Ein Content Script kann auf JavaScript-Objekte aus seinem globalen Gültigkeitsbereich oder auf Xray-gekapselte Versionen von der Webseite treffen. Auf normalen Webseiten ist
globalThisidentisch mitwindow; in Firefox-Content-Scripts istglobalThisdagegen ein eigenständiges Objekt, das vonwindowerbt. Dieser Unterschied hat für die Verfügbarkeit globaler APIs häufig keine praktischen Auswirkungen. Eine Ausnahme besteht, wenn der globale Gültigkeitsbereich eine Definition einer Standard-API enthält, die die Definition inwindowüberschattet, etwa beistructuredClonein Content Scripts. -
In Chrome wird dieses Verhalten durch eine isolierte Umgebung durchgesetzt, die einen grundlegend anderen Ansatz verwendet.
Betrachten Sie eine Webseite wie diese:
<!doctype html>
<html lang="en-US">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
<script src="page-scripts/page-script.js"></script>
</body>
</html>
Das Skript page-script.js tut Folgendes:
// page-script.js
// add a new element to the DOM
let 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 = () => {
alert("The page script has also redefined 'confirm'");
};
Nun fügt eine Erweiterung ein Content Script in die Seite ein:
// content-script.js
// can access and modify the DOM
let pageScriptPara = document.getElementById("page-script-para");
pageScriptPara.style.backgroundColor = "blue";
// can't see properties added by page-script.js
console.log(window.foo); // undefined
// sees the original form of redefined properties
window.confirm("Are you sure?"); // calls the original window.confirm()
Umgekehrt gilt dasselbe: Seitenskripte können keine JavaScript-Eigenschaften sehen, die von Content Scripts hinzugefügt wurden.
Content Scripts können sich daher darauf verlassen, dass sich DOM-Eigenschaften vorhersehbar verhalten, ohne Kollisionen zwischen ihren Variablen und denen des Seitenskripts befürchten zu müssen.
Eine praktische Folge dieses Verhaltens ist, dass ein Content Script keinen Zugriff auf JavaScript-Bibliotheken hat, die von der Seite geladen wurden. Wenn die Seite beispielsweise jQuery einbindet, kann das Content Script nicht darauf zugreifen.
Wenn ein Content Script eine JavaScript-Bibliothek verwenden muss, sollte die Bibliothek selbst zusammen mit dem Content Script, das sie verwenden möchte, als Content Script eingefügt werden:
"content_scripts": [
{
"matches": ["*://*.mozilla.org/*"],
"js": ["jquery.js", "content-script.js"]
}
]
Hinweis: Firefox stellt cloneInto() und exportFunction() bereit. Damit können Content Scripts auf JavaScript-Objekte zugreifen, die von Seitenskripten erstellt wurden, und ihre eigenen JavaScript-Objekte für Seitenskripte verfügbar machen.
Weitere Einzelheiten finden Sie unter Objekte mit Seitenskripten teilen.
WebExtension-APIs
Zusätzlich zu den standardmäßigen DOM-APIs können Content Scripts folgende WebExtension-APIs verwenden:
Aus extension:
Aus runtime:
connect()getDocumentId()getFrameId()getManifest()getVersion()getURL()onConnectonMessagesendMessage()
Aus i18n:
Aus menus:
Alles aus:
XHR und Fetch
Content Scripts können Anfragen über die normalen APIs window.XMLHttpRequest und window.fetch() stellen.
Hinweis:
In Firefox mit Manifest V2 erfolgen Anfragen von Content Scripts (beispielsweise mit fetch()) im Kontext einer Erweiterung. Deshalb müssen Sie eine absolute URL angeben, um auf Seiteninhalte zu verweisen.
In Chrome und Firefox mit Manifest V3 erfolgen diese Anfragen im Kontext der Seite und werden daher an eine relative URL gesendet. Beispielsweise wird /api an https://«current page URL»/api gesendet.
Content Scripts erhalten dieselben domainübergreifenden Berechtigungen wie der Rest der Erweiterung: Wenn die Erweiterung über den Schlüssel permissions in manifest.json domainübergreifenden Zugriff für eine Domain angefordert hat, können auch ihre Content Scripts auf diese Domain zugreifen.
Hinweis: Bei Verwendung von Manifest V3 können Content Scripts Cross-Origin-Anfragen ausführen, wenn der Zielserver dies über CORS erlaubt. Host-Berechtigungen gelten jedoch nicht für Content Scripts, wohl aber weiterhin für reguläre Erweiterungsseiten.
Dies wird erreicht, indem dem Content Script XHR- und fetch-Instanzen mit erweiterten Berechtigungen zur Verfügung gestellt werden. Eine Nebenwirkung ist, dass die Header Origin und Referer nicht so gesetzt werden, wie es bei einer Anfrage von der Seite selbst der Fall wäre. Das ist oft erwünscht, damit die Anfrage ihre Cross-Origin-Eigenschaft nicht offenlegt.
Hinweis:
In Firefox mit Manifest V2 können Erweiterungen, die Anfragen ausführen müssen, die sich wie vom Seiteninhalt selbst gesendete Anfragen verhalten, stattdessen content.XMLHttpRequest und content.fetch() verwenden.
Bei browserübergreifenden Erweiterungen muss die Verfügbarkeit dieser Methoden per Feature-Erkennung geprüft werden.
In Manifest V3 ist dies nicht möglich, da content.XMLHttpRequest und content.fetch() nicht verfügbar sind.
Hinweis: In Chrome ab Version 73 und in Firefox ab Version 101 mit Manifest V3 unterliegen Content Scripts derselben CORS-Richtlinie wie die Seite, auf der sie ausgeführt werden. Nur Hintergrundskripte verfügen über erweiterte domainübergreifende Berechtigungen. Siehe Änderungen an Cross-Origin-Anfragen in Content Scripts von Chrome-Erweiterungen.
Sichere Kontexte
Seiten, die über HTTPS oder aus einer anderen vertrauenswürdigen Quelle wie localhost geladen werden, stellen einen sicheren Kontext bereit. Einige Web-APIs, etwa crypto.subtle und navigator.geolocation, sind nur in sicheren Kontexten verfügbar. Diese eingeschränkten APIs stellen Informationen oder Funktionen bereit, deren Nutzung auf einer Seite riskant wäre, die ein Angreifer manipulieren könnte.
Content Scripts werden im Kontext der Seite ausgeführt, in die sie eingefügt werden. Daher gilt die Einschränkung für diese APIs auch für Content Scripts: Ein Content Script, das in einem unsicheren Kontext ausgeführt wird, kann keine Web-API verwenden, die einen sicheren Kontext erfordert, selbst wenn der Rest der Erweiterung weiterhin darauf zugreifen kann.
Hinweis:
In Firefox kann die auf sichere Kontexte beschränkte API PointerEvent.getCoalescedEvents() aus Content Scripts auch in unsicheren Kontexten aufgerufen werden.
Mit Hintergrundskripten kommunizieren
Obwohl Content Scripts die meisten WebExtension-APIs nicht direkt verwenden können, können sie über die Messaging-APIs mit den Hintergrundskripten der Erweiterung kommunizieren. Dadurch können sie indirekt auf alle APIs zugreifen, auf die auch die Hintergrundskripte zugreifen können.
Für die Kommunikation zwischen Hintergrundskripten und Content Scripts gibt es zwei grundlegende Muster:
- Sie können einmalige Nachrichten senden (optional mit Antwort).
- Sie können eine länger bestehende Verbindung zwischen beiden Seiten herstellen und darüber Nachrichten austauschen.
Einmalige Nachrichten
Um einmalige Nachrichten zu senden, optional mit einer Antwort, können Sie folgende APIs verwenden:
| Im Content Script | Im Hintergrundskript | |
|---|---|---|
| Nachricht senden |
browser.runtime.sendMessage()
|
browser.tabs.sendMessage()
|
| Nachricht empfangen |
browser.runtime.onMessage
|
browser.runtime.onMessage
|
Das folgende Content Script lauscht beispielsweise auf Klickereignisse auf der Webseite.
Wenn auf einen Link geklickt wurde, sendet es eine Nachricht mit der Ziel-URL an die Hintergrundseite:
// content-script.js
window.addEventListener("click", notifyExtension);
function notifyExtension(e) {
if (e.target.tagName !== "A") {
return;
}
browser.runtime.sendMessage({ url: e.target.href });
}
Das Hintergrundskript lauscht auf diese Nachrichten und zeigt über die API notifications eine Benachrichtigung an:
// background-script.js
browser.runtime.onMessage.addListener(notify);
function notify(message) {
browser.notifications.create({
type: "basic",
iconUrl: browser.runtime.getURL("link.png"),
title: "You clicked a link!",
message: message.url,
});
}
(Dieser Beispielcode ist leicht an das GitHub-Beispiel notify-link-clicks-i18n angelehnt.)
Verbindungsbasierter Nachrichtenaustausch
Wenn Sie viele Nachrichten zwischen einem Hintergrundskript und einem Content Script austauschen, kann das Senden einzelner Nachrichten umständlich werden. Eine Alternative besteht darin, eine länger bestehende Verbindung zwischen den beiden Kontexten herzustellen und darüber Nachrichten auszutauschen.
Beide Seiten verfügen über ein Objekt vom Typ runtime.Port, über das sie Nachrichten austauschen können.
So stellen Sie die Verbindung her:
- Eine Seite lauscht mit
runtime.onConnectauf Verbindungen. - Die andere Seite ruft Folgendes auf:
tabs.connect()(für eine Verbindung zu einem Content Script)runtime.connect()(für eine Verbindung zu einem Hintergrundskript)
Dieser Aufruf gibt ein Objekt vom Typ runtime.Port zurück.
- Dem Listener für
runtime.onConnectwird ein eigenes Objekt vom Typruntime.Portübergeben.
Sobald beide Seiten einen Port haben, können sie:
- mit
runtime.Port.postMessage()Nachrichten senden - mit
runtime.Port.onMessage()Nachrichten empfangen
Das folgende Content Script führt beispielsweise direkt nach dem Laden diese Schritte aus:
- Es stellt eine Verbindung zum Hintergrundskript her.
- Es speichert den
Portin einer Variablen namensmyPort. - Es lauscht auf Nachrichten an
myPortund protokolliert sie. - Es sendet über
myPortNachrichten an das Hintergrundskript, wenn der Benutzer auf das Dokument klickt.
// content-script.js
let myPort = browser.runtime.connect({ name: "port-from-cs" });
myPort.postMessage({ greeting: "hello from content script" });
myPort.onMessage.addListener((m) => {
console.log("In content script, received message from background script: ");
console.log(m.greeting);
});
document.body.addEventListener("click", () => {
myPort.postMessage({ greeting: "they clicked the page!" });
});
Das zugehörige Hintergrundskript:
-
lauscht auf Verbindungsversuche des Content Scripts
-
führt bei einem Verbindungsversuch Folgendes aus:
- Es speichert den Port in einer Variablen namens
portFromCS. - Es sendet über den Port eine Nachricht an das Content Script.
- Es beginnt, auf über den Port empfangene Nachrichten zu lauschen, und protokolliert sie.
- Es speichert den Port in einer Variablen namens
-
sendet über
portFromCSNachrichten an das Content Script, wenn der Benutzer auf die Browser-Aktion der Erweiterung klickt.
// background-script.js
let portFromCS;
function connected(p) {
portFromCS = p;
portFromCS.postMessage({ greeting: "hi there content script!" });
portFromCS.onMessage.addListener((m) => {
portFromCS.postMessage({
greeting: `In background script, received message from content script: ${m.greeting}`,
});
});
}
browser.runtime.onConnect.addListener(connected);
browser.browserAction.onClicked.addListener(() => {
portFromCS.postMessage({ greeting: "they clicked the button!" });
});
Mehrere Content Scripts
Wenn mehrere Content Scripts gleichzeitig kommunizieren, können Sie deren Verbindungen in einem Array speichern.
// background-script.js
let ports = [];
function connected(p) {
ports[p.sender.tab.id] = p;
// …
}
browser.runtime.onConnect.addListener(connected);
browser.browserAction.onClicked.addListener(() => {
ports.forEach((p) => {
p.postMessage({ greeting: "they clicked the button!" });
});
});
Zwischen einmaligen Nachrichten und verbindungsbasiertem Nachrichtenaustausch wählen
Ob Sie einmalige Nachrichten oder verbindungsbasierten Nachrichtenaustausch verwenden, hängt davon ab, wie Ihre Erweiterung die Nachrichtenübermittlung nutzen soll.
Folgende Vorgehensweisen werden empfohlen:
- Verwenden Sie einmalige Nachrichten, wenn …
- auf eine Nachricht nur eine Antwort erwartet wird.
- nur wenige Skripte auf eingehende Nachrichten lauschen (Aufrufe von
runtime.onMessage).
- Verwenden Sie verbindungsbasierten Nachrichtenaustausch, wenn …
- Skripte in Sitzungen mehrere Nachrichten austauschen.
- die Erweiterung über den Fortschritt einer Aufgabe oder eine Unterbrechung informiert werden muss oder eine über Nachrichtenübermittlung gestartete Aufgabe unterbrechen möchte.
Mit der Webseite kommunizieren
Standardmäßig haben Content Scripts keinen Zugriff auf Objekte, die von Seitenskripten erstellt wurden. Sie können jedoch über die DOM-APIs window.postMessage und window.addEventListener mit Seitenskripten kommunizieren.
Beispiel:
// page-script.js
let 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", (event) => {
if (
event.source === window &&
event?.data?.direction === "from-page-script"
) {
alert(`Content script received message: "${event.data.message}"`);
}
});
Ein vollständiges, funktionsfähiges Beispiel finden Sie auf der Demoseite auf GitHub. Folgen Sie dort den Anweisungen.
Warnung: Seien Sie sehr vorsichtig, wenn Sie auf diese Weise mit nicht vertrauenswürdigen Webinhalten interagieren! Erweiterungen sind privilegierter Code mit potenziell weitreichenden Fähigkeiten. Bösartige Webseiten können sie leicht dazu verleiten, diese Fähigkeiten einzusetzen.
Ein einfaches Beispiel: Angenommen, der Code des Content Scripts, der die Nachricht empfängt, tut Folgendes:
// content-script.js
window.addEventListener("message", (event) => {
if (
event.source === window &&
event?.data?.direction === "from-page-script"
) {
eval(event.data.message);
}
});
Nun kann das Seitenskript beliebigen Code mit allen Berechtigungen des Content Scripts ausführen.
eval() in Content Scripts verwenden
Hinweis:
eval() ist in Manifest V3 nicht verfügbar.
- In Chrome
-
evalführt Code immer im Kontext des Content Scripts aus, nicht im Kontext der Seite. - In Firefox
-
Wenn Sie
eval()aufrufen, wird Code im Kontext des Content Scripts ausgeführt.Wenn Sie
window.eval()aufrufen, wird Code im Kontext der Seite ausgeführt.
Betrachten Sie beispielsweise dieses Content Script:
// 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",
},
"*",
);
Dieser Code erstellt mit window.eval() und eval() lediglich die Variablen x und y, protokolliert ihre Werte und sendet anschließend eine Nachricht an die Seite.
Wenn das Seitenskript die Nachricht empfängt, protokolliert es dieselben Variablen:
window.addEventListener("message", (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 ergibt sich eine Ausgabe wie diese:
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 ergibt sich eine Ausgabe wie diese:
In content script, window.x: undefined In content script, window.y: 2 In page script, window.x: 1 In page script, window.y: undefined
Dasselbe gilt für setTimeout(), setInterval() und Function().
Warnung: Seien Sie sehr vorsichtig, wenn Sie Code im Kontext der Seite ausführen!
Die Umgebung der Seite wird von möglicherweise bösartigen Webseiten kontrolliert. Diese können Objekte, mit denen Sie interagieren, so umdefinieren, dass sie sich unerwartet verhalten:
// page.js redefines console.log
let original = console.log;
console.log = () => {
original(true);
};
// content-script.js calls the redefined version
window.eval("console.log(false)");