Inhalts-Skripte

Ein Inhalts-Skript ist ein Teil Ihrer Erweiterung, das im Kontext einer Webseite ausgeführt wird. Es kann Seiteninhalte lesen und modifizieren, indem es die standardmäßigen Web-APIs verwendet. Das Verhalten von Inhalts-Skripten ist ähnlich wie das von Skripten, die Teil einer Webseite sind, wie z.B. jene, die mit dem <script>-Element geladen werden. Allerdings können Inhalts-Skripte nur auf Seiteninhalte zugreifen, wenn Host-Berechtigungen für den Ursprung der Webseite erteilt wurden.

Inhalts-Skripte können auf eine kleine Teilmenge der WebExtension-APIs zugreifen, jedoch können sie mit Hintergrundskripten kommunizieren und so indirekt auf die WebExtension-APIs zugreifen. Hintergrundskripte können auf alle WebExtension JavaScript-APIs zugreifen, haben jedoch keinen direkten Zugriff auf den Inhalt von Webseiten.

Hinweis: Einige Web-APIs sind auf sichere Kontexte beschränkt, was auch für Inhalts-Skripte in diesen Kontexten gilt. Ausgenommen ist PointerEvent.getCoalescedEvents(), das aus Inhalts-Skripten in unsicheren Kontexten in Firefox aufgerufen werden kann.

Laden von Inhalts-Skripten

Sie können ein Inhalts-Skript in eine Webseite laden:

  1. Zur Installationszeit, in Seiten, die zu URL-Mustern passen.
  2. Zur Laufzeit, in Seiten, die zu URL-Mustern passen.
  3. Zur Laufzeit, in spezifische Tabs.

Es gibt nur einen globalen Gültigkeitsbereich pro Frame, pro Erweiterung. Dies bedeutet, dass Variablen aus einem Inhalts-Skript von allen anderen Inhalts-Skripten zugänglich sind, unabhängig davon, wie das Inhalts-Skript geladen wurde.

Mit den Methoden (1) und (2) können Sie Skripte nur in Seiten laden, deren URLs mittels eines Übereinstimmungsmusters dargestellt werden können.

Mit Methode (3) können Sie auch Skripte in Seiten laden, die mit Ihrer Erweiterung gebündelt sind, aber Sie können keine Skripte in privilegierte Browserseiten laden (wie "about:debugging" oder "about:addons").

Hinweis: Dynamische JS-Modul-Importe funktionieren nun in Inhalts-Skripten. Weitere Details finden Sie in Firefox-Bug 1536094. Nur URLs mit dem moz-extension-Schema sind erlaubt, was Daten-URLs ausschließt (Firefox-Bug 1587336).

Persistenz

Inhalts-Skripte, die mit scripting.executeScript() oder (nur in Manifest V2) tabs.executeScript() geladen werden, laufen auf Anfrage und bleiben nicht bestehen.

Inhalts-Skripte, die im content_scripts-Schlüssel der manifest.json-Datei definiert sind oder mit der scripting.registerContentScripts() API (nur in Manifest V2 in Firefox) registriert wurden, bleiben standardmäßig bestehen. Sie bleiben über Browser-Neustarts und -Aktualisierungen sowie Erweiterungs-Neustarts hinaus registriert.

Die scripting.registerContentScripts() API ermöglicht es jedoch, das Skript als nicht persistent zu definieren. Dies kann nützlich sein, wenn zum Beispiel Ihre Erweiterung (im Namen eines Benutzers) ein Inhalts-Skript nur in der aktuellen Browsersitzung aktivieren möchte.

Berechtigungen, Einschränkungen und Begrenzungen

Berechtigungen

Registrierte Inhalts-Skripte werden nur ausgeführt, wenn der Erweiterung Host-Berechtigungen für die Domain erteilt wurden.

Um Skripte programmgesteuert zu injizieren, benötigt die Erweiterung entweder die activeTab-Berechtigung oder Host-Berechtigungen. Die scripting-Berechtigung ist erforderlich, um Methoden der scripting API zu verwenden.

Mit Manifest V3 werden Host-Berechtigungen nicht automatisch bei der Installation erteilt. Benutzer können sich für oder gegen Host-Berechtigungen entscheiden, nachdem sie die Erweiterung installiert haben.

Eingeschränkte Domains

Sowohl Host-Berechtigungen als auch die activeTab-Berechtigung haben Ausnahmen für einige Domains. Inhalts-Skripte sind daran gehindert, auf diesen Domains auszuführen, um den Benutzer beispielsweise vor einer Erweiterung zu schützen, die ihre Privilegien durch spezielle Seiten eskaliert.

In Firefox umfassen diese 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 haben ähnliche Einschränkungen hinsichtlich der Webseiten, von denen Erweiterungen installiert werden können. Beispielsweise ist in Chrome der Zugriff auf chrome.google.com eingeschränkt.

Hinweis: Da diese Einschränkungen auch addons.mozilla.org einschließen, kann es vorkommen, dass Benutzer, die versuchen, Ihre Erweiterung unmittelbar nach der Installation zu nutzen, feststellen, dass sie nicht funktioniert. Um dies zu vermeiden, sollten Sie eine geeignete Warnung oder eine Onboarding-Seite hinzufügen, um Benutzer von addons.mozilla.org wegzuleiten.

Der Satz von Domains kann durch Unternehmensrichtlinien weiter eingeschränkt werden: Firefox erkennt die restricted_domains-Richtlinie an, wie in ExtensionSettings in mozilla/policy-templates dokumentiert. Chromes runtime_blocked_hosts-Richtlinie ist unter Configure ExtensionSettings policy dokumentiert.

Begrenzungen

Ganze Tabs oder Frames können mithilfe von data:-URI, Blob-Objekten und anderen ähnlichen Techniken geladen werden. Die Unterstützung der Inhalts-Skript-Injektion in solche speziellen Dokumente variiert je nach Browser, siehe den Firefox Bug #1411641 Kommentar 41 für einige Details.

Inhalts-Skript-Umgebung

DOM-Zugriff

Inhalts-Skripte können auf das DOM der Seite zugreifen und dieses modifizieren, genauso wie normale Seitenskripte es können. Sie können auch alle Änderungen sehen, die an dem DOM von Seitenskripten vorgenommen wurden.

Allerdings erhalten Inhalts-Skripte eine "saubere" Ansicht des DOMs. Das bedeutet:

  • Inhalts-Skripte können keine JavaScript-Variablen sehen, die von Seitenskripten definiert wurden.
  • Wenn ein Seitenskript eine eingebaute DOM-Eigenschaft neu definiert, sieht das Inhalts-Skript die ursprüngliche Version der Eigenschaft, nicht die neu definierte Version.

Wie unter "Content script environment" bei Chrome-Inkompatibilitäten angemerkt, verhält sich dies je nach Browser unterschiedlich:

  • In Firefox wird dieses Verhalten als Xray vision bezeichnet. Inhalts-Skripte können JavaScript-Objekte aus ihrem eigenen globalen Gültigkeitsbereich oder Xray-umhüllte Versionen von der Webseite antreffen.

  • In Chrome wird dieses Verhalten durch eine isolierte Welt erzwungen, die einen grundlegend anderen Ansatz verwendet.

Betrachten Sie eine Webseite wie diese:

html
<!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 macht Folgendes:

js
// 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 injiziert eine Erweiterung ein Inhalts-Skript in die Seite:

js
// 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()

Dasselbe trifft auch im umgekehrten Fall zu; Seitenskripte haben keinen Zugriff auf JavaScript-Eigenschaften, die von Inhalts-Skripten hinzugefügt wurden.

Das bedeutet, dass Inhalts-Skripte sich darauf verlassen können, dass sich DOM-Eigenschaften vorhersehbar verhalten, ohne sich Sorgen machen zu müssen, dass ihre Variablen mit den Variablen des Seitenskripts kollidieren.

Eine praktische Konsequenz dieses Verhaltens ist, dass ein Inhalts-Skript keinen Zugriff auf JavaScript-Bibliotheken hat, die von der Seite geladen werden. Wenn die Seite beispielsweise jQuery enthält, kann das Inhalts-Skript dies nicht sehen.

Wenn ein Inhalts-Skript eine JavaScript-Bibliothek verwenden muss, sollte die Bibliothek selbst als Inhalts-Skript neben dem Inhalts-Skript, das sie verwenden möchte, injiziert werden:

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

Hinweis: Firefox bietet cloneInto() und exportFunction(), um Inhalts-Skripten den Zugriff auf JavaScript-Objekte zu ermöglichen, die von Seitenskripten erstellt wurden und ihre JavaScript-Objekte den Seitenskripten bereitzustellen.

Weitere Details finden Sie unter Sharing objects with page scripts.

WebExtension-APIs

Zusätzlich zu den standardmäßigen DOM-APIs können Inhalts-Skripte diese WebExtension-APIs verwenden:

Von extension:

Von runtime:

Von i18n:

Von menus:

Alles von:

XHR und Fetch

Inhalts-Skripte können Anfragen mit den normalen window.XMLHttpRequest und window.fetch() APIs machen.

Hinweis: In Firefox in Manifest V2 erfolgen Inhalts-Skript-Anfragen (zum Beispiel mit fetch()) im Kontext einer Erweiterung, daher müssen Sie eine absolute URL angeben, um auf Seiteninhalte zu referenzieren.

In Chrome und Firefox in Manifest V3 erfolgen diese Anfragen im Kontext der Seite, so dass sie an eine relative URL gesendet werden. Zum Beispiel wird /api an https://«aktueller Seiten-URL»/api gesendet.

Inhalts-Skripte haben dieselben Cross-Domain-Berechtigungen wie der Rest der Erweiterung: Wenn die Erweiterung also Cross-Domain-Zugriff für eine Domain mithilfe des permissions-Schlüssels in manifest.json beantragt hat, dann erhalten ihre Inhalts-Skripte Zugriff auf diese Domain ebenfalls.

Hinweis: Bei Verwendung von Manifest V3 können Inhalts-Skripte übergreifende Anfragen ausführen, wenn der Zielserver über CORS zustimmt; Host-Berechtigungen funktionieren jedoch nicht in Inhalts-Skripten, aber sie tun es weiterhin in normalen Erweiterungsseiten.

Dies wird erreicht, indem man privilegiertere XHR- und Fetch-Instanzen im Inhalts-Skript bereitstellt, was den Nebeneffekt hat, dass die Origin- und Referer-Header nicht gesetzt werden, wie es eine Anfrage von der Seite selbst tun würde; dies ist oft vorzuziehen, um zu verhindern, dass die Anfrage ihre Überquerung von Ursprüngen offenbart.

Hinweis: In Firefox in Manifest V2 können Erweiterungen, die Anfragen ausführen müssen, die so verhalten, als ob sie vom eigentlichen Inhalt gesendet wurden, content.XMLHttpRequest und content.fetch() stattdessen verwenden.

Für browserübergreifende Erweiterungen muss das Vorhandensein dieser Methoden funktional nachgewiesen werden.

Dies ist in Manifest V3 nicht möglich, da content.XMLHttpRequest und content.fetch() nicht verfügbar sind.

Hinweis: In Chrome, beginnend mit Version 73, und Firefox, beginnend mit Version 101 bei Verwendung von Manifest V3, unterliegen Inhalts-Skripte der gleichen CORS-Richtlinie wie die Seite, auf der sie laufen. Nur Backend-Skripte haben erhöhte Privilegien über Domains. Siehe Änderungen bei Cross-Origin-Anfragen in Chrome Extension Content Scripts.

Kommunikation mit Hintergrundskripten

Obwohl Inhalts-Skripte nicht direkt die meisten der WebExtension-APIs verwenden können, können sie mit den Hintergrundskripten der Erweiterung über die Messaging-APIs kommunizieren und so indirekt auf alle dieselben APIs zugreifen, die auch die Hintergrundskripte verwenden können.

Es gibt zwei grundlegende Muster für die Kommunikation zwischen den Hintergrund- und Inhalts-Skripten:

  • Sie können einmalige Nachrichten senden (mit einer optionalen Antwort).
  • Sie können eine längerlebige Verbindung zwischen den beiden Seiten einrichten und diese Verbindung nutzen, um Nachrichten auszutauschen.

Einmalige Nachrichten

Um einmalige Nachrichten zu senden, mit einer optionalen Antwort, können Sie die folgenden APIs verwenden:

Im Inhalts-Skript Im Hintergrund-Skript
Eine Nachricht senden browser.runtime.sendMessage() browser.tabs.sendMessage()
Eine Nachricht empfangen browser.runtime.onMessage browser.runtime.onMessage

Beispielsweise hier ein Inhalts-Skript, das auf Klickereignisse in der Webseite hört.

Wenn der Klick auf einen Link war, sendet es eine Nachricht an die Hintergrundseite mit der Ziel-URL:

js
// content-script.js

window.addEventListener("click", notifyExtension);

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

Das Hintergrund-Skript lauscht auf diese Nachrichten und zeigt eine Benachrichtigung mithilfe der notifications API an:

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

(Dieses Beispielcode ist leicht adaptiert von dem notify-link-clicks-i18n-Beispiel auf GitHub.)

Verbindung-basierte Nachrichtenübermittlung

Das Senden von einmaligen Nachrichten kann mühsam werden, wenn Sie viele Nachrichten zwischen einem Hintergrundskript und einem Inhalts-Skript austauschen. Ein alternatives Muster besteht darin, eine längerlebige Verbindung zwischen den beiden Kontexten zu etablieren und diese Verbindung zum Nachrichtenaustausch zu verwenden.

Beide Seiten haben ein runtime.Port-Objekt, das sie zum Nachrichtenaustausch verwenden können.

Um die Verbindung herzustellen:

Diese Rückkehr ein runtime.Port-Objekte.

Sobald jede Seite einen Port hat, können die beiden Seiten:

  • Nachrichten mit runtime.Port.postMessage() senden
  • Nachrichten mit runtime.Port.onMessage() empfangen

Zum Beispiel, sobald es geladen ist, das folgende Inhalts-Skript:

  • Verbindet sich mit dem Hintergrund-Skript
  • Speichert den Port in einer Variable myPort
  • Lauscht auf Nachrichten auf myPort (und protokolliert sie)
  • Verwendet myPort, um Nachrichten an das Hintergrund-Skript zu senden, wenn der Benutzer auf das Dokument klickt
js
// 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 entsprechende Hintergrund-Skript:

  • Lauscht auf Verbindungsversuche vom Inhalts-Skript

  • Wenn es einen Verbindungsversuch erhält:

    • Speichert den Port in einer Variablen namens portFromCS
    • Sendet dem Inhalts-Skript eine Nachricht mittels des Ports
    • Beginnt auf Nachrichten zu lauschen, die auf dem Port empfangen werden, und protokolliert sie
  • Sendet Nachrichten an das Inhalts-Skript, mithilfe von portFromCS, wenn der Benutzer auf die Browser-Aktion der Erweiterung klickt

js
// 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 Inhalts-Skripte

Wenn Sie mehrere Inhalts-Skripte verwenden, die gleichzeitig kommunizieren, könnten Sie Verbindungen zu ihnen in einem Array speichern wollen.

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

Entscheidung zwischen einmaligen Nachrichten und Verbindung-basierter Nachrichtenübermittlung

Die Wahl zwischen einmaligen und Verbindung-basierter Nachrichtenübermittlung hängt davon ab, wie Ihre Erweiterung erwartet, Messaging zu nutzen.

Die empfohlenen Best Practices sind:

  • Verwenden Sie einmalige Nachrichten, wenn…
    • Nur eine Antwort auf eine Nachricht erwartet wird.
    • Eine kleine Anzahl von Skripten Nachrichten empfangen (runtime.onMessage Aufrufe).
  • Verwenden Sie Verbindung-basierte Nachrichtenübermittlung, wenn…
    • Skripte an Sitzungen teilnehmen, in denen mehrere Nachrichten ausgetauscht werden.
    • Die Erweiterung über den Fortschritt einer Aufgabe wissen muss oder wenn eine Aufgabe unterbrochen wird oder eine Aufgabe unterbrechen möchte, die durch Messaging initiiert wurde.

Kommunikation mit der Webseite

Standardmäßig haben Inhalts-Skripte keinen Zugriff auf die von Seitenskripten erstellten Objekte. Sie können jedoch mit Seitenskripten mithilfe der DOM-APIs window.postMessage und window.addEventListener kommunizieren.

Zum Beispiel:

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

Für ein vollständiges Arbeitsbeispiel, besuchen Sie die Demo-Seite auf GitHub und folgen Sie den Anweisungen.

Warnung: Seien Sie sehr vorsichtig, wenn Sie in dieser Weise mit nicht vertrauenswürdigen Webinhalten interagieren! Erweiterungen sind privilegierter Code, der mächtige Fähigkeiten haben kann, und feindliche Webseiten können sie leicht dazu verleiten, auf diese Fähigkeiten zuzugreifen.

Um ein triviales Beispiel zu geben, nehmen wir an, dass der Inhaltsskriptcode, der die Nachricht empfängt, so etwas wie dieses macht:

js
// 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 Privilegien des Inhalts-Skripts ausführen.

Verwendung von eval() in Inhalts-Skripten

Hinweis: eval() ist nicht verfügbar in Manifest V3.

In Chrome

eval führt immer Code im Kontext des Inhalts-Skripts aus, nicht im Kontext der Seite.

In Firefox

Wenn Sie eval() aufrufen, führt es Code im Kontext des Inhalts-Skripts aus.

Wenn Sie window.eval() aufrufen, führt es Code im Kontext der Seite aus.

Betrachten Sie zum Beispiel ein Inhalts-Skript wie dieses:

js
// 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 einfach einige Variablen x und y mit Hilfe von window.eval() und eval(), protokolliert ihre Werte und sendet dann eine Nachricht an die Seite.

Beim Empfang der Nachricht protokolliert das Seitenskript dieselben Variablen:

js
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 erzeugt dies 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 erzeugt dies 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 potenziell schädlichen Webseiten kontrolliert, die Objekte, mit denen Sie interagieren, neu definieren können, um sich auf unerwartete Weise zu verhalten:

js
// page.js definiert console.log neu

let original = console.log;

console.log = () => {
  original(true);
};
js
// content-script.js ruft die neu definierte Version auf

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