Inhaltsskripte

Ein Inhaltsskript 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. Das Verhalten eines Inhaltsskripts ähnelt dem von Skripten, die Teil einer Website sind, wie z.B. solche, die mit dem <script>-Element geladen werden. Allerdings können Inhaltsskripte nur auf Seiteninhalte zugreifen, wenn Host-Berechtigungen für den Ursprung der Webseite gewährt wurden.

Inhaltsskripte können auf einen kleinen Teil der WebExtension-APIs zugreifen, jedoch können sie mit Hintergrundskripten kommunizieren und dadurch indirekt auf die WebExtension-APIs zugreifen. Hintergrundskripte können auf alle WebExtension-JavaScript-APIs zugreifen, aber nicht direkt auf die Inhalte von Webseiten.

Hinweis: Einige Web-APIs sind auf sichere Kontexte beschränkt, was auch für Inhaltsskripte gilt, die in diesen Kontexten laufen. Mit Ausnahme von PointerEvent.getCoalescedEvents(), das in Inhaltsskripten in unsicheren Kontexten in Firefox aufgerufen werden kann.

Laden von Inhaltsskripten

Sie können ein Inhaltsskript in eine Webseite laden:

  1. Zur Installationszeit, 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 eines Inhaltsskripts von allen anderen Inhaltsskripten, unabhängig davon, wie das Inhaltsskript geladen wurde, zugänglich sind.

Mit den Methoden (1) und (2) können Sie Skripte nur in Seiten laden, deren URLs mit einem Match-Muster 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 privilegierten Browserseiten (wie about:debugging oder about:addons) laden.

Hinweis: Dynamische JS-Modulimporte funktionieren jetzt in Inhaltsskripten. Weitere Details finden Sie im Firefox Bug 1536094. Nur URLs mit dem moz-extension-Schema sind erlaubt, was Daten-URLs ausschließt (Firefox Bug 1587336).

Persistenz

Inhaltsskripte, die mit scripting.executeScript() oder (nur in Manifest V2) tabs.executeScript() geladen werden, laufen auf Anforderung und sind nicht persistent.

Inhaltsskripte, die im Manifest-Dokument über den Schlüssel content_scripts definiert sind oder mit der scripting.registerContentScripts() oder (nur in Manifest V2 in Firefox) contentScripts API registriert sind, bleiben standardmäßig persistent. Sie bleiben über Browser-Neustarts, Updates und Erweiterungs-Neustarts hinweg registriert.

Die scripting.registerContentScripts() API ermöglicht jedoch die Definition des Skripts als nicht persistent. Dies kann nützlich sein, wenn Ihre Erweiterung beispielsweise im Namen eines Benutzers ein Inhaltsskript nur in der aktuellen Browsersitzung aktivieren möchte.

Berechtigungen, Einschränkungen und Begrenzungen

Berechtigungen

Registrierte Inhaltsskripte werden nur dann ausgeführt, wenn der Erweiterung Host-Berechtigungen für die Domain gewährt wurden.

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

Ab Manifest V3 werden Host-Berechtigungen nicht automatisch zur Installationszeit gewährt. Benutzer können sich nach der Installation der Erweiterung für oder gegen Host-Berechtigungen entscheiden.

Eingeschränkte Domains

Sowohl Host-Berechtigungen als auch die Berechtigung activeTab haben Ausnahmen für einige Domains. Inhaltsskripte werden auf diesen Domains blockiert, um beispielsweise den Benutzer davor zu schützen, dass eine Erweiterung Privilegien durch spezielle Seiten eskaliert.

In Firefox umfasst dies die folgenden 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 Websites, von 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, kann es sein, dass Benutzer, die versuchen, Ihre Erweiterung sofort nach der Installation zu verwenden, feststellen, dass sie nicht funktioniert. Um dies zu vermeiden, sollten Sie eine entsprechende Warnung oder eine Willkommensseite hinzufügen, um Benutzer von addons.mozilla.org wegzubewegen.

Das Set der Domains kann durch Unternehmensrichtlinien weiter eingeschränkt werden: Firefox erkennt die restricted_domains Richtlinie an, wie bei ExtensionSettings in mozilla/policy-templates dokumentiert. Chromes runtime_blocked_hosts Richtlinie ist bei Configure ExtensionSettings policy dokumentiert.

Begrenzungen

Ganze Tabs oder Frames können mit data: URI, Blob Objekten und anderen ähnlichen Techniken geladen werden. Die Unterstützung für die Injektion von Inhaltsskripten in solche speziellen Dokumente variiert je nach Browser. Einzelheiten finden Sie in Firefox-Bug #1411641 Kommentar 41.

Inhaltsskript-Umgebung

DOM-Zugriff

Inhaltsskripte können auf das DOM der Seite zugreifen und es ändern, genauso wie normale Seitenskripte dies können. Sie können auch alle Änderungen sehen, die an dem DOM durch Seitenskripte vorgenommen wurden.

Allerdings erhalten Inhaltsskripte eine "saubere" Sicht auf das DOM. Das bedeutet:

  • Inhaltsskripte können keine JavaScript-Variablen sehen, die durch Seitenskripte definiert wurden.
  • Wenn ein Seitenskript eine eingebaute DOM-Eigenschaft neu definiert, sieht das Inhaltsskript die ursprüngliche Version der Eigenschaft, nicht die neu definierte Version.

Wie im Abschnitt "Inhaltsskript-Umgebung" bei Inkompatibilitäten in Chrome erwähnt, unterscheidet sich das Verhalten je nach Browser:

  • In Firefox wird dieses Verhalten als Xray-Sicht bezeichnet. Inhaltsskripte können JavaScript-Objekte aus ihrem eigenen globalen Gültigkeitsbereich oder Xray-umwickelte Versionen von den Webseiten sehen.

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

Stellen Sie sich eine Webseite wie diese vor:

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 Inhaltsskript 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()

Das Gleiche gilt umgekehrt; Seitenskripte können JavaScript-Eigenschaften, die von Inhaltsskripten hinzugefügt wurden, nicht sehen.

Das bedeutet, dass Inhaltsskripte sich darauf verlassen können, dass DOM-Eigenschaften vorhersehbar funktionieren, ohne sich Sorgen machen zu müssen, dass ihre Variablen mit Variablen aus dem Seitenskript kollidieren.

Eine praktische Konsequenz dieses Verhaltens ist, dass ein Inhaltsskript auf keine JavaScript-Bibliotheken zugreifen kann, die von der Seite geladen werden. Wenn die Seite beispielsweise jQuery enthält, kann das Inhaltsskript es nicht sehen.

Wenn ein Inhaltsskript eine JavaScript-Bibliothek verwenden muss, sollte die Bibliothek selbst als Inhaltsskript neben dem Inhaltsskript 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() an, um Inhaltsskripten den Zugriff auf JavaScript-Objekte zu ermöglichen, die von Seitenskripten erstellt wurden, und um ihre JavaScript-Objekte Seitenskripten zugänglich zu machen.

Weitere Informationen finden Sie unter Teilen von Objekten mit Seitenskripten.

WebExtension-APIs

Zusätzlich zu den standardmäßigen DOM-APIs können Inhaltsskripte folgende WebExtension-APIs verwenden:

Von extension:

Von runtime:

Von i18n:

Von menus:

Alles von:

XHR und Fetch

Inhaltsskripte können Anfragen mit den normalen window.XMLHttpRequest und window.fetch() APIs ausführen.

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

In Chrome und Firefox in Manifest V3 erfolgen diese Anfragen im Kontext der Seite, daher werden sie mit einer relativen URL durchgeführt. Zum Beispiel wird /api an https://«current page URL»/api gesendet.

Inhaltsskripte erhalten dieselben bereichsübergreifenden Privilegien wie der Rest der Erweiterung: Wenn die Erweiterung also für eine Domain den bereichsübergreifenden Zugriff über den Schlüssel permissions in manifest.json angefordert hat, erhalten auch deren Inhaltsskripte Zugang zu dieser Domain.

Hinweis: Bei Verwendung von Manifest V3 können Inhaltsskripte bereichsübergreifende Anfragen ausführen, wenn der Zielserver über CORS zustimmt; Host-Berechtigungen funktionieren jedoch nicht in Inhaltsskripten, jedoch weiterhin in regulären Erweiterungsseiten.

Dies wird erreicht, indem inhaltsskripten privilegiertere XHR- und fetch-Instanzen bereitgestellt werden, was zur Folge hat, dass die Header Origin und Referer nicht gesetzt werden, wie es bei einer Anfrage von der Seite selbst der Fall wäre; dies ist oft vorzuziehen, um zu verhindern, dass die Anfrage ihre bereichsübergreifende Natur offenbart.

Hinweis: In Firefox in Manifest V2 können Erweiterungen, die Anfragen ausführen müssen, die sich so verhalten, als ob sie vom Inhalt selbst gesendet worden wären, content.XMLHttpRequest und content.fetch() verwenden.

Für browserübergreifende Erweiterungen muss die Anwesenheit dieser Methoden durch Funktionsprüfung ermittelt werden.

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

Hinweis: In Chrome, ab Version 73, und Firefox, ab Version 101 bei Verwendung von Manifest V3, unterliegen Inhaltsskripte derselben CORS Richtlinie wie die Seite, in der sie ausgeführt werden. Nur Hintergrundskripte haben erhöhte bereichsübergreifende Privilegien. Siehe Änderungen bei bereichsübergreifenden Anfragen in Chrome-Erweiterungs-Inhaltsskripten.

Kommunikation mit Hintergrundskripten

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

Es gibt zwei grundlegende Muster zur Kommunikation zwischen Hintergrundskripten und Inhaltsskripten:

  • Sie können Einweg-Nachrichten senden (mit einer optionalen Antwort).
  • Sie können eine länger lebende Verbindung zwischen den beiden Seiten aufbauen und diese Verbindung zum Austausch von Nachrichten nutzen.

Einweg-Nachrichten

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

In Inhaltsskript In Hintergrundskript
Nachricht senden browser.runtime.sendMessage() browser.tabs.sendMessage()
Nachricht empfangen browser.runtime.onMessage browser.runtime.onMessage

Zum Beispiel ist hier ein Inhaltsskript, das auf Klickereignisse auf der Webseite lauscht.

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 Hintergrundskript lauscht 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.)

Verbindungsbasiertes Messaging

Das Senden von Einweg-Nachrichten kann mühsam werden, wenn Sie viele Nachrichten zwischen einem Hintergrundskript und einem Inhaltsskript austauschen. Ein alternatives Muster ist, eine längerfristige Verbindung zwischen den beiden Kontexten herzustellen und diese Verbindung zum Austauschen von Nachrichten zu nutzen.

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

Um die Verbindung herzustellen:

  • Eine Seite lauscht Verbindungen mit runtime.onConnect

  • Die andere Seite ruft auf:

    • tabs.connect() (wenn eine Verbindung zu einem Inhaltsskript hergestellt wird)
    • runtime.connect() (wenn eine Verbindung zu einem Hintergrundskript hergestellt wird)

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 verbindet sich folgendes Inhaltsskript sofort nach dem Laden mit dem Hintergrundskript, speichert den Port in einer Variable myPort, lauscht auf Nachrichten über myPort (und protokolliert sie) und verwendet myPort, um Nachrichten an das Hintergrundskript 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 Hintergrundskript lauscht auf Verbindungsversuche vom Inhaltsskript, und wenn es eine Verbindung erhält, speichert es den Port in einer Variablen namens portFromCS, sendet dem Inhaltsskript eine Nachricht über den Port und lauscht auf Nachrichten, die über den Port empfangen werden und protokolliert diese:

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 Inhaltsskripte

Wenn Sie mehrere Inhaltsskripte haben, die gleichzeitig kommunizieren, möchten Sie vielleicht die 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 Einweg-Nachrichten und verbindungsbasiertem Messaging

Die Wahl zwischen Einweg- und verbindungsbasiertem Messaging hängt davon ab, wie Ihre Erweiterung Messaging nutzen möchte.

Die empfohlenen Best Practices sind:

  • Verwenden Sie Einweg-Nachrichten, wenn…
    • Nur eine Antwort auf eine Nachricht erwartet wird.
    • Eine kleine Anzahl von Skripten Nachrichten empfängt (runtime.onMessage Aufrufe).
  • Verwenden Sie verbindungsbasiertes Messaging, wenn…
    • Skripte an Sitzungen teilnehmen, in denen mehrere Nachrichten ausgetauscht werden.
    • Die Erweiterung den Fortschritt einer Aufgabe kennen muss oder wissen muss, ob eine Aufgabe unterbrochen wird, oder eine mittels Messaging begonnene Aufgabe unterbrechen möchte.

Kommunikation mit der Webseite

Standardmäßig erhalten Inhaltsskripte keinen Zugriff auf 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 komplett arbeitsfähiges Beispiel, besuchen Sie die Demoseite auf GitHub und folgen Sie den Anweisungen.

Warnung: Seien Sie sehr vorsichtig, wenn Sie in dieser Weise mit untrusted Web-Inhalten interagieren! Erweiterungen sind privilegierter Code, der mächtige Möglichkeiten hat, und feindliche Webseiten können sie leicht täuschen, um auf diese Möglichkeiten zuzugreifen.

Um ein triviales Beispiel zu geben, nehmen Sie an, dass das Inhaltskript, welches die Nachricht empfängt, etwas in der Art wie das Folgende ausführt:

js
// content-script.js

window.addEventListener("message", (event) => {
  if (
    event.source === window &&
    event?.data?.direction === "from-page-script"
  ) {
    eval(event.data.message);
  }
});

Jetzt kann das Seitenskript beliebigen Code mit allen Privilegien des Inhaltsskriptes ausführen.

Verwendung von eval() in Inhaltsskripten

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

In Chrome

eval führt immer Code im Kontext des Inhaltsskriptes aus, nicht im Kontext der Seite.

In Firefox

Wenn Sie eval() aufrufen, führt es den Code im Kontext des Inhaltsskriptes aus.

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

Zum Beispiel betrachten Sie ein Inhaltsskript wie folgt:

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(), loggt deren Werte und sendet dann Nachrichten an die Seite.

Beim Empfang der Nachricht loggt 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 folgt:

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 folgt:

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

Das Gleiche 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 bösartigen Webseiten kontrolliert, die Objekte, mit denen Sie interagieren, neu definieren können, um sich unerwartet zu verhalten:

js
// page.js redefiniert console.log

let original = console.log;

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

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