Inhalts-Skripte

Ein Inhalts-Skript ist ein Teil Ihrer Erweiterung, das im Kontext einer Webseite ausgeführt wird. Es kann Seiteninhalte mithilfe der standardmäßigen Web-APIs lesen und ändern. Das Verhalten von Inhalts-Skripten ähnelt dem von Skripten, die Teil einer Website sind, wie zum Beispiel solche, die mit dem <script>-Element geladen werden. Inhalts-Skripte können jedoch nur auf Seiteninhalte zugreifen, wenn Host-Berechtigungen für die Herkunft der Webseite gewährt wurden.

Inhalts-Skripte können auf eine kleine Teilmenge der WebExtension-APIs zugreifen, können jedoch mit Hintergrund-Skripten kommunizieren mithilfe eines Nachrichtensystems und somit indirekt auf die WebExtension-APIs zugreifen. Hintergrund-Skripte können auf alle WebExtension JavaScript-APIs zugreifen, jedoch nicht direkt auf die Inhalte von Webseiten.

Hinweis:Einige Web-APIs sind aufsichere Kontexte beschränkt, was auch für Inhalts-Skripte gilt, die in diesen Kontexten ausgeführt werden. Eine Ausnahme ist PointerEvent.getCoalescedEvents(), das in Firefox von Inhalts-Skripten in unsicheren Kontexten aufgerufen werden kann.

Laden von Inhalts-Skripten

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

  1. Zum Installationszeitpunkt, in Seiten, die mit URL-Mustern übereinstimmen.
  2. Zur Laufzeit, in Seiten, die mit URL-Mustern übereinstimmen.
  3. Zur Laufzeit, in spezifische Tabs.

Es gibt nur einen globalen Gültigkeitsbereich pro Frame, pro Erweiterung. Das bedeutet, dass Variablen aus einem Inhalts-Skript von anderen Inhalts-Skripten unabhängig davon zugegriffen werden können, wie das Inhalts-Skript geladen wurde.

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

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

Hinweis: Dynamische JS-Modulimporte funktionieren jetzt in Inhalts-Skripten. Weitere Details finden Sie unter Firefox Bug 1536094. Nur URLs mit dem moz-extension-Schema sind erlaubt, was data-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 sind nicht persistent.

Inhalts-Skripte, die im Manifest-Datei-Schlüssel content_scripts definiert sind oder mit der scripting.registerContentScripts() API, oder (nur in Manifest V2 in Firefox) mit der contentScripts API, sind standardmäßig persistent. Sie bleiben über Browser- und Erweiterungsneustarts hinweg registriert.

Die scripting.registerContentScripts() API bietet jedoch die Möglichkeit, das Skript als nicht persistent zu definieren. Dies kann sinnvoll sein, wenn Ihre Erweiterung (im Namen eines Nutzers) 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 sind.

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

Beginnend mit Manifest V3 werden Host-Berechtigungen nicht automatisch zum Installationszeitpunkt gewährt. Nutzer können nach der Installation der Erweiterung für oder gegen Host-Berechtigungen optieren.

Eingeschränkte Domains

Sowohl Host-Berechtigungen als auch die activeTab-Berechtigung haben Ausnahmen für bestimmte Domains. Inhalts-Skripte werden daran gehindert, auf diesen Domains auszuführen, um den Nutzer vor einer Eskalation der Privilegien durch Erweiterungen in speziellen Seiten zu schützen.

In Firefox schließen diese Domains ein:

  • 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 für die Websites, aus denen Erweiterungen installiert werden können. Zum Beispiel ist der Zugriff auf chrome.google.com in Chrome eingeschränkt.

Hinweis: Da diese Einschränkungen addons.mozilla.org einschließen, könnten Nutzer, die versuchen, Ihre Erweiterung direkt nach der Installation zu verwenden, feststellen, dass sie nicht funktioniert. Um dies zu vermeiden, sollten Sie eine entsprechende Warnung oder eine Onboarding-Seite hinzufügen, um Nutzer von addons.mozilla.org wegzuführen.

Mit Unternehmensrichtlinien kann der Satz an Domains weiter eingeschränkt werden: Firefox erkennt die restricted_domains-Richtlinie an, wie sie unter ExtensionSettings in mozilla/policy-templates dokumentiert ist. Chromes runtime_blocked_hosts-Richtlinie ist in der 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 für die Injektion von Inhalts-Skripten in solche speziellen Dokumente variiert zwischen den Browsern. Weitere Details finden Sie im Firefox Bug #1411641 Kommentar 41.

Inhalts-Skript Umgebung

DOM-Zugriff

Inhalts-Skripte können auf das DOM der Seite zugreifen und es ändern, wie es normale Seiten-Skripte auch können. Sie können auch alle Änderungen sehen, die von Seiten-Skripten am DOM vorgenommen wurden.

Inhalts-Skripte erhalten jedoch eine "saubere" Ansicht des DOM. Das bedeutet:

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

Wie unter "Inhalts-Skript Umgebung" bei Chrome-Inkompatibilitäten erwähnt, unterscheidet sich das Verhalten zwischen den Browsern:

  • In Firefox wird dieses Verhalten Xray vision genannt. Inhalts-Skripte können auf JavaScript-Objekte aus ihrem eigenen globalen Gültigkeitsbereich oder auf Xray-umwickelte Versionen der Webseite stoßen.

  • In Chrome wird dieses Verhalten durch eine isolierte Welt erzwungen, das 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 dies:

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'");
};

Jetzt 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 gilt umgekehrt; Seitenskripte können JavaScript-Eigenschaften, die von Inhalts-Skripten hinzugefügt wurden, nicht sehen.

Dies bedeutet, dass sich Inhalts-Skripte darauf verlassen können, dass DOM-Eigenschaften vorhersehbar funktionieren, ohne sich Sorgen machen zu müssen, dass ihre Variablen mit Variablen aus dem Seiten-Skript kollidieren.

Eine praktische Konsequenz dieses Verhaltens ist, dass ein Inhalts-Skript keinen Zugang zu JavaScript-Bibliotheken hat, die von der Seite geladen werden. Wenn die Seite also zum Beispiel jQuery enthält, kann das Inhalts-Skript es nicht sehen.

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

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

Hinweis: Firefox bietet cloneInto() und exportFunction(), um es Inhalts-Skripten zu ermöglichen, auf JavaScript-Objekte zuzugreifen, die von Seitenskripten erstellt wurden, und deren JavaScript-Objekte an Seitenskripte freizugeben.

Siehe Gemeinsame Nutzung von Objekten mit Seitenskripten für weitere Details.

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 stellen.

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

In Chrome und Firefox in Manifest V3, werden diese Anfragen im Kontext der Seite gestellt, sodass sie zu einer relativen URL gemacht werden. Zum Beispiel wird /api an https://«current page URL»/api geschickt.

Inhalts-Skripte erhalten dieselben domainübergreifenden Privilegien wie der Rest der Erweiterung: Wenn die Erweiterung für eine Domain den domainübergreifenden Zugriff über den permissions-Schlüssel in manifest.json angefordert hat, erhalten ihre Inhalts-Skripte ebenfalls Zugriff auf diese Domain.

Hinweis: Bei der Verwendung von Manifest V3 können Inhalts-Skripte dann plattformübergreifende Anfragen durchführen, wenn der Zielserver mithilfe von CORS zustimmt; jedoch funktionieren Host-Berechtigungen nicht in Inhalts-Skripten, aber sie tun es immer noch in regulären Erweiterungsseiten.

Dies wird durch die Bereitstellung privilegierterer XHR- und Fetch-Instanzen im Inhalts-Skript erreicht, welche den Nebeneffekt haben, dass die Origin- und Referer-Header nicht gesetzt werden, wie eine Anfrage von der Seite selbst es tun würde; dies ist oft bevorzugt, um zu verhindern, dass die Anfrage ihre plattformübergreifende Natur offenbart.

Hinweis: In Firefox in Manifest V2 können Erweiterungen, die Anfragen ausführen müssen, die sich wie von den Inhalten selbst gesendet verhalten, stattdessen content.XMLHttpRequest und content.fetch() verwenden.

Für plattformübergreifende Erweiterungen muss das Vorhandensein dieser Methoden durch Funktionserkennung überprüft 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 der Verwendung von Manifest V3, unterliegen Inhalts-Skripte derselben CORS-Richtlinie wie die Seite, in der sie ausgeführt werden. Nur Back-End-Skripte haben erhöhte plattformübergreifende Privilegien. Siehe Änderungen bei plattformübergreifenden Anfragen in Chrome Extension Content Scripts.

Kommunikation mit Hintergrund-Skripten

Obwohl Inhalts-Skripte die meisten der WebExtension-APIs nicht direkt verwenden können, können sie über die Messaging-APIs mit den Hintergrund-Skripten der Erweiterung kommunizieren und erhalten somit indirekt Zugriff auf dieselben APIs, auf die auch die Hintergrund-Skripte zugreifen können.

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

  • Sie können einmalige Nachrichten senden (mit einer optionalen Antwort).
  • Sie können eine längerfristige 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

Zum Beispiel ein Inhalts-Skript, das auf Klick-Ereignisse auf der Webseite hört.

Wenn der Klick auf einem 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 hört auf diese Nachrichten und zeigt eine Benachrichtigung mit 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,
  });
}

(Dieser Beispielcode ist leicht angepasst aus dem notify-link-clicks-i18n Beispiel auf GitHub.)

Verbindungsgestützte Nachrichtenübermittlung

Das Senden von einmaligen Nachrichten kann umständlich werden, wenn Sie viele Nachrichten zwischen einem Hintergrund-Skript und einem Inhalts-Skript austauschen. Ein alternatives Muster besteht darin, eine längerfristige Verbindung zwischen den beiden Kontexts herzustellen und diese Verbindung zu nutzen, um Nachrichten auszutauschen.

Beide Seiten haben ein runtime.Port-Objekt, das sie verwenden können, um Nachrichten auszutauschen.

Um die Verbindung herzustellen:

Dies gibt ein runtime.Port-Objekt zurück.

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, verbindet sich das folgende Inhalts-Skript:

  • Mit dem Hintergrund-Skript
  • Speichert den Port in einer Variable myPort
  • Hört auf Nachrichten auf myPort (und protokolliert sie)
  • Sendet Nachrichten an das Hintergrund-Skript, wenn der Benutzer das Dokument anklickt
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:

  • Hört auf Verbindungsversuche vom Inhalts-Skript

  • Wenn es einen Verbindungsversuch empfängt:

    • Speichert den Port in einer Variablen namens portFromCS
    • Sendet dem Inhalts-Skript eine Nachricht mit dem Port
    • Beginnt, Nachrichten zu hören, die auf dem Port empfangen werden, und protokolliert sie
  • Sendet Nachrichten an das Inhalts-Skript, indem es portFromCS verwendet, 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 haben, die gleichzeitig kommunizieren, möchten Sie möglicherweise Verbindungen zu ihnen in einem Array speichern.

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

Auswahl zwischen einmaligen Nachrichten und verbindungsgestützter Nachrichtenübermittlung

Die Wahl zwischen einmaligen und verbindungsgestützter Nachrichtenübermittlung hängt davon ab, wie Ihre Erweiterung die Nachrichtenübermittlung verwenden möchte.

Die empfohlenen Best Practices sind:

  • Verwenden Sie einmalige Nachrichten, wenn…
    • Nur eine Antwort auf eine Nachricht erwartet wird.
    • Eine geringe Anzahl von Skripten Nachrichten empfängt (runtime.onMessage-Aufrufe).
  • Verwenden Sie die verbindungsgestützte Nachrichtenübermittlung, wenn…
    • Skripte Kommunikationssitzungen durchführen, in denen mehrere Nachrichten ausgetauscht werden.
    • Die Erweiterung über den Fortschritt einer Aufgabe oder eine Unterbrechung einer Aufgabe informiert werden muss oder eine Aufgabe, die über die Nachrichtenübermittlung initiiert wurde, unterbrechen möchte.

Kommunikation mit der Webseite

Standardmäßig erhalten Inhalts-Skripte keinen Zugriff auf die Objekte, die von Seitenskripten erstellt wurden. Sie können jedoch mit Seitenskripten über die 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 funktionierendes Beispiel dafür, Besuchen Sie die Demoseite auf GitHub und folgen Sie den Anweisungen.

Warnung: Seien Sie äußerst vorsichtig beim Interagieren mit unzuverlässigen Inhalten auf diese Weise! Erweiterungen sind privilegierter Code, der mächtige Fähigkeiten haben kann, und feindliche Webseiten können sie leicht dazu bringen, auf diese Fähigkeiten zuzugreifen.

Um ein triviales Beispiel zu geben, nehmen Sie an, dass der Inhalt-Skript-Code, der die Nachricht empfängt, etwas wie dies tut:

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.

Die Verwendung von eval() in Inhalts-Skripten

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

In Chrome

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

In Firefox

Wenn Sie eval() aufrufen, wird der Code im Kontext des Inhalts-Skriptes ausgeführt.

Wenn Sie window.eval() aufrufen, wird der Code im Kontext der Seite ausgeführt.

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 window.eval() und eval(), protokolliert deren 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 ergibt 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 ergibt 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 äußerst vorsichtig, wenn Sie Code im Kontext der Seite ausführen!

Die Umgebung der Seite wird von potenziell böswilligen Webseiten kontrolliert, die Objekte, mit denen Sie interagieren, neu definieren können, um sich unerwartet 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)");