Créer un navigateur personnalisé - 3ème partie

Injection de scripts JavaScript et de styles CSS suivant l'URL

21 janvier 2016

Introduction

Ce projet est open source et accessible depuis GitHub GL-Browser.

Cet article est le 3ème d'une série d'articles concernant la création d'un navigateur internet incluant par défaut des fonctionnalités que l'on retrouve habituellement sous forme d'extensions dans Chrome ou Firefox.

Exemples de fonctionnalités :

Nous souhaitons :

Nous utilisons Electron qui mixe le navigateur open source Chromium avec la richesse du framework Node.js.

Objectifs

Dans ce 3ème article, nous réaliserons l'injection d'un style CSS et d'un code JavaScript correspondant à l'URL visitée, afin de supprimer par exemple, le voile Pinterest, les annonces/recommandations de Twitter, ...

Supprimer le voile PinterestSupprimer les annonces/recommandations de Twitter
Capture d'écran PinterestCapture d'écran Twitter

Résumé des articles précédents

Article 1 : initialisation du projet avec Electron.
Article 2 : injection du CSS et du JavaScript dans la page de Google.

Pour récupérer directement les sources du 2nd article :

git clone https://github.com/emmanuelroecker/GL-Browser
git checkout article2

Table d'association URL - JS/CSS

La table d'association est représentée sous la forme d'un fichier contenant une liste d'identifiants.
Une liste de motifs/masques d'URL est associée à chaque identifiant.

Chaque identifiant est également un nom de fichier contenant le code js ou le css (suivant son extension) à injecter dans l'URL compatible avec l'un des masques associés.

Les match patterns

Généralement, un motif (un ensemble de chaînes de caractères possibles) est représenté à l'aide d'une expression régulière.
Les chaînes de caractères étant limitées à des URL, les motifs peuvent être simplifiés en employant les match patterns.

Les match patterns utilisent uniquement le caractère spécial * pour indiquer qu'à cet emplacement n'importe quelle chaîne de caractères peut être présente.

Exemple :

https://*.example.com/foo*bar correspond à toutes les URL composées :

https://www.example.com/foo/baz/bar et https://docs.example.com/foobar sont compatibles avec https://*.example.com/foo*bar

Le format YAML

YAML est un standard de sérialisation de données facile à lire, à comprendre et à éditer.

Créer le fichier config.yml au format YAML qui contiendra la liste des identifiants et les match patterns :

- name : pinterest
  patterns:
           - https://*.pinterest.com/*
           - http://*.pinterest.com/*
- name: twitter
  patterns:
           - https://*.twitter.com/*
- name: google
  patterns:
           - https://www.google.com/*
           - https://www.google.fr/*

Répertoire du projet

L'arborescence du projet est désormais :

│   .gitignore
│   config.yml
│   index.html
│   LICENSE
│   main.js
│   package.json
│   README.md

├───cfg
│       google.css
│       google.js
│       pinterest.css
│       pinterest.js
│       twitter.css
│       twitter.js

└───node_modules

Installer les librairies

Utiliser la librairie js-yaml pour lire un fichier YAML.
Utiliser la librairie match-pattern pour convertir un match patterns en expression régulière.

Ajouter au fichier package.json les dépendances :

{
    "devDependencies": {
        "electron-prebuilt": "^0.36.0",
        "js-yaml": "^3.5.0",
        "match-pattern": "^0.0.1"
    }
}

Pour les installer :

npm install

Le code

Initialisation de la table d'association URL - JS/CSS

Regrouper ensemble les opérations pouvant être effectuées une seule fois au lancement du navigateur :

Ajouter dans le fichier index.html :

let fs = require('fs');
let yaml = require('js-yaml');
let matchPattern = require('match-pattern');
let directoryCfg = 'cfg';   //répertoire des css et js
let fileCfg = 'config.yml';
let cfg;

try {
    cfg = yaml.safeLoad(fs.readFileSync(fileCfg, 'utf8')); //lecture du fichier yaml
    cfg = cfg.map(function(elem) { //parcourt l'ensemble des identifiants
        let name = elem['name'];
        elem['css'] = fs.readFileSync(directoryCfg + "/" + name + ".css", 'utf-8'); //lit le contenu du fichier css associé à l'identifiant
        elem['js'] = fs.readFileSync(directoryCfg + "/" + name + ".js", 'utf-8'); //lit le contenu du fichier js associé à l'identifiant

        let patterns = elem['patterns'];
        elem['patterns'] = patterns.map(function(pattern) { //parcourt l'ensemble des match patterns associés à un identifiant
            pattern = matchPattern.parse(pattern); //convertit le match pattern en regexp
            if (pattern === null) {
                console.log("Bad pattern : " + pattern + " in " + name);
            }
            return pattern;
        });
        return elem;
    });
} catch (e) {
    console.log(e);
}

Injection des CSS et JavaScript associés à une URL

À la validation de l'URL saisie par l'utilisateur, parcourir tous les motifs jusqu'au premier compatible et injecter les CSS et JavaScript correspondants.

Ajouter au fichier index.html :

function getToInject(url) {
    for (let elem of cfg) { //parcourt l'ensemble des identifiants
      let patterns = elem['patterns']
      for (let pattern of patterns) { //parcourt l'ensemble des motifs d'un identifiant
        if (pattern.test(url)) { //test si l'url est compatible avec le motif 
          injectCSS = elem['css'];
          injectJS = elem['js'];
          return; //arrête la recherche au premier trouvé
        }
      };
    };
}

let url = document.getElementById("urltext").value;
getToInject(url);
webview.src = url;

Exemples d'injections

Pinterest

Créer le fichier pinterest.js à injecter au chargement de la page de Pinterest et indiquant l'élément HTML à supprimer :

let signeup = document.getElementsByClassName('InlineSignup')
if (signeup && signeup[0]) {
  signeup[0].remove();
}

Le rendu :

AvantAprès
Capture d'écran du résultatCapture d'écran du résultat

Twitter

Créer le fichier twitter.js à injecter au chargement de la page de Twitter et indiquant les éléments HTML à supprimer :

let recommendations = document.getElementById("empty-timeline-recommendations");
if (recommendations) {
    recommendations.remove();
}

let trends = document.getElementsByClassName('Trends');
if (trends && trends[0]) {
    trends[0].remove();
}

let whoToFollow = document.getElementsByClassName('WhoToFollow');
if (whoToFollow && whoToFollow[0]) {
    whoToFollow[0].remove();
}

let relatedUsers = document.getElementsByClassName('RelatedUsers');
if (relatedUsers && relatedUsers[0]) {
    relatedUsers[0].remove();
}

Le rendu :

AvantAprès
Capture d'écran du résultatCapture d'écran du résultat

Envoyer sur GitHub

git add -A
git commit -m "article 3"
git push

Prochain article

Le prochain article traitera du blocage des requêtes de suivi/tracking des utilisateurs à l'instar de Adblock ou uBlock.