Introduction aux WebExtensions

Modifier le Web ou l’interception des requêtes et du fonctionnement des pages Web et la transformation du contenu grâce à XSLT et JavaScript

Julien Garderon

Ce tutoriel expliquera comment modifier les requêtes reçues (en réalité, vous pouvez également contrôler celles envoyées, les intercepter, en produire d’autres…). Cependant ce n’est pas la modification en tant que telle qui est intéressante même si, pour beaucoup de développements en Web, c’est déjà un changement de paradigme. C’est appréhender une vision radicale des « pages » du Web au profit d’une gestion des ressources offertes par le réseau et de l’utilisation de l’ensemble des « contenus » du navigateur.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Introduction aux WebExtensions

- Auteur : Julien Garderon

- Parution : 16 octobre 2018

- Licence : Copyright ® 2018 Julien Garderon. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Mises en garde

- I-B. Explications et objectifs

- I-B-1. Rappel

- I-C. Un des outils utilisés : le XSL

- I-C-1. XSL : un « ancêtre » si performant

- I-C-2. Pourquoi utiliser XSL pour les pages HTML à traiter ?

- II. Première partie – Bien plus large que le « Web »

- II-A. Pour quel navigateur ?

- II-A-1. Comment savoir si mon navigateur est compatible ?

- II-B. Démarrer le projet

- II-B-1. Comprendre JavaScript dans un navigateur moderne : portée et contextes

- II-B-2. Phase 0 - D’où l’on vient ; où l’on va

- II-B-3. Phase 1 - Détecter les requêtes intéressantes

- II-B-4. Phase 2 – Comprendre ce que l’on reçoit et ce que l’on émet

- II-B-4-a. Code source actuel du fichier background.js

- II-B-5. Phase 3 – Appliquer la transformation XSL

- II-B-5-a. Et sans XSL malgré tout ?

- II-C. Quelques réflexions supplémentaires

- II-D. Créer une page d’édition des transformations XSL

- II-D-1. La gabarisation en HTML5

- II-D-2. Un cas pratique et exotique de script de contenu

- II-E. Les sources complètes de la page d’édition

- II-E-1. La page

- II-E-2. Le script de contenu

- III. Seconde partie : ajouter un bouton d’action à un réseau social

- III-A. Le contexte

- III-B. Le principe

- III-C. Bonus : et si on affichait plutôt une notification ?

- III-D. Le code source complet du script twitter.js

- IV. Conclusion, ouverture et remerciements

- IV-A. De la question des droits et des usages

- IV-B. Les WebExtensions sont-elle finalement la pierre angulaire du Web décentralisé ?

- V. Note de la rédaction de Developpez.com

I - Introduction

I-A - Mises en garde

Cet article aborde le sujet d’extensions à votre navigateur avec des concepts poussés et de nombreux chausses-trappes même pour un développeur JS expérimenté. Cela n’indique pas que ce tutoriel leur est réservé, mais un certain niveau de connaissances est recommandé pour saisir pleinement les imbrications et comprendre que certaines « limitations » que j’expose, sont en réalité des nécessités absolues.

Du fait de la plus grande liberté dans l’accès aux données et des « droits » accordés, ces extensions agissent bien au-delà de leur pré carré et peuvent intercepter, modifier et annuler à la fois des requêtes (de manière transparente pour l’utilisateur, pour les entêtes ou les corps de requêtes) mais aussi certains protocoles de sécurité (en les contournant voire en les supprimant : par exemple la récupération d’un corps de requête sur une liaison TLS ou d’annuler une requête en HTTPS au profit de HTTP seulement). C’est-à-dire rendre vulnérable votre navigateur à des attaques ou à des exploits.

C’est aussi permettre de faire des captures d’écran, de stocker et télécharger des fichiers sur le poste du client, créer des PDF à la demande, gérer les onglets, l’historique, les marque-pages et tant d’autres choses…

Pour cela, je vous indique ici trois règles d’or qui peuvent paraître extrêmes mais comme nous le verrons plus loin, sont à la mesure de ce qui est offert par les API que nous utiliserons.

- Ne testez pas votre WebExtension sur votre navigateur « quotidien ». Tester dans une installation de votre navigateur « autonome » et dans un environnement si possible limité (chroot). Seulement lorsque vous serez sûr, testez en conditions plus « réelles ».

- N’accordez pas de droits « inutiles » à votre WebExtension, particulièrement WebRequest et WebNavigation. Ne tenter pas de communiquer avec des domaines ou des serveurs que vous ne connaissez pas et ne laissez jamais un serveur distant, même légitime, « prendre le contrôle » de votre WebExtension.

- Gardez à l’esprit que votre WebExtension, par sa seule présence, doit être considérée comme dégradant les performances et la sécurité – même si elle offre un service légitime : elle vient toujours en modifiant le processus normal et optimisé de votre navigateur. Le mieux est l’ennemi du bien : ne faites que ce qui est hautement utile.

I-B - Explications et objectifs

Si l’objectif premier de ce tutoriel est évidemment la présentation des WebExtension, de leur intérêts, de leurs constructions et de certains « pièges », nous allons nous attarder sur quelques points spécifiques d’un sujet par ailleurs bien trop vaste.

Ce tutoriel expliquera par exemple comment modifier les requêtes reçues (en réalité, vous pouvez également contrôler celles envoyées, les intercepter, en produire d’autres…). Cependant ce n’est pas la modification en tant que telle qui est intéressante même si, pour beaucoup de développements en Web, c’est déjà un changement de paradigme. C’est appréhender une vision radicale des « pages » du Web au profit d’une gestion des ressources offertes par le réseau et de l’utilisation de l’ensemble des « contenus » du navigateur.

La motivation des points abordés & des scripts liés est d’agir en lieu et place de ce qui est prévu par le serveur (ou le service) Web de l’autre côté de la connexion. C’est dénaturer non seulement quelques aspects graphiques mais aussi en violer le fonctionnement natif : nous verrons dans une seconde partie, qu’il n’est pas toujours possible de se contenter de la seule interception du code source et qu’il est parfois préférable et plus facile, de modifier à la marge le comportement « naturel » des pages. C’est tout autant un monde possible qui s’ouvre que d’alertes à garder à l’esprit.

… « Je croyais que la règle d’or indiquer qu’aucune communication ne devait avoir lieu avec un serveur inconnu et son contenu ? Une page en exécution n’entre pas ce registre ? » C’est vrai, mais il s’agit là d’un cas bien plus restrictif : nous utilisons un contenu déjà récupéré par ailleurs par le navigateur. Certains garde-fous existent. C’est-à-dire qu’à aucun moment nous n’agirons avec le serveur à proprement parler, et les scripts envoyés depuis le serveur, ne peuvent agir sur notre WebExtension (WebExt) : tout est fait à partir des ressources locales et exploitées depuis les seules ressources du navigateur. Cela reste un risque, mais moindre que celui d’entamer en direct l’interception HTTP (nous, nous agirons toujours au niveau de la source HTML dans nos exemples).

Dans la première partie donc, du contenu HTML classique, repris et modifié, avec un site que j’ai sélectionné car l’information est accessible pour tous mais au prix d’une grande lourdeur de page. Ici cette modification va annuler une partie aussi de la surveillance et du tracking – du suivi en bon français – publicitaire. Par la suite c’est à Twitter que nous nous occuperons, mais cela pourrait être Facebook ou tout autre réseau social. Dans les deux cas, des nouveaux services peuvent être associés, particulièrement en local.

Enfin garder à l’esprit qu’il n’y a aucun « hack » dans le code que je vous propose : vous ne verrez jamais davantage que ce que le serveur envoie – je n’entre d’ailleurs pas ici sur l’action de négociation des protocoles. Cependant ce qui est est exposé ici enfreint clairement la plupart des CGU – Conditions Générales d’Utilisation - des sites et des services : je vous incite à ne pas reproduire, en dehors d’un cadre pédagogique limité, cette mise bout-à-bout des possibilités des WebExtensions sans en comprendre les enjeux légaux qui s’appliquent dans votre pays.

I-B-1 - Rappel

L’auteur rejette par avance tous les problèmes de sécurité ou de stabilité, ou de contournement des CGU, occasionnés par l’utilisation des exemples fournis dans le présent article, qui se veut strictement de sensibilisation et d’apprentissage.

I-C - Un des outils utilisés : le XSL

I-C-1 - XSL : un « ancêtre » si performant

Depuis longtemps le HTML n’est plus dérivé du XML, même s’il en garde une ergonomie générale : les balises meta par exemple, dans le head, ne sont pas fermées ; c’est pour cela que l’on parle souvent de soupe en évoquant le code (la « source ») HTML. Parser (ou traduire en bon français) du HTML avec un outil conçu pour le XML échouera. Cela ne veut pas dire que le HTML ne peut pas passer à du XML ou l’inverse – mais ce n’est pas forcément sans conséquences. Il existe par ailleurs « d’anciennes » variantes comme le XHTML, qui elles reprennent les canons du XML.

Nous ne verrons pas ici ces « bizarreries » de HTML vis-à-vis du XML : contentons-nous de savoir qu’un lien entre les deux existent et que le passage de l’un à l’autre peut être aussi possible, notamment en JavaScript.

XSL – eXtensible Stylesheet Language – est aussi vieux que le XML. Il est une feuille de route, une notice de changements à opérer du XML en entrée, vers tout autre langage « compatible » avec le XML (ou qui en est dérivé : PDF, HTML, etc) ou du XML pur. Ces changements sont des transformations, définies par plusieurs normes. XSL s’appuie sur XPath, en quelque sorte la grammaire d’une écriture des liens et des parentés entre les éléments XML.

JavaScript, lorsque vous utilisez un document « DOM » (compatible avec…), s’appuie en grande partie sur des réalisations, sur un « esprit » XML pour gérer l’arborescence de HTML. Celle-ci est simplement moins stricte, car l’héritage de HTML a été imprégné – c’est là une opinion toute personnelle – d’années de dérives d’IE (Microsoft) et de raccourcis pour de mauvaises raisons des développeurs du Web par la suite.

Il n’y a pas d’incompatibilité à utiliser XML en entrée pour produire du HTML, y compris la 5e version du nom. Avec un peu de verve, une de mes actualités évoquait ces liens et l’utilisation des transformations pour générer des pages côté client. Nous utiliserons un principe similaire ici, en partie à cause d’un cadre avec des différences de contraintes vis-à-vis de ce qu’est le Web classique, celui côté client « page ».

En résumé :

- XML est un format de données statiques, tout comme HTML, qui hérite de certaines propriétés de XML ;

- XSL est un format de données des transformations à faire sur XML ;

- XPath permet des opérations de parcours complexe d’une arborescence XML. On parle également de « localisation » dans un arbre pouvant être représenté comme des ensembles d’éléments imbriqués ;

- L’ensemble est mixé par un moteur dédié (il en existe plusieurs), à partir d’une même norme, qui définit en outre des fonctions appelables au sein de XSL.

I-C-2 - Pourquoi utiliser XSL pour les pages HTML à traiter ?

Nous verrons plus loin dans cet article des limitations fortes dans le cadre des WebExt, notamment du côté de l’évaluation d’expressions ; je n’entre pas ici dans le détail. Gardons pour principe qu’en dehors de l’utilisation d’un pseudo-langage – avec le temps de développement / test, la complexité vis-à-vis de la finalité et la courbe d’apprentissage côté utilisateur ; bref caduque dans notre cas –, peu de solutions existent.

Or un code source HTML reçu d’un serveur, même avec un pattern matching des URL, reste très variable en fonction de chaque site et en fonction de chaque page sur un site. Il y a toujours la possibilité de faire un seul et même « moteur » pour récupérer l’information intéressante (et c’est par exemple offert avec certains navigateurs par le biais du mode « lecture »), mais cela reste de l’arbitraire du développeur : peut-être que l’utilisateur a d’autres souhaits, d’autres besoins.

Pour donner un peu de souplesse dans un tel cas, le XSL est le meilleur. Pas le plus simple, pas le plus facile, pas le moins fastidieux, mais probablement le plus efficace. Si seule la première version de la norme est implémentée largement dans les navigateurs (tous pour ceux qui supportent les WebExt), la seconde version, bien plus pointue et astucieuse, n’est pour ainsi dire pas disponible sauf à passer par des tiers.

De plus le XSL n’est jamais qu’une chaîne de caractères qui se stocke donc très bien (soit par stockage.local soit par IndexedDB), se parse en Javascript sans difficulté et s’applique à un objet Document (DOM).

Alors comment faire ?

II - Première partie – Bien plus large que le « Web »

II-A - Pour quel navigateur ?

Comme vous l’aurez désormais compris, point de serveur ou de connexion dans cet article. Tout se passe exclusivement entre votre navigateur et vous. J’utilise ici Firefox, mais sachez qu’avec très peu d’adaptations, la quasi-totalité de ce que j’indique est utilisable directement sous Chrome. En effet les WebExtensions sont régies par une norme qui définit les API utilisables ainsi que les différents contextes d’exécution et sont supportées et développés par les principales équipes de développeurs des navigateurs.

En bref, ce qui est commun ou différent entre navigateurs :

- tous utilisent principalement l’asynchronisme (callback pour Chrome, promise pour Firefox et Opera) ;

- le fichier de « découverte » et de configuration peut avoir quelques variations d’un navigateur à l’autre. Cependant ce n’est pas nécessairement une limite, car une bonne pratique est de séparer rapidement le projet en un tronc commun qui sera décliné par navigateur, afin de respecter la procédure de publication ;

- tous utilisent les mêmes droits (et les même portées de droits) à quelques nuances ;

- les WebExtensions sont publiables par un réseau certifiant et sécurisant a minima, les WebExtensions auprès des utilisateurs (ce qui n’empêche absolument pas d’utiliser un autre canal ou une installation directe).

II-A-1 - Comment savoir si mon navigateur est compatible ?

Simplement : s’il supporte les WebExtension, notamment à partir de la version 57 de Firefox (en réalité antérieurement, mais l’API est réduite car le multiprocessus n’y est pas encore la norme) et la version 25 de Chrome (même les WebExtensions n’étaient pas alors dans une version normée définitive), alors il est très probable que votre navigateur supporte la plupart de ce que je décris dans cet article.

II-B - Démarrer le projet

Tout d’abord créer un dossier dédié : inutile de vous préoccuper si ce dernier est accessible par un éventuel serveur ; il pourra fort bien être sur le bureau. Vous créerez un certain nombre de fichiers dans ce dossier qui sera la racine votre WebExt et que vous laisserez vides pour l’instant :

- /manifest.json : ce sera le fichier « de découverte » de l’extension par le navigateur, et de configuration. Dans notre cas, il y définira les scripts à charger, les droits et permissions, ainsi que des éléments de présentation. Sa seule présence pour tous les navigateurs est obligatoire ;

- /background.js : un script dit d’arrière-plan, qui sera complété au fur et à mesure de l’avancée du tutoriel ;

- /logo.png : vous pouvez y mettre une image comme vous le souhaitez (au format PNG naturellement).

Vous devez ensuite éditer le fichier manifest.json, en gardant à l’esprit que ce fichier, réellement indispensable, doit être construit avec soin. Il devra notamment être toujours valide au format JSON. Pour que l’extension soit valide au démarrage du projet, éditons le fichier comme suit :

1. {

2. "manifest_version": 2,

3. "name": "ControlePage",

4. "version": "1.0",

5. "description": "Contrôler les requêtes grâce aux 'pattern matching'.",

6. "icons": {

7. "48": "logo.png"

8. },

9. "permissions": [],

10. "background": {

11. "scripts": [

12. "background.js"

13. ]

14. }

15. }

Peu de choses à dire je crois, l’ensemble se comprenant facilement. A noter tout de même que la version d’un manifest doit toujours être à « 2 » (compte tenu que c’est une nouvelle « manière » de produire des extensions vis-à-vis de l’existant, dénommées régulièrement les add-ons sous Firefox). Également la racine de la WebExtension sera dans notre cas toujours le dossier dans lequel est le manifest.

Il ne reste qu’à charger l’extension dans le navigateur (« extension » → paramètres (la pièce crantée) → « Installer un module depuis un fichier »). Vous devriez toujours éviter de travailler sur votre navigateur du « quotidien », celui avec vos profils connectés. Il n’y a pas de « retour en arrière » si malencontreusement votre WebExtension est mal développée, et provoque une erreur ou une action non-voulue.

Vous pouvez aussi utiliser Web-ext (un module NodeJS), qui permet de recharger automatiquement la WebExt dans le navigateur, pratique pour une phrase de développement et de debugging. Web-ext utilise par ailleurs, au moins sous Linux, un profil vierge pour démarrer.

Avant d’aller plus en avant, notamment éditer le fichier background.js, nous devons faire un point sur les différents types et contextes dans lequel agissent habituellement les scripts JS. Loin des canons habituels du Web…

II-B-1 - Comprendre JavaScript dans un navigateur moderne : portée et contextes

Les WebExtensions sont une bonne introduction à la richesse – et son pendant, un relatif bazar – qu’entoure désormais le langage « du Web, pour le Web » à tous les niveaux. La banche de JS à la sauce NodeJS, et son célèbre moteur V8, est probablement l’un des aspects les plus connus. Pourtant l’évolution touche également les clients.

J’ai regroupé ici les principales familles de ce côté « client » et qui nous intéressent dans le cadre d’une utilisation sur le navigateur. À chaque fois c’est bien le même langage (une même version), le même environnement (le navigateur) mais pas le même contexte (et donc pas la même disponibilité d’API), portant aussi sur la portée de certaines variables (en écriture et en lecture).

Ce tableau qui tente une synthèse probablement très critiquable et partielle, s’appuie sur la documentation du MDN et est une construction personnelle. En jaune, vous trouverez les scripts « habituels », insérés dans une page et que nous avons l’habitude de voir dans les tutoriels et autres exemples. En gris, ceux sur lesquels s’arrêtera principalement cet article.

Portées

Cadres d’utilisation

Familles techniques (groupes de portées ou classes dérivées)

Sous-familles techniques
(classes instanciées)

Accès aux DOM / à certains objets

Portée principalement interne au navigateur (script à vocation asynchrone ; correspond à une notion de « services »)

Arrière-plan de WebExt

Script(s) d’une page fictive ou non ; chargés en permanence au démarrage du navigateur et défini par le manifest

– Pas de DOM véritable : les scripts peuvent être appelés indépendamment d’une page et celle-ci n’est de toute façon qu’une page « virtuelle »
– API complète des WebExtensions et un peu de celle des API Web

Arrière-plan de page

« Worker »
(en réalité l’interface qui sera dérivée d’AbstractWorker) : le chargement à la demande d’une page, peut être actif après la fermeture de celle-ci

ServiceWorker :
– Proxy / gestion d’un cache local
– Gestion des synchronisations avancées

– Pas d’accès direct à un DOM
– Est défini comme « Web »-Worker lorsque l’appel de construction est interne à une page donnée ou à un autre WebWorker, hors WebExtension
– API spécifiques aux Workers
– Peut instancier des WebWorkers ou des SharedWorkers

Portée principalement interne à une ou plusieurs pages

Avant-plan et arrière-plan d’une seule page

« Web » Worker :
– scripts JS « multi-threads » pour des calculs lourds et déportés. Cela permet d’utilisation d’un code bloquant (synchrone) sans bloquer une page

– Pas d’accès direct à un DOM
– Est défini comme « Web »-Worker lorsque l’appel de construction est interne à une page donnée ou à un autre WebWorker, hors WebExtension
– API spécifiques aux Workers
– Peut instancier des WebWorkers ou des SharedWorkers

Avant-plan et arrière-plan entre plusieurs pages d’un même domaine

SharedWorker :
– Synchronisation entre plusieurs pages (plusieurs onglets)
– Permet de superviser plusieurs contextes différents (compte tenu que chaque page / onglet a son propre contexte)

– Pas d’accès direct à un DOM
– Même origine : même hôte, même port, même protocole

Script « de page », que l’on qualifiera d’avant-plan de page

– Accès direct au DOM
– Pas d’accès aux APIs WebExtension, mais peut instancier des WebWorkers et des SharedWorkers
– Accès à l’API Web

Portée mixte

– Accès direct à un DOM
– Peut être insérée dans toutes les pages ou iFrames en fonction des autorisations données à la WebExtension parente
– Accès partiel à l’API Web
– Accès partiel aux Workers
– Accès partiel à l’API de la WebExtension

– Accès direct à un DOM
– Peut être insérée dans toutes les pages ou iFrames en fonction des autorisations données à la WebExtension parente
– Accès partiel à l’API Web
– Accès partiel aux Workers
– Accès partiel à l’API de la WebExtension

Ainsi nous pouvons tirer de ce tableau et de la documentation, quelques points-clé pour bien comprendre quoi utiliser et quand en trois grands points (donc résumé arbitraire) :

- la portée du script, c’est-à-dire sur quel aspect d’une ressource (page, DOM, bases de données, etc) ou d’une interface (Workers, etc) il agira. Par exemple pour un script de contenu :

- a-t-il un accès direct ou indirect à une ressource → accès direct à l’espace de stockage de la WebExtension et du DOM de la page où il s’exécute ;

- partage-t-il la possibilité directe d’une lecture et/ou écriture de variable → si un script de contenu a accès au DOM, il ne verra pas et ne peut modifier les variables associées à l’objet Document ou Window des scripts de page ;

- sa nature, c’est-à-dire s’il est à vocation strictement synchrone, mixte ou strictement asynchrone :

- un code bloquant dans une vocation strictement synchrone bloquera tout le script → ex. le fonctionnement originel de JS sur les navigateurs,

- un code bloquant dans une vocation mixte, peut bloquer tout ou partie d’un script ou l’utilisation d’une ressource → ex. actuellement sur un script de page qui utilise à la fois du code synchrone et de la gestion d’événements, ainsi que des appels à des Workers,

- un code bloquant dans une vocation strictement asynchrone pourra retourner une erreur et celle-ci peut-être silencieuse → ex. script d’arrière-plan d’une WebExtension (l’erreur apparaît dans les logs seulement du navigateur) ou script de ServiceWorker gérant un cache (l’erreur apparaît dans les log de la page où il est lancé) ;

- son intérêt ; c’est-à-dire à quoi il est traditionnellement assigné, comme je l’indique plus haut. Son intérêt est souvent lié au moment du déclenchement dans l’ensemble des événements possibles sur un navigateur (qui ne se limite pas, encore une fois, à quelques pages… !).

II-B-2 - Phase 0 - D’où l’on vient ; où l’on va

Le choix d’un site pour cette première partie, n’a pas été anodin. Il s’agit d’un site commercial, avec énormément de scripts de pages internes et externes, notamment publicitaires, de nombreuses ressources mais un code qui reste relativement clair et un contenu structuré (pouvant être tabularisé pour l’exercice). Surtout, la mise à jour intervient à un rythme régulier (une fois par jour), qui nous permet de faire des tests sur un temps long et pouvant facilement être appréhendé. Enfin,un point & non des moindres : vous pouvez tester indifféremment par HTTP et HTTPS sur cette page, qui retourne la même chose (et ainsi constater qu’effectivement, vous n’agissez pas ici sur la connexion mais bien sur la requête qui transite sur la connexion).

Voici l’URL sur laquelle travailler (les autres pages ne nous intéresseront pas ici) : http://www.programme-tv.net/programme/programme-tnt.html

Voici que vous pourriez voir sans transformation de la page :

Voici ce que vous devriez voir après transformation :

Notez qu’aucun script de la page initiale (celle renvoyée par le serveur, avec les scripts publicitaires) n’aura été finalement chargé après la transformation. Pour arriver à ce résultat, nous aurons les étapes suivantes :

1. avant la requête…

1. ajouter une fonction aux écouteurs d’événement,

2. procéder à un filtrage par motif (pattern matching) ainsi que par type (ici les codes sources des pages dites main_frame, jamais des frames incluses dans une page) ;

2. au niveau de la requête en elle-même…

1. récupérer le corps de la réponse, pour en produire du « texte », qui sera échappé et introduit dans une page HTML type ;

2. renvoyer cette page type comme réponse à interpréter dans le bon format ;

3. au niveau de la page (du code source) renvoyée…

1. une fois cette page type avec le contenu original de la requête chargée dans l’onglet, procéder à l’analyse (DOMParse) et aux transformations XSL,

2. réinjecter dans la source HTML transformé, son objet DOM, dans la page qui a servi de support.

Pourquoi traiter les transformations XSL au sein d’un onglet et non directement lors de la réception du contenu original ? Plusieurs raisons m’ont poussé à ce choix :

- ne pas perdre du temps : lorsque l’utilisateur demande une page, lorsqu’il demande une URL ou parce qu’elle est chargée à l’ouverture d’un onglet, il souhaite connaître l’état d’avancement. Renvoyer une page type, qui indique tout de suite l’interception, l’état actuel de chargement, la possibilité d’annuler la transformation et l’avancement de la transformation, puis se chargera d’être le « support », permet à la fois une information complète et une meilleure maîtrise ;

- circonscrire les erreurs : si votre script rencontre une erreur sur la transformation d’une page, mieux vaut qu’un plantage ou un ralentissement soit au niveau de l’onglet qu’au niveau de la WebExt ;

- fractionner le développement : la récupération est un processus différent de la transformation, géré par un autre script. Une évolution de l’un ne touche pas l’autre et pour la relecture par un tiers, en plus des commentaires, les scripts ainsi « découpés » sont moins indigestes.

Pour les transformations, nous utiliserons les seules fonctions natives JS du navigateur, en passant d’un code source HTML à un objet Document puis la transformation de ce dernier avec un contenu XSL parsé.

II-B-3 - Phase 1 - Détecter les requêtes intéressantes

Dans notre phase précédente, nous avons installé une WebExt vide, sans aucune permission, ainsi qu’un fichier de script d’arrière-plan. Pour détecter des requêtes intéressantes, il ne s’agit pas juste de « lire » l’URL et de la comparer à un motif : encore faut-il, une fois détectée, pouvoir extraire de la ressource qui y sera associée (ici du contenu HTML), ce qui nous intéresse. Et mieux : modifier cette ressource.

Il nous faudra aussi stocker notre transformation XSL, qui sera appliquée… Ici nous n’en stockerons qu’une, pour l’exemple, qui sera éditable dans la seconde partie. Pour une seule transformation, ouvrir un espace de stockage, au détriment d’une simple variable, n’est pas utile. C’est pourquoi dans la seconde partie, nous pourrons « rentabiliser » cet espace ouvert en pouvant créer de nouvelles transformations. J’ai également utilisé l’espace de stockage pour une page type, j’en détaillerai plus loin la raison.

Mettons donc tout d’abord les permissions nécessaires à notre script. N’oubliez pas qu’à chaque étape, si vous n’utilisez pas l’outil web-ext mentionné plus haut, vous devrez recharger manuellement votre WebExt afin que les modifications soient prises en compte.

La partie permissions du fichier manifest.json, doit désormais ressembler à cela :

1. {

2. "permissions": [

3. "unlimitedStorage",

4. "storage",

5. "webRequest",

6. "webRequestBlocking",

7. "<all_urls>"

8. ]

9. }

Les deux premiers items vous permettent d’accéder à une ressource de stockage pour la WebExt – c’est-à-dire indépendamment des autres espaces de stockage notamment ceux liés aux domaines visités. En réalité, Firefox considère l’ID de l’extension comme un domaine à part entière, et repose donc sa stratégie de stockage pour la WebExt comme il le ferait pour n’importe quel site Internet. L’item unlimitedStorage n’a pas d’utilité dans notre exemple ; je le donne à titre d’information. Il ne s’applique pas d’ailleurs au stockage sync mais seulement aux stockages locaux – c sync étant celui qui est synchronisé entre les différentes instances connectées à un même compte (ex. Firefox Sync ou votre compte Google pour Google Chrome) et dont les règles de quota sont gérés par ces mêmes services.

→ Bon à savoir : Firefox, comme d’autres navigateurs, ne garantit pas la préservation « impérative » des données – c-à-d que vous pourriez avoir des données effacées sans que la navigateur ne le demande à la WebExtension voire ni même ne l’alerte. Un critère temporel (l‘accès le plus vieux à une base) est utilisé, puis un effacement dans la base la plus ancienne sans critère précis indiqué (aléatoire?).
Si pour un poste fixe récent, la quantité de mémoire « en dur » est bien plus que suffisante en dehors de certains usages (jeux, calculs scientifiques, etc) ; ce qui n’est pas le cas pour les périphériques mobiles. Un paragraphe sera dédié au cas des WebExtensions sur périphérique mobile dans la conclusion de l’article.
Attention donc, à ne pas considérer cette base comme une base de données « définitive » comme peut l’être par exemple MySQL, qui arrêtera simplement l’enregistrement de nouvelles données. Vous devez toujours considérer qu’il s’agit ici d’un SGBD à vocation « temporaire » ou au moins d’un système « incertain », quand bien même le cas n’aurait pas de raison de présenter, par le simple fait qu’il est possible.

Les autres items sont eux plus intéressants et permettent le suivi de toutes les requêtes (quel que soit le format de celles-ci, j’y reviendrai), ainsi que leur manipulation comme peut être traduit webRequestBlocking. L’item <all_urls> est en quelque sorte « un opérateur », que l’on peut retrouver dans d’autres parties des API WebExtension et dont l’usage implique une absence de restriction sur les URL pouvant être ainsi « gérées », quel que soit le protocole (HTTP ou HTTPS). Attention, le domaine du magasin d’applications pour Firefox ou pour Chrome, est naturellement inviolable par les WebExtension, afin d’éviter toute manipulation, ainsi que certains protocoles liés au fonctionnement du navigateur (une WebExt ne peut pas, par exemple, agir sur ses propres requêtes – ou globalement lorsqu’il s’agit du protocole moz-extension://).

Au sein du fichier background.js, des ajouts sont à prévoir. Je vais tenter de résumer schématiquement comme l’utiliser pour le mieux :

- en qualité de script d’arrière-plan lorsque le navigateur démarre avec une WebExt déjà installée, il est déclenché au moment du lancement du navigateur. En réalité ce dernier est déjà lancé et votre script intervient entre la fin du chargement des principaux organes du navigateur, et le démarrage du lancement du ou des premiers onglets affichés à l’utilisateur (le navigateur étant ici un « client » des données). C’est important de se rappeler cela : vous ne pourrez jamais être avant ce moment du chargement, par ce qui est offert dans un script d’arrière-plan. Dans certains cas, notamment lorsque des WebExt agissent sur une même fonction, vous ne pouvez donc pas être certains que ce qui arrive à votre WebExt n’a pas déjà été modifié ou stoppé par une autre(1) ;

- contrairement à d’autres scripts qui doivent être « véritablement » asynchrones (notamment les scripts de ServiceWorker à vocation de gestion d’un cache local), les scripts d’arrière-plan sont tout à fait synchrones (donc pouvant être bloqués par une opération) et sont surtout utiles pour formaliser (au sens d’une déclaration de fonction) et enregistrer les interceptions d’événements.

Nous allons d’abord déclarer une fonction – listener – qui servira à traiter les requêtes intéressantes. Non pas à les filtrer, car cela n’est rendu possible que par l’enregistrement d’une écoute d’événement qui réagit aux requêtes correspondant à un motif (pattern matching) que nous verrons plus loin :

1. browser.SurveilleURLs = [

2. "*://www.programme-tv.net/programme/programme-tnt.html"

3. ];  // → l'objet 'browser' est l’équivalent de 'window' pour un script de page, et tous les appels de toutes les API des WebExt passent par cet objet

5. function listener(details) {

6. let filter = browser.webRequest.filterResponseData(

7. details.requestId

8. );

9. let tabBuffer = [];  // → on reçoit du binaire ! Pas du texte

10. filter.ondata = event => {

11. tabBuffer.push(event.data); // → à chaque réception d’une portion de la réponse, on ajoute cette portion de code binaire à notre tableau qui sert de 'buffer'

12. }

13. filter.onstop = event => { // → fin de la réception des portions

14. var source = new Blob(tabBuffer, {type : 'text/html'});

15. console.log(source);

16. filter.write(source); // → on renvoie directement la source sans modification pour la poursuite

17. filter.disconnect();  // → on arrête l’écoute

18. }

19. }

Dans la console du navigateur et pas la console d’une page ouverte (« Débogger des modules » dans le menu général des WebExt), vous verrez le contenu de la source ainsi interceptée.

Cela devrait ressembler à cette ligne :

Blob { size: 276530, type: "text/html" }

Nous positionnons auparavant cette fonction dans un écouteur d’événement, grâce à l’API offerte par webRequestBlocking :

1. browser.webRequest.onBeforeRequest.addListener(

2. listener,

3. {

4. urls: browser.SurveilleURLs, // → mettre '<all_urls>' pour intercepter toutes les pages sources disponibles

5. types: [

6. "main_frame" // → seulement les pages « sources » (pas les iframes, les images, scripts, CSS, etc). De toute façon leur chargement intervient sur instruction de la source : si celle-ci est modifié, leur chargement n’existera pas

7. ]

8. },

9. [

10. "blocking"

11. ]

12. );

J’ai choisi d’indiquer une interception avant la requête elle-même – onBeforeRequest –, afin de nous assurer que cette requête, si elle aboutit (même en cas de retour 404 ou 500), soit correctement « écoutée » et modifiée dans notre cas.

→ Bon à savoir : il est souvent noté qu’un écouteur d’événement est une fonctionnalité du DOM pour les pages. C’est incomplet : le Document Object Model regroupe l’ensemble des événements qui sont liés aux contenus et à leurs traitements au sein d’un logiciel (ici le navigateur). C’est en quelque sorte la même « recette » qui est appliquée pour chacun des « événements » qui sont suivis (c-à-d schématiquement un changement d’état d’une fonction ou d’une variable), permettant d’unifier les écouteurs et les interceptions.
Ainsi un DOM pour HTML est voisin d’un DOM pour des événement HTTP ou lié au fonctionnement du navigateur (ouverture d’un onglet, d’un menu, etc).

II-B-4 - Phase 2 – Comprendre ce que l’on reçoit et ce que l’on émet

Si votre WebExt est rechargée et que vous chargez la page surveillée, vous verrez désormais… une magnifique page vierge (et une page normale partout ailleurs, sauf dans le cas <all_urls> évidemment). Incompréhensible : n’a-t-on pas fourni le contenu avant de désactiver notre interception ?

Le problème c’est que ce que vous renvoyez ne correspond pas à ce que Firefox attend en terme de format (pas d’encodage, je parle bien de format). Il ne peut pas du binaire avec un flag (un drapeau) text/HTML, mais l’équivalent d’une chaîne de caractère dans le langage JS.

1. function listener(details) {

2. let filter = browser.webRequest.filterResponseData(

3. details.requestId

4. );

5. let tabBuffer = [];  // → on reçoit du binaire ! Pas du texte au sens JS

6. let encoder = new TextEncoder(); // → on va vraiment "comprendre" le binaire comme du texte

7. filter.ondata = event => {

8. tabBuffer.push(event.data);

9. }

10. filter.onstop = event => {

11. var source = new Blob(tabBuffer, {type : 'text/html'});

12. var reader = new FileReader();

13. reader.addEventListener("loadend", function(resultat) {

14. console.log(resultat.target.result); // ici le résultat comme du texte "source" HTML classique

15. filter.write(encoder.encode(resultat.target.result));

16. filter.disconnect();

18. });

19. reader.readAsText(source);

20. }

21. }

Nous avons cette fois-ci la page qui est affiché à l’utilisateur et dans la console du navigateur, le texte « source » HTML.

Je vais également ajouter les lignes de code pour définir les objets dans l’espace de stockage Storage, que nous utiliserons après.

II-B-4-a - Code source actuel du fichier background.js

Pour cette première partie, voilà à quoi doit ressembler votre fichier. N’oubliez pas qu’ici, à chaque chargement de votre WebExt (à l’installation et au redémarrage du navigateur si l’installation n’est pas temporaire), ce script sera lancé. Il écrasera donc la transformation et la page par défaut (vous devez rajouter une condition de non-existence si vous souhaitez empêcher cela).

J’ai ajouté également un appel à l’espace de stockage pour charger une page « support » par défaut (cf browser.storage.local.get("page:defaut").then). Cela n’est pas pertinent s’il n’y a qu’une page « support » type, qui peut être utilement et seulement stockée dans une variable. Cependant si l’envie vous prend de compléter cette extension et de multiplier les pages supports, les transformations et les règles associées, probablement aurez-vous besoin de faire appel à un espace de stockage (car le déchargement du script annulerait les modifications).

Prenez donc ce code comme une « indication » d’une manière de faire.

1. self.transformationDefaut = `<?xml version="1.0"?>

2. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

3. <xsl:output method="html" doctype-system="about:legacy-compat" encoding="UTF-8" omit-xml-declaration="yes" indent="yes" />

4. <xsl:template name="colonne_NomChaine">

5. <td>

6. <xsl:value-of select=".//a[contains(@class, 'channel_label')]/@title" />

7. </td>

8. </xsl:template>

9. <xsl:template name="colonne_NomProgramme">

10. <td>

11. <xsl:value-of select=".//a[contains(@class, 'prog_name')]/@title" /><br />

12. <xsl:value-of select=".//span[contains(@class, 'prog_type')]/text()" />

13. </td>

14. </xsl:template>

15. <xsl:template name="ligne_Chaine">

16. <tr>

17. <xsl:call-template name="colonne_NomChaine"></xsl:call-template>

18. <xsl:for-each select=".//div[contains(@class, 'programme')]">

19. <xsl:call-template name="colonne_NomProgramme"></xsl:call-template>

20. </xsl:for-each>

21. </tr>

22. </xsl:template>

23. <xsl:template match="/">

24. <html>

25. <head>

26. <title>

27. <xsl:value-of select="/html/head/title"/>

28. </title>

29. </head>

30. <body>

31. <table>

32. <thead>

33. <tr>

34. <td colspan="3">Chaînes</td>

35. </tr>

36. </thead>

37. <tbody>

38. <xsl:for-each select="//div[contains(@class, 'p-v-md')]">

39. <xsl:call-template name="ligne_Chaine"></xsl:call-template>

40. </xsl:for-each>

41. </tbody>

42. </table>

43. </body>

44. </html>

45. </xsl:template>

46. </xsl:stylesheet>`;

48. browser.storage.local.set(

49. {

50. "transformation:defaut": self.transformationDefaut

51. }

52. );

54. self.pageDefaut = `<!DOCTYPE html>

55. <html lang="fr">

56. <head>

57. <meta charset="utf-8">

58. <title>ControlePage</title>

59. </head>

60. <body>

61. <code id="ControlePage"></code>

62. <header>

63. <h1>WebExtension 'ControlePage'</h1>

64. <p class="info">

65. Cette page a été interceptée

66. car elle répond à un schéma défini

67. dans le base de données de la WebExtension.

68. </p>

69. </header>

70. %page%

71. <footer>

72. <p>

73. Etat de la page :

74. <span id="etat">

75. en attente d'être traitée...

76. </span>

77. </p>

78. </footer>

79. </body>

80. </html>`;

82. browser.storage.local.set(

83. {

84. "page:defaut": self.pageDefaut

85. }

86. );

88. /*---*/

90. browser.SurveilleURLs = [

91. "*://www.programme-tv.net/programme/programme-tnt.html",

92. ];

94. browser.TransformationsURLs = [

95. {

96. "url": "http(s)?://www.programme-tv.net/programme/programme-tnt.html",

97. "transformationId" : "defaut"

98. }

99. ];

101. browser.storage.local.set(

102. {

103. "transformations:urls": browser.TransformationsURLs

104. }

105. );

107. /*---*/

109. function listener(details) {

110. let filter = browser.webRequest.filterResponseData(

111. details.requestId

112. );

113. let encoder = new TextEncoder();

114. let tabBuffer = [];

115. filter.ondata = event => {

116. tabBuffer.push(event.data);

117. }

118. filter.onstop = event => {

119. var source = new Blob(tabBuffer, {type : 'text/html'});

120. var reader = new FileReader();

121. reader.addEventListener("loadend", function(resultat) {

122. browser.storage.local.get("page:defaut").then(

123. (objRStockage) => {

124. if (Object.keys(arg1).length<1) {

125. var page = self.pageDefaut;

126. var texte = "<div id=\"erreur\"><p>Oups, la web extension est en panne !</p></div>";

127. } else {

128. var page = objRStockage["page:defaut"];

129. var texte = "<div id=\"chargement\"></div><template id=\"pageSource\">"+encodeURIComponent(

130. resultat.target.result

131. )+"</template>"

132. }

133. texte = page.replace(

134. "%page%",

135. texte

136. );

137. filter.write(encoder.encode(texte));

138. filter.disconnect();

139. }

140. );

141. });

142. reader.readAsText(source);

143. }

144. }

146. browser.webRequest.onBeforeRequest.addListener(

147. listener,

148. {

149. urls: browser.SurveilleURLs,

150. types: [

151. "main_frame"

152. ]

153. },

154. [

155. "blocking"

156. ]

157. );

II-B-5 - Phase 3 – Appliquer la transformation XSL

En ouvrant la console de la page, notez que j’ai utilisé la balise template, dont j’évoquerai les qualités et usages plus loin dans l’article, à dessein. Elle contient la source HTML originale de la requête, celle reçue du serveur, en version textuelle échappée. Cette source originale est donc inerte et n’est pas traitée par le navigateur : l’onglet Réseau de la console, vous indique ainsi qu’aucun autre chargement de ressource ne se produit, ce qui est logique.

Ce qui est moins logique par contre, c’est lorsque vous regardez dans ce même onglet Réseau, la réponse de la requête… Vous trouvez la source originale et non notre page type ! Idem si vous ajouter view-source : à votre URL, indiquant que vous souhaitez voir seulement le contenu HTML. Seul l’Inspecteur, onglet qui permet de parcourir le document HTML en temps réel, vous donne la page type. Pourquoi un tel comportement ? D’abord une raison pratique : comme je vous l’indiquais, votre WebExt ne dispose que d’une ressource qui lui est attribué par le navigateur, à un moment donné. Le navigateur, lui, peut disposer de toutes les versions et donner à chaque composant une version ou une autre. Vous ne disposez pas « totalement » de la capacité à modifier un contenu « original », seulement dans un contexte un contenu « dupliqué »…

C’est ensuite une raison de sécurité : l’utilisateur a un outil fiable, la console de la page, disponible pour chaque onglet, qui lui permet de voir si le traitement d’une page, sa source, est celle reçue du réseau ou a été modifiée. Évidemment sur le cas d’un site complexe comme le webmail de Google ou globalement des interfaces utilisant React ou équivalent, une telle vérification n’a pas grand sens vu (1) la complexité du code, (2) une génération de chaque composant en temps réel.

Bien : votre page type s’affiche mais toujours pas la transformation du contenu désiré. Nous allons devoir « injecter » du code Javascript quelque part. Et tant qu’à faire, « proprement », sans utiliser le code source. Avec une WebExt, c’est possible et c’est facile, toujours grâce aux motifs et aux écouteurs d’événements.

Voici un exemple de ce qui se passera : le navigateur ira chercher la page d’un célèbre site d’annonces en ligne (nous voyons dans la console de l’onglet que c’est bien le cas), et notre WebExtension ne lui retournera que ce qu’il lui chante :

Allons-y ! Dans notre fichier background.js, insérez le code suivant :

1. browser.contentScripts.register({

2. "js": [{file: "/surveille.js"}],

3. "css": [{file: "/commun.css"}],

4. "matches": ["<all_urls>"], // ou browser.SurveilleURLs pour faire du pattern matching

5. "runAt": "document_idle"

6. });

Vous avez déjà deviné l’intérêt : créer le fichier à la racine de la WebExt, le fichier surveille.js et commun.css.

Dans ce dernier, modifiez le contenu avec le CSS suivant :

1. body {

2. background: rgba(0,0,255,0.15);

3. }

5. table * {

6. text-align: center;

7. }

Dans le premier, ajouter le code JS suivant :

console.log("ControlePage :: nouvelle page suivant le résultat du 'pattern matching'");

… puis regarder ce qui se passe pour chaque page, notamment sur Google où le fond de la page n’est plus blanc ! Avec le message dans la console de l’onglet.

Nous avons bien une insertion après le chargement du DOM et de l’ensemble des ressources, de nos deux fichiers de WebExt. Logique ! l’attribut runAt dans l’objet passé en argument de l’écouteur, définit le moment du chargement des ressources. « Notre » CSS prend donc le pas sur le reste car c’est le dernier a être chargé (écrasement des attributs). Nous verrons dans la seconde partie de cet article, les pièges qui existent avec cette méthode et dans quel cas ne pas l’utiliser.

Vous pouvez repasser ["<all_urls>"] à la la variable browser.SurveilleURLs, ce qui restreindra cette injection à nos URL définies. Modifiez le script surveille.js avec le contenu suivant :

1. console.log("ControlePage :: nouvelle page suivant le résultat du 'pattern matching'");

3. function Transformer(SourceTexteXSLT, SourceTexteHTML) {

4. var parseurXSLT = new DOMParser();

5. var docXSLT = parseurXSLT.parseFromString(

6. SourceTexteXSLT,

7. "text/xml"

8. );

9. var parseurHTML = new DOMParser();

10. var docHTML = parseurHTML.parseFromString(

11. SourceTexteHTML,

12. "text/html"

13. );

14. var processeurXSLT = new XSLTProcessor();

15. processeurXSLT.importStylesheet(

16. docXSLT

17. );

18. return processeurXSLT.transformToFragment(

19. docHTML,

20. document

21. );

22. }

24. browser.storage.local.get(

25. "transformations:urls"

26. ).then(

27. (objResultTous) => {

28. var regExt =  new RegExp("");

29. var docURL = ""+window.location.href;

30. try {

31. objResultTous["transformations:urls"].forEach(

32. (objResultUn) => {

33. regExt.compile(

34. objResultUn["url"]

35. );

36. if (regExt.test(docURL)) {

37. browser.storage.local.get(

38. "transformation:"+objResultUn["transformationId"]

39. ).then(

40. (retour) => {

41. var sourceTexteBrut = document.querySelector("template#pageSource").innerHTML;

42. var sourceTexte = decodeURIComponent(

43. sourceTexteBrut

44. );

45. var docXHTML = Transformer(

46. retour["transformation:defaut"],

47. sourceTexte

48. );

49. [

50. "head",

51. "body"

52. ].forEach(

53. (element) => {

54. var elNouveau = docXHTML.querySelector(

55. element

56. );

57. var elOrigine = document.querySelector(

58. element

59. );

60. elOrigine.parentElement.replaceChild(

61. elNouveau,

62. elOrigine

63. );

64. }

65. );

66. var elTemplate = document.createElement("template");

67. elTemplate.textContent = sourceTexteBrut;

68. elTemplate.setAttribute("id", "pageSource");

69. document.querySelector("body").appendChild(

70. elTemplate

71. );

72. console.log("ControlePage :: fin de la génération de la page suivant la transformation donnée");

73. }

74. );

75. throw BreakException;

76. }

77. }

78. );

79. } catch (erreur) {

80. if (erreur !== BreakException)

81. throw erreur;

82. }

83. }

84. );

Ça y est : votre page apparaît, belle et bien transformée par XSL. En modifiant la source de la transformation, vous agirez sur la présentation. Avec CSS et Javascript, une fois injectés et par la suite avec des appels supplémentaires, vous pouvez également maîtriser plus en profondeur ces transformations. Vous pouvez également récupérer des ressources initialement demandées par la page d’origine : tout est possible.

Avec, pourquoi pas, la possibilité de vous faire un moteur des informations collectées, une sorte de moteur de recherche avancée, dans le contenu d’un historique qui ne se limite pas à la paire URL / titre de la page !

II-B-5-a - Et sans XSL malgré tout ?

C’est toujours possible mais, comme nous le verrons dans cet article, l’évaluation d’expression n’est pas possible au sein d’une WebExtension ce qui en limite considérablement l’usage (et même si c’est – presque – possible par le biais d’un hack sur la page, c’est de toute façon totalement décommandé).

Il faudra alors avoir recours à du JS codé « en dur » dans le script qui, s’il n’est pas compliqué, n’offre que peu le loisir d’être modifié par l’utilisateur :

1. var sourceTexte = decodeURIComponent(

2. document.querySelector("template#pageSource").innerHTML

3. );

5. var parser = new DOMParser();

6. var doc = parser.parseFromString(

7. sourceTexte,

8. "text/html"

9. );

11. var blocChaines = ".p-v-md";

12. var blocProgrammes = ".programme";

14. var chainesListe = doc.querySelectorAll(

15. blocChaines

16. );

17. var chaines = [];

18. chainesListe.forEach(

19. (blocChaine, i, tab) => {

20. var chaine = blocChaine.querySelector(

21. "a.channel_label"

22. ).getAttribute(

23. "title"

24. ).replace(

25. "Programme de ",

26. ""

27. );

28. var programmes = [];

29. var blocImages = blocChaine.querySelectorAll(

30. ".programme"

31. ).forEach(

32. (blockProgramme) => {

33. var nom = blockProgramme.querySelector(

34. "a.prog_name"

35. ).getAttribute(

36. "title"

37. );

38. var type = blockProgramme.querySelector(

39. ".prog_type"

40. ).textContent;

41. programmes.push(

42. {

43. "nom": nom,

44. "type" : type

45. }

46. );

47. }

48. );

49. chaines.push(

50. {

51. "nom": chaine,

52. "programmes": programmes

53. }

54. );

55. }

56. );

58. var tableau = document.createElement("table");

59. tableau.setAttribute(

60. "border",

61. 1

62. );

63. var tableau_entete = document.createElement("thead");

64. var tableau_entete_ligne = document.createElement("tr");

65. var tableau_entete_colonne1 = document.createElement("td");

66. tableau_entete_colonne1.textContent = "Chaînes disponibles";

67. tableau_entete_colonne1.setAttribute(

68. "colspan",

69. 3

70. );

71. tableau_entete_ligne.appendChild(

72. tableau_entete_colonne1

73. );

74. tableau_entete.appendChild(

75. tableau_entete_ligne

76. );

77. var tableau_corps = document.createElement("tbody");

78. chaines.forEach(

79. (chaine) => {

80. var tableau_corps_ligne = document.createElement("tr");

81. var tableau_corps_colonne1 = document.createElement("td");

82. tableau_corps_colonne1.textContent = chaine["nom"];

83. tableau_corps_ligne.appendChild(

84. tableau_corps_colonne1

85. );

86. chaine["programmes"].forEach(

87. (programme) => {

88. var tableau_corps_colonne = document.createElement("td");

89. var tableau_corps_nom = document.createElement("div");

90. tableau_corps_nom.textContent = programme["nom"];

91. tableau_corps_colonne.appendChild(

92. tableau_corps_nom

93. );

94. var tableau_corps_type = document.createElement("div");

95. tableau_corps_type.textContent = programme["type"];

96. tableau_corps_colonne.appendChild(

97. tableau_corps_type

98. );

99. tableau_corps_ligne.appendChild(

100. tableau_corps_colonne

101. );

102. }

103. );

104. tableau_corps.appendChild(

105. tableau_corps_ligne

106. );

107. }

108. );

111. tableau.appendChild(tableau_entete);

112. tableau.appendChild(tableau_corps);

113. document.body.appendChild(tableau); // → même résultat qu’avec XSL!

Le résultat sera conforme à ce que nous attendons. Notez qu’il y a une subtile nuance pour les noms de chaînes, qui sont le contenu exact des titres, avec le préalable systématique « Programme de… » :

II-C - Quelques réflexions supplémentaires

Dans la page renvoyée pour traitement dans l’onglet, pourquoi garder la source de la page « originale » c’est-à-dire le contenu HTML exact téléchargé du serveur ? Tout d’abord, et pour une « mauvaise raison », afin de « basculer » de la page reconstruite à celle d’origine sans « recharger » – c-à-d redemander la source au serveur.

Une limitation cependant : les événements du navigateur, liés à la création même de l’onglet du document, qui lui n’est que modifié et non recréé, peuvent générer des erreurs : les écouteurs d’événement peuvent ne pas être déclenchés, ce qui peut rendre les scripts de la page inopérants.

Une autre raison, plus légitime et surtout moins problématique, est de se cantonner à l’exploitation ultérieure : imaginer par exemple modifier en direct la source de transformations XSL, et obtenir des résultats (ou simplement parcourir le contenu grâce à XPath). Sans avoir la source originale, des informations peuvent être perdues et, plus grave, la structure initiale (son arborescence) de la page source est perdue.

La recréation d’un élément dédié dans la page générée n’apporte donc rien dans notre exemple, mais se révèle utile pour exploiter pleinement les conditions offertes par les WebExtensions.

II-D - Créer une page d’édition des transformations XSL

Je donne en fin de cette partie, le code source en deux parties d’une page d’édition des transformations XSL stockées. Je parle bien ici seulement des transformations, et pas d’une page pour éditer également les règles (cf le pattern matching).

La page en tant que telle n’offre pas beaucoup d’intérêt, sinon permettre d’éditer les « fichiers » (sources, en réalité des simples chaînes de caractères) XSL qui servent aux transformations du HTML pour certaines URL interceptées.

Elle ne nous servira cependant de support qu’en illustration de deux points :

- Un cas de script de page qui se comporte comme un script de contenu (quel contexte, quel intérêt et pourquoi un tel comportement?) ;

- L’utilisation de la balise template, une nouveauté fort utile pour la mise au gabarit issue de HTML5, que je citais plus haut.

II-D-1 - La gabarisation en HTML5

Avant de rentrer dans notre sujet, comprenons notre contexte en détail. Dans un script de contenu ou d’arrière-plan (ou par la console du débug des WebExt, pas par la console d’une page!), testez le code suivant :

1. Function("console.log('ok');");

… la réaction ne se fait pas attendre et l’évaluation de texte composant le corps de la fonction n’est pas exécutée. Ainsi Function est un « équivalent » (à gros traits) de la fonction eval – à cette différence que la première est une fonction qui en retourne une « prête à l’emploi » et non une évaluation destinée à une exécution immédiate. Vous devriez avoir le message suivant dans une console liée à la WebExt :

1. >> Function("console.log('ok');");

2. Error: call to Function() blocked by CSP debugger eval code:1:1

3. Content Security Policy: Les paramètres de la page ont empêché le chargement d’une ressource à self (« script-src »). Source: call to eval() or related function blocked by CSP. debugger eval code:1:1

Dans un contexte de script de page, ou de la console d’une page « classique », le résultat est bien une fonction anonyme :

1. >> Function("console.log('ok');");

2. anonymous()

3. arguments: null

4. caller: null

5. length: 0

6. name: "anonymous"

7. prototype: Object { … }

8. <prototype>: function ()

Vous pouvez donc « exécuter » cette fonction anonyme. Je vous invite à lire la documentation afférente (ainsi qu’un chapitre en sus ici) et à tester en dehors de ce tutoriel ces notions avant d’aller plus loin.

Ce que vous devez garder à l’esprit, c’est qu’une des limitations – sinon la limitation majeure –, des WebExt est l’évaluation d’expressions. Elle est strictement interdite en tout lieu(2). Ce comportement est une CSP – Content Security Policy (ici géré par le navigateur lui-même) – dont hérite par exemple le refus d’une requête cross-domain.

Pour faire simple : les règles entre page Web et WebExt sont des inverses sur ces sujets. Le cross-domain est permis (avec les droits associés obtenus), mais l’évaluation ne l’est pas. La raison est évidente : éviter qu’un assaillant puisse exécuter en contexte d’arrière-plan ou de script de contenu, du code arbitraire. Y compris si ce code est une chaîne « en dur » dans votre script initial.

Or certains outils de mise au gabarit, dont la qualité est inégale par ailleurs, souvent lourds pour peu d’opérations, peuvent être amenés à utiliser l’évaluation d’expressions de manière légitime. Aussi je vous déconseille fortement l’utilisation des bibliothèques « toutes prêtes » qui ne sont pas spécifiquement conçues pour les WebExt. D’abord leur récupération par le navigateur imposerait d’exposer la WebExt à du code extérieur (avec des complications pour que leur contenu soit exécuté si cela passe par une requête!), ce qui est déjà en soi un risque, mais de plus les bibliothèques peuvent provoquer des erreurs dues à la configuration particulière de leur environnement.

Le mieux si vous souhaitez en utiliser, est leur intégration dans le projet en tant que tel (donc « figer » la version utilisée) et en prenant un soin minutieux à connaître leur fonctionnement interne.

La solution la plus simple est de passer par ce qui est offert par le navigateur et l’évolution de HTML. La version 5 prévoit ainsi une balise template qui agit d’une manière similaire à ce que serait un DocumentFragment. La balise, qui est insérée dans dans le code source HTML, fait partie du DOM mais n’est pas prise en compte pour l’affichage par le navigateur : il n’y a donc aucun rendu (aucune règle CSS ne devant lui-être directement associée). Elle est appelable par les fonctions de sélection CSS cependant, ce qui lui permet d’être utilisé indépendamment par .getElementByTag(Class)Name ou par .querySelector.

Son appel par JS retourne une entité HTML un peu particulière (comme la balise form retourne par exemple une liste accessible d’entrées, pouvant se comporter comme un tableau). Ici le contenu du gabarit est accessible par l’attribut .content de l’objet template. Ce contenu peut être modifié et/ou cloné à volonté, par exemple pour reproduire un même formulaire, un tableau, etc – ou même un fil d’actualité.

L’usage, comme vous pourrez en avoir un aperçu dans la source que je donne pour l’éditeur, est très souple en plus de correspondre strictement aux spécifications du W3C.

Car, dernier point à garder à l’esprit, certaines pratiques – si elles sont toujours possibles – sont déconseillées pour les WebExt. Ainsi l’évaluation de HTML est possible quand l’évaluation JS ne l’est pas, mais la validation et la publication sur une plateforme – notamment celle de Mozilla – provoquera une alerte. A terme, cela pourrait être rédhibitoire :

(Extrait) Warning: If your project is one that will undergo any form of security review, using innerHTML most likely will result in your code being rejected. For example, if you use innerHTML in a browser extension and submit the extension to addons.mozilla.org, it will not pass the automated review process.

Ainsi voici ce qui ne faut pas faire :

var p = document.createElement("p");
p.innerHTML = "<i>coucou</i>";

Voici ce qui est souhaitable :

→ soit prendre un gabarit template dans votre code source avec querySelector, et utiliser .textContent pour ajouter votre contenu texte. Vous pouvez également utiliser la fonction DOMParser qui vous retournera un document utilisable ;

→ soit créer de toute pièce le paragraphe et le balisage italique en Javascript, avant d’insérer le contenu là aussi avec .textContent.

L’utilisation conjointe et intelligente de template et de MutationObserver permet d’offrir à peu de frais un comportement similaire à une bibliothèque de DOM virtuelle.

II-D-2 - Un cas pratique et exotique de script de contenu

Dans notre fichier d’arrière-plan background.js, nous avons utilisé les écouteurs d’événements pour intercepter les requêtes HTTP en fonction du type (main_frame pour notre cas) et de pattern matching, c’est-à-dire d’une correspondance de motif des URL.

1. browser.webRequest.onBeforeRequest.addListener(

2. listener,

3. {

4. urls: browser.SurveilleURLs,

5. types: [

6. "main_frame"

7. ]

8. },

9. [

10. "blocking"

11. ]

12. );

Cet écouteur renvoie en outre à une fonction listener créée de toute pièce pour l’occasion. Afin d’accéder à notre page d’édition, nous utiliserons le même principe. À cette différence que nous allons écouter le clic sur un bouton ajouté à l’interface du navigateur. Commençons par ajouter à notre fichier manifest le bloc suivant, avec un fichier de logo « qui va bien » à la racine de notre WebExt :

1. "browser_action": {

2. "default_icon": {

3. "19": "logo.png"

4. },

5. "default_title": "Editeur de transformations"

6. }

Magie : le bouton apparaît ! Il ne reste plus dans notre fichier d’arrière-plan qu’à ajouter un écouteur adapté :

1. browser.browserAction.onClicked.addListener(

2. () => {

3. browser.tabs.create(

4. {

5. url: browser.extension.getURL(

6. "edition.html"

7. )

8. }

9. ).then(

10. (objTab) => {

11. browser.tabs.executeScript(

12. objTab["id"],

13. {

14. "file" : browser.extension.getURL(

15. "edition.js"

16. )

17. }

18. );

19. }

20. );

21. }

22. );

Décomposons les deux étapes de cette portion de code : ouvrir un nouvel onglet vers la page edition.html, puis une fois cet onglet créé, nous utilisons son ID sur le navigateur pour y injecter un script JS : edition.js.

L’insertion étant faite lorsqu’une page est appelée – c’est-à-dire ici qu’un onglet est ouvert –, le rechargement simple, avec F5 par exemple, n’appellera pas le script à nouveau. Celui-ci n’étant pas connu sur la page, celle-ci restera inerte. Je vous encourage vivement à tester le code plus haut, même s’il ne remplit pas la fonction que l’on aurait pu initialement attendre. Ce cas se présente ici compte tenu de la spécificité de l’environnement (page interne à la WebExt).

Pour être chargé à chaque chargement du document, le script de notre page d’édition doit être appelé dans l’entête de la page d’édition et non pas dans le script d’arrière-plan…

<script type="text/javascript" src="./edition.js"></script>

Le script d’arrière-plan pour ce point, se limite désormais à :

1. browser.browserAction.onClicked.addListener(

2. () => {

3. browser.tabs.create(

4. {

5. url: browser.extension.getURL(

6. "edition.html"

7. )

8. }

9. );

10. }

Nouveau problème me direz-vous ? Comme nous appelons notre script dans la page, quelle sera la portée des variables ? Aura-t-on accès à la partie de l’API WebExt dédiée au script de contenu ? Hé bien oui : contre toute attente, c’est l’URL, pointant sur l’ID de la WebExt et sur un protocole « propriétaire » (moz-extension), qui permettra au navigateur d’autoriser le script devenu script de page à se comporter comme un script de contenu.

Faites le test :

console.log("browser", browser);

… renverra aussi bien l’objet browser dans le script d’arrière-plan insérant un script de contenu, que dans le script de page se comportant comme un script de contenu.

Nous pouvons donc intelligemment penser notre code pour réutiliser un peu partout les mêmes bibliothèques de procédures et de fonctions. Cependant nous devons désormais intercepter le chargement de la page : l’insertion du script en tant que de notre script de contenu en tant que script de page, arrive antérieurement à ce qu’aurait fait une insertion depuis l’arrière-plan de la WebExt.

Ainsi window.addEventListener sur l’événément load peut devenir impératif si vous souhaitez contrôler le déclenchement de certaines fonctions après le chargement complet de la page (DOM et contenus extérieurs).

II-E - Les sources complètes de la page d’édition

II-E-1 - La page

1. <!DOCTYPE html>

2. <html lang="fr">

3. <head>

4. <meta charset="utf-8">

5. <title>ControlePage - éditon page</title>

6. <script type="text/javascript" src="./edition.js"></script>

7. </head>

8. <body>

9. <header>

10. <h1>WebExtension 'ControlePage' - éditeur des transformations</h1>

11. </header>

12. <div id="page">

14. <h2>Gabarits XSL de transformation</h2>

16. <h3>Chercher un garabit</h3>

17. <form id="chercher">

18. <input type="text" name="id" value="defaut" />

19. <input type="submit" value="chercher" />

20. </form>

22. <h3>Indication</h3>

23. <p>

24. Les templates XSL dont le contenu est vierge, ne seront pas gardés. Un identifiant de transformation XSL portant le même nom qu'une entrée existante, sera écrasé. L'identifiant "defaut" ne peut pas (et ne doit pas) être supprimé.

25. </p>

27. <h3>Editer un gabarit</h3>

28. <p id="resultat">En attente...</p>

29. <form id="editer"></form>

31. </div>

32. <template id="gabTemplate">

33. <fieldset>

34. <legend>Template</legend>

35. <p>

36. Nom : <input type="text" name="name" value="" />

37. </p>

38. <p>

39. Match : <input type="text" name="match" value="" />

40. </p>

41. <textarea style="width: 100%; height: 250px;"></textarea>

42. </fieldset>

43. </template>

44. </body>

45. </html>

II-E-2 - Le script de contenu

1. console.log("browser", browser); // vérifier dans la console si l’on a accès ou non à la version prévue de l’API WebExt pour les scripts de contenu

3. // Function("console.log('ok');"); // tester si les fonctions d’évaluation JS sont disponibles (spoiler: non!)

5. function templateEditer(elTemplate) {

6. var elT = document.querySelector(

7. "template#gabTemplate"

8. ).content.cloneNode(

9. true

10. );

11. if (elTemplate.getAttribute("match")!=null)

12. elT.querySelector("input[name='match']").value = elTemplate.getAttribute("match");

13. if (elTemplate.getAttribute("name")!=null)

14. elT.querySelector("input[name='name']").value = elTemplate.getAttribute("name");

15. elT.querySelector("textarea").value = elTemplate.innerHTML.trim();

16. var elP = document.querySelector(

17. "form#editer input[type='submit']"

18. );

19. elP.parentElement.insertBefore(

20. elT,

21. elP

22. );

23. }

25. function gabTemplateVierge(arg) {

26. try {

27. arg.preventDefault();

28. } catch(err) {

30. } finally {

31. var elP = document.querySelector(

32. "form#editer input[type='submit']"

33. );

34. elP.parentElement.insertBefore(

35. document.querySelector(

36. "template#gabTemplate"

37. ).content.cloneNode(

38. true

39. ),

40. elP

41. );

42. }

43. }

45. function gabSupprimer(evtForm) {

46. evtForm.preventDefault();

47. var transfoId = "transformation:"+document.querySelector(

48. "form#chercher input[name='id']"

49. ).value;

50. if (transfoId=="defaut") {

51. var r = "Impossible de supprimer la transformation XSL par défaut.";

52. } else {

53. var r = "Demande de suppression de la transformation si elle existe.";

54. browser.storage.local.remove(

55. transfoId

56. );

57. }

58. document.querySelector(

59. "#resultat"

60. ).textContent = r;

61. }

63. function gabSauvegarder(evtForm) {

64. evtForm.preventDefault();

65. document.querySelector(

66. "#resultat"

67. ).textContent = "Construction puis demande de sauvegarde...";

68. var r = `

69. <?xml version="1.0"?>

70. <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

71. <xsl:output method="html" doctype-system="about:legacy-compat" encoding="UTF-8" omit-xml-declaration="yes" indent="yes" />

72. %corps

73. </xsl:stylesheet>

74. `;

75. var t = `<xsl:template %remplace>%contenu</xsl:template>`;

76. var ts = [];

77. evtForm.target.querySelectorAll(

78. "fieldset"

79. ).forEach(

80. (bloc) => {

81. var nom = bloc.querySelector("input[name='name']").value;

82. var match = bloc.querySelector("input[name='match']").value;

83. var r = [];

84. r["%contenu"] = bloc.querySelector("textarea").value;

85. if (nom!="")

86. r["%remplace"] = 'name="'+nom+'"';

87. if (match!="")

88. r["%remplace"] = 'match="'+match+'"';

89. var t_tmp = t;

90. for (var p in r) {

91. t_tmp = t_tmp.replace(

92. p,

93. r[p]

94. );

95. }

96. ts.push(

97. t_tmp

98. );

99. }

100. );

101. r = (

102. r.replace(

103. "%corps",

104. ts.join(" ")

105. )

106. ).trim();

107. var transfoId = "transformation:"+document.querySelector(

108. "form#chercher input[name='id']"

109. ).value;

110. var obj = {};

111. obj[transfoId] = r;

112. browser.storage.local.set(

113. obj

114. ).then(

115. (arg) => {

116. document.querySelector(

117. "#resultat"

118. ).textContent = "Sauvegarde réussie";

119. }

120. );

121. }

123. function gabBoutons(elP) {

124. [

125. ["submit", "sauvegarder", null],

126. ["submit", "supprimer", gabSupprimer],

127. ["submit", "ajouter template vierge", gabTemplateVierge],

128. ].forEach(

129. (typeBouton) => {

130. var bouton = document.createElement(

131. "input"

132. );

133. bouton.setAttribute(

134. "type",

135. typeBouton[0]

136. );

137. bouton.setAttribute(

138. "value",

139. typeBouton[1]

140. );

141. if (typeof typeBouton[2]=="function")

142. bouton.addEventListener(

143. "click",

144. typeBouton[2]

145. );

146. elP.appendChild(

147. bouton

148. );

149. }

150. );

151. }

153. function gabChercher(evtForm) {

154. evtForm.preventDefault();

155. var form = evtForm.target;

156. var transfoId = "transformation:"+form[0].value;

157. browser.storage.local.get(

158. transfoId

159. ).then(

160. (objR) => {

161. var formEditer = document.querySelector(

162. "form#editer"

163. );

164. while(formEditer.firstChild) {

165. formEditer.removeChild(

166. formEditer.firstChild

167. );

168. }

169. gabBoutons(

170. formEditer

171. );

172. if (transfoId in objR) {

173. var r = "Gabarit '"+transfoId+"' trouvé";

174. var parseur = new DOMParser();

175. var doc = parseur.parseFromString(

176. objR[transfoId],

177. "text/xml"

178. );

179. var elsTemplate = Array.from(

180. doc.getElementsByTagName("xsl:template")

181. ).forEach(

182. templateEditer

183. );

184. } else {

185. var r = "Gabarit '"+transfoId+"' pas trouvé ; sauvegarder pour créer";

186. gabTemplateVierge();

187. }

188. document.querySelector(

189. "#resultat"

190. ).textContent = r;

191. }

192. );

193. }

195. window.addEventListener(

196. "load",

197. (evt) => {

198. document.querySelector("form#chercher").addEventListener(

199. "submit",

200. gabChercher

201. );

202. }

203. );

205. window.addEventListener(

206. "load",

207. (evt) => {

208. document.querySelector("form#editer").addEventListener(

209. "submit",

210. gabSauvegarder

211. );

212. }

213. );

III - Seconde partie : ajouter un bouton d’action à un réseau social

III-A - Le contexte

J’indique ici la possibilité d’ajouter un bouton d’action : cela peut être aussi une surveillance de contenu, récupérer ou copier des contenus, etc : la base – la surveillance d’une page, avec l’injection d’un code – reste peu ou prou la même.

Dans notre fichier background.js, nous ajoutons ces quelques lignes que vous connaissez déjà :

1. browser.contentScripts.register({

2. "js": [{file: "/twitter.js"}],

3. "matches": ["https://twitter.com/"],

4. "runAt": "document_idle"

5. });

… et créons un fichier twitter.js à la racine de notre WebExt. Désormais ce fichier est injecté dans toutes les pages qui correspondent à la racine du domaine Twitter.

Si vous décortiquez les principaux éléments intéressants de la page Accueil de Twitter, celle avec la timeline, vous noterez que finalement cette « ligne du temps » est composée d’une liste et de quelques DIV : rien d’exceptionnel. Nous agirons sur le menu de chaque tweet, en ajoutant à la fin un bouton qui, une fois cliqué, affiche un message.

→ Pour la liste des tweets, voici un extrait des principaux éléments HTML intéressants :

div#timeline > div.stream > ol#stream-items-id > li.stream-item > div.tweet

→ Pour le menu de chaque tweet :

div.dropdown-menu > ul (role=menu) > li.copy-link-to-tweet > button.dropdown-link

III-B - Le principe

Twitter, comme Facebook ou Google, et d’autres, génère en quelque sorte une page avec très peu de données envoyées directement par le code source HTML de la page (que ce soit du HTML ou du JSON). Le gros du contenu est récupéré ensuite, par des requêtes (WS, ajout de scripts par le DOM ou Ajax, peu nous importe). Compte tenu du principe commun qu’il s’agit d’un fil mis à jour en continu, intercepter chaque requête est lourd et il n’y a évidemment pas de documentation sur le fonctionnement interne des outils. Ces sites utilisent généralement des requêtes imbitiques : ne tentons pas de les comprendre !

Comme nous injectons un fichier JS et que n’avons pas les variables des scripts de la page, ce serait de toute façon bien trop compliqué à faire, il existe un moyen simple et probablement un des outils les plus puissants de JS pour le Web : MutationObserver.

Il s’agit d’un « poste » d’observation tel que peut le faire les evenement listener. À chaque événement déterminé arbitrairement, un callback est appelé par le navigateur avec en argument, un objet utile. Un MutationObserver peut être activé et désactivé.

Dans notre fichier twitter.js, ajoutons le code suivant :

1. function Agir(objR) {

2. console.log(objR);

3. }

5. var observateur = new MutationObserver(

6. (elsListe) => {

7. Agir(

8. elsListe[0].addedNodes

9. );

10. }

11. );

13. observateur.observe(

14. document.querySelector(

15. "ol#stream-items-id"

16. ),

17. {

18. childList: true // on surveille seulement les éléments HTML enfants ajoutés

19. }

20. );

→ Votre console de page devrait afficher un résultat avec un tableau. Le premier item est le résultat en tant que tel, le second item est l’objet observé. Le premier item est un objet dont l’un des attributs, addedNodes, est la liste des enfants ajoutés à l’élément ol#stream-items-id. C’est-à-dire la liste des tweets de la timeline de la page.

À chaque fois que vous allez cliquer sur « Voir (n) nouveaux tweets », votre observateur déclenchera le callback Agir qui sera la fonction pour ajouter notre prochain bouton de menu. Reste que les tweets déjà ajoutés lors de la construction de la page ne seront pas traités : nous devrons donc, dès le chargement de notre script de contenu twitter.js, les prendre en compte…

1. var observateur = new MutationObserver(

2. (elsListe) => {

3. Agir(

4. elsListe[0].addedNodes

5. );

6. }

7. );

9. observateur.observe(

10. document.querySelector(

11. "ol#stream-items-id"

12. ),

13. {

14. childList: true

15. }

16. );

18. Agir(

19. document.querySelectorAll(

20. "div#timeline li.stream-item"

21. )

22. );

Vous aurez compris le principe : on laisse la page se charger et agir « normalement », et on ne fait que surveiller puis intercepter les éléments qui nous intéressent.

Le fichier twitter.js permettant l’ajout d’un bouton dans le menu de chaque tweet, vous trouvez le code en fin de cette seconde partie. Il se passe de commentaires particuliers : aucune difficulté n’est à noter et les scripts de Twitter ne peuvent ni savoir (sauf s’ils surveillent eux-même les modifications effectuées) ni agir sur notre script de contenu.

Ce type de dispositif est ce que l’on retrouve entre Avast et Gmail par le navigateur, où l’utilisateur ne voit pas – lors de l’édition – qu’Avast ajoute une signature sans le prévenir directement dans le code HTML des courriels envoyés par Gmail.

Une pub gratuite car le destinataire voit la signature et se sent faussement protégé. Ce comportement, inacceptable, est aussi un des mondes offerts par les WebExtension : j’imagine que mon propos liminaire sur la mise en garde prend son sens…

III-C - Bonus : et si on affichait plutôt une notification ?

Plutôt que la classique « alerte » JS qui impose d’être sur le navigateur et sur la page appelée, une notification gérée par le navigateur dans le contexte de l’OS est parfois plus intéressante (agenda, alerte hors du contexte de navigation, etc).

Modifions tout d’abord notre fichier manifest pour rajouter la permission des notifications :

1. "permissions": [

2. "unlimitedStorage",

3. "storage",

4. "webRequest",

5. "notifications",

6. "webRequestBlocking",

7. "<all_urls>"

8. ],

Puis ajoutons les quelques lignes suivantes au script d’arrière-plan background.js :

1. browser.notifications.create({

2. "type": "basic",

3. "iconUrl": browser.extension.getURL("logo.png"),

4. "title": "Clic",

5. "message": "Je viens de Twitter !"

6. });

… magie, dès le rechargement (automatique ou non) de votre WebExt, une notification apparaît ! Et le plus tragique : c’est ce que ce beau code ne fonctionne pas en script de contenu… Votre navigateur vous informera simplement que « browser.notifications is not defined ». C’est logique : votre API en contexte script de contenu est davantage limité que celle offerte au script d’arrière-plan, pour éviter un abus généré par la page.

Modifions notre fonction BoutonAlerte par le code suivant :

1. function BoutonAlerte(elementMenuTweet) {

2. var itemSupp = document.createElement("li");

3. itemSupp.setAttribute(

4. "class",

5. "copy-link-to-tweet"

6. );

7. var boutonSupp = document.createElement("button");

8. boutonSupp.textContent = "Item de menu ajouté";

9. boutonSupp.setAttribute(

10. "class",

11. "dropdown-link"

12. );

13. itemSupp.appendChild(

14. boutonSupp

15. );

16. itemSupp.addEventListener(

17. "click",

18. (evt) => {

19. browser.runtime.sendMessage(

20. {

21. "titre": "coucou",

22. "message": "Que la force de Developpez.com soit avec toi !"

23. }

24. );

25. }

26. );

27. elementMenuTweet.appendChild(

28. itemSupp

29. );

30. }

J’utilise ici la méthode runtime de l’objet browser qui permet de transmettre des objets entre les différentes zones / contextes d’exécution des scripts de la WebExt. Dès lors il ne reste plus qu’à créer l’écouteur d’événement et savoir si le message associé est bien une demande de notification :

1. browser.runtime.onMessage.addListener(

2. (objMessage) => {

3. if (

4. "type" in objMessage

5. &

6. objMessage["type"] == "notification"

7. )

8. browser.notifications.create({

9. "type": "basic",

10. "iconUrl": browser.extension.getURL("logo.png"),

11. "title": objMessage["titre"],

12. "message": objMessage["message"]

13. });

14. }

15. );

Et là miracle : le clic sur le menu d’un tweet provoque l’apparition d’une notification ! Je vous laisse imaginer l’outil puissant de suivi des réseaux sociaux que vous pouvez ainsi construire ainsi, assez facilement.

Voici votre item de menu pour chaque tweet tel qu’il devrait apparaître :

III-D - Le code source complet du script twitter.js

1. // Pour Twitter exclusivement ; à adapter en fonction des sites

3. /* menu : div.dropdown-menu > ul (role=menu) > li.copy-link-to-tweet > button.dropdown-link */

4. /* liste : div#timeline > div.stream > ol#stream-items-id > li.stream-item > div.tweet */

6. function BoutonAlerte(elementMenuTweet) {

7. var itemSupp = document.createElement("li");

8. itemSupp.setAttribute(

9. "class",

10. "copy-link-to-tweet"

11. );

12. var boutonSupp = document.createElement("button");

13. boutonSupp.textContent = "Item de menu ajouté";

14. boutonSupp.setAttribute(

15. "class",

16. "dropdown-link"

17. );

18. itemSupp.appendChild(

19. boutonSupp

20. );

21. itemSupp.addEventListener(

22. "click",

23. (evt) => {

24. browser.runtime.sendMessage(

25. {

26. "type" : "notification",

27. "titre" : "coucou",

28. "message" : "Que la force de Developpez.com soit avec toi !"

29. }

30. );

31. }

32. );

33. elementMenuTweet.appendChild(

34. itemSupp

35. );

36. }

38. function Agir(listeConteneurs) {

39. console.log(listeConteneurs);

40. listeConteneurs.forEach(

41. (conteneurTweet) => {

42. elementMenuTweet = conteneurTweet.querySelector(

43. "div.stream-item-header div.dropdown-menu ul"

44. );

45. BoutonAlerte(

46. elementMenuTweet

47. );

48. }

49. );

50. }

52. var observateur = new MutationObserver(

53. (elsListe) => {

54. Agir(

55. elsListe[0].addedNodes

56. );

57. }

58. );

60. observateur.observe(

61. document.querySelector(

62. "ol#stream-items-id"

63. ),

64. {

65. childList: true

66. }

67. );

69. Agir(

70. document.querySelectorAll(

71. "div#timeline li.stream-item"

72. )

73. );

IV - Conclusion, ouverture et remerciements

Ce tutoriel a balayé de nombreux concepts et outils, intégrés à différents niveaux dans les navigateurs. Si les WebExtensions comme les transformations du XML sont bien gérées par les « poids lourds » du secteur, des nuances peuvent encore exister dans l’exploitation des API. Ainsi les WebExtension ne signent pas la fin du travail d’ajustements entre navigateurs, mais une première étape fondamentale à l’harmonisation.

J’ai abordé ici le seul point du contenu, du corps, des requêtes. L’association des WebExtensions avec toutes les possibilités (interception et émission de requêtes, modification d’entêtes, proxy « complet », palliatif aux restrictions cross-domain, etc), les API pour dialoguer avec les autres logiciels du poste, le code machine à la sauce WebAssembly, ainsi qu’un contexte désormais multi-thread, tout cela permet à un navigateur déjà « lourd » de devenir « colossal » – pouvant remplaçant ou gérer la quasi-totalité de nos tâches quotidiennes.

Un aperçu de la compatibilité des API exhaustives, entre les principaux navigateurs, nous illustre ce poids considérable que peut prendre un navigateur.

Probablement à terme, malgré la réticence des majors de services en ligne, irons-nous vers des applications bien plus « ouvertes » (au moins parce que le code n’est pas compilé!) et non-propriétaires : Firefox est par exemple accessible sous les OS, mobiles y compris (à l’exclusion d’Apple, dont iOS impose un moteur). Or l’intérêt d’une WebExtension pour un service en ligne, c’est de cibler la quasi-totalité des OS, fixes et mobiles, ainsi que les principaux navigateurs mobiles.

Ne faut-il pas déjà réfléchir à abandonner une part des applications mobiles et des écosystèmes parfois fermés de diffusion (Apps Android / Google Store, Apple Store, Windows Store, etc.) ?

La question mérite d’être posée et d’être posée rapidement.

IV-A - De la question des droits et des usages

Derrière toutes ces opportunités, se cache encore et toujours l’éducation de l’utilisateur : je n’ai illustré ici qu’une infime partie de ce qu’il est possible de faire. Les bloqueurs de pub et les « anonymiseurs », rentrent naturellement dans la catégorie des « bons » usages de ces droits étendus sur le surf de l’utilisateur. Mais nous serions pour le moindre crédules, de croire que ces usages restent pacifiques. Comme pour Android, ou iOS, les développeurs risquent d’être tentés de demander « toujours plus » de droit pour les WebExtensions, alors que ces droits sont finalement très peu « lisibles » pour un utilisateur lambda, et peuvent lui créer de sérieux dommages sur le respect de sa vie privée et des considérations techniques sur la sécurité et la fiabilité de ses machines.

De plus la question des droits rejoint celle des usages : or un navigateur est souvent « connecté » à d’autres, par le biais d’un profil qui permet à l’utilisateur d’être itinérant dans sa logique d’utilisateur du Web, jonglant entre les périphériques (mobiles ou fixes). Les effets peuvent être en cascade, alors que l’attente est différente. Les puissances, l’espace de stockage et la gestion même des ressources : sur Android par exemple, il n’est pas rare de voir l’OS « décharger » de la RAM un logiciel, puis le reprendre. Si vous avez une WebExt d’agenda, vous pourriez ne pas remplir pleinement la fonction si votre navigateur n’est pas régulièrement ouvert et ce dernier sera précautionneux quant au temps de calcul et à la fréquence du déclenement de certains événements pris par vos scripts.

IV-B - Les WebExtensions sont-elle finalement la pierre angulaire du Web décentralisé ?

Avec l’émergence et une nouvelle cohérence dans la socialisation du Web, dont l’éclatement en « fédérations » et « services » impose de nouvelles approches à taille humaine, les WebExtensions pourraient devenir une clé de voûte de systèmes nouveaux mais aussi de risques nouveaux : comme je l’indiquais dans les recommandations en introduction, la sécurité des extensions est loin d’être parfaite malgré les limitations des API. Derrière, c’est la sécurité d’un logiciel, le navigateur, qui est impactée et donc la sûreté de l’ensemble de la machine.

Reste d’autres limites – sur la garantie des données stockées, qui n’est aujourd’hui logiciellement pas assurée –, comme l’intérêt d’étendre certaines API (faut-il par exemple, permettre à un navigateur de se comporter comme un serveur, à la mode NodeJS ?). Car créer un navigateur « qui fait tout », c’est aussi (un peu) remplacer progressivement l’intérêt d’un OS et de la richesse de ses logiciels, en rendant l’utilisateur dépendant d’une plateforme qui n’offre tout de même pas les garanties et le suivi que peuvent avoir la plupart des OS aujourd’hui. Il est donc, au-delà de la partie sûreté / sécurité, question de stabilité et d’intérêts.

ChromeOS avait en partie répondu à cette question dès 2011, suivi par Ubuntu Touch qui n’a pas trouvé son public de « partenaires »., Les WebExtensions ne sont pas éloignées de l’intérêt des conclusions sur ces OS précurseurs quant aux coûts réduits des machines, et dont la puissance est déportée sur le réseau vers les fermes de serveurs. La très faible configuration nécessaire est permise aussi compte tenu d’une interface exclusivement orientée vers le HTML5/Web.

Vraiment un autre monde mais finalement pas si éloigné des anciens…

V - Note de la rédaction de Developpez.com

Nous tenons à remercier ALT pour la relecture orthographique de ce tutoriel.

(1)Ainsi pour le cas du blocage d’une requête (annulation, modification ou redirection) que j’évoque plus loin, l’ordre n’est pas défini entre les WebExt et l’action peut être poursuivie avec un même identifiant de requête : When multiple blocking handlers modify a request, only one set of modifications take effect. Redirects and cancellations have the same precedence. So if you canceled a request, you might see another request with the same requestId again if another blocking handler redirected the request.

(2)Mon propos est extrême car il est en réalité possible et assez facile d’envoyer, depuis un script d’arrière-plan, une chaîne de caractère qui sera évaluée à un onglet depuis l’API tab. La fonction tab.executeScript autorise ainsi soit le chargement d’une URL (dont possiblement un blob, pouvant être construit par ailleurs), soit l’injection directe d’un code qui sera évalué. Si vous ne savez pas exactement ce que vous faites et que vous n’êtes pas certain du résultat comme de la source d’où provient le code à exécuter, ne le faites pas !