Firefox webextensions

De Banane Atomic
Aller à la navigationAller à la recherche

Liens

Hierarchie des fichiers

  • manifest.json
  • main.js
  • main.html
  • icons
    • icon-48.png
    • icon-96.png
  • content_scripts

manifest.json

manifest.json
{
  "manifest_version": 2,
  "name": "Nom de l'extension",
  "version": "1.0",

  "description": "description",
  // taille 48 ou 96
  "icons": {
    "48": "icons/icon-48.png"
  },

  "permissions": [ "tabs" ]
}

permissions

Accès au manifest

manifest.json
{
    "mySetting": "value"
}
background.js
var mySetting = browser.runtime.getManifest().mySetting;

Content script

Tourne dans le contexte de la page et peut accéder au contenu de la page
console.log ne fonctionne pas ici, utiliser plutôt alert

Background script

manifest.json
"background": {
    "scripts": ["background.js"]
}
background.js

Injecter du JS / CSS dans la page

content_scripts manifest.json key

manifest.json
// charge le script si le pattern correspond
"content_scripts": [
    {
        "matches": ["*://*.mozilla.org/*"],
        "js": ["script.js"]
    }
]
script.js
document.body.style.border = "5px solid red";

contentScripts API

tabs.executeScript

background.js
browser.tabs.executeScript({
    code: "alert('xxx');"
});

var executing = browser.tabs.executeScript({
    file: "modifyPage.js"
});

executing.then((result) => {
    console.log(result);  // Array [ 0 ]
}, (error) => {
    console.log(error);
});
modifyPage.js
// to avoid the error result is non-structured-clonable data
// return a value as a result which is structured clonable.
0;

Nécessite des permissions d'hôte pour la page dans laquelle on veut injecter du js.

manifest.json
"permissions": [ "<all_urls>", "*://developer.mozilla.org/*" ]

insertCSS

background.js
browser.tabs.insertCSS({
    code: "#id { font-size: 64px; }"
});

browser.tabs.insertCSS({
    file: "style.css"
});

web_accessible_resources

Security Error: Content at https://www.domain.fr.com may not load or link to moz-extension://<guid>/icons/icon.png.
manifest.json
"web_accessible_resources": ["icons/icon.png"]
content.js
var icon = document.createElement('img');
var iconUrl = browser.extension.getURL("icons/icon.png");
icon.src = iconUrl;

Communication

Content → Background

content.js
browser.runtime.sendMessage({"key": value});
background.js
browser.runtime.onMessage.addListener((message) => {
    console.log("Message received " + message.key);
});

Background → Content

background.js
browser.tabs.sendMessage(tabId, {"key": value});
content.js
browser.runtime.onMessage.addListener((message) => {
    alert("Message received " + message.key);
});

toolbar button

manifest.json
"browser_action": {
    "default_icon": "icons/icon-32.png",
    "theme_icons": [{
        "light": "icons/icon-32-light.png",
        "dark": "icons/icon-32.png",
        "size": 32
    }],
    "default_title": "Title"
}

Sans Popup

manifest.json
"background": {
    "scripts": ["background.js"]
}
background.js
function doSomething() { }

browser.browserAction.onClicked.addListener(doSomething);

Popup

manifest.json
"browser_action": {
    "default_popup": "popup/actions.html"
}
actions.html
<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="actions.css"/>
  </head>

  <body>
    <div>Choix 1</div>
    <div>Choix 2</div>

    <script src="actions.js"></script>
  </body>

</html>
actions.css
html, body {
    width: 200px;
    background-color: black;
    color: white;
}

div {
    border-bottom: solid 1px black;
    margin: 10px 0;
}
div:hover {
    border-color: red;
    cursor: pointer;
}
actions.js
document.addEventListener("click", function(e) {
    if (e.target.tagName != "DIV") {
        return;
    }
    
    alert(e.target.textContent);
});

button title and icon

Javascript.svg
function toggleTitle(title) {
  if (title == "Old Title") {
    browser.browserAction.setTitle({ title: "New Title" });
    browser.browserAction.setIcon({ path: "new-icon.png" });
  } else {
    browser.browserAction.setTitle({title: "Old Title"});
    browser.browserAction.setIcon({ path: "old-icon.png" });
  }
}

browser.browserAction.onClicked.addListener(() => {
  var gettingTitle = browser.browserAction.getTitle({});
  gettingTitle.then(toggleTitle);
});

Tab

background.js
// listen to tab URL changes
browser.tabs.onUpdated.addListener((tabId, changeInfo, tabInfo) => {
    if (changeInfo.url) {
        // changeInfo.url nécessite la permission tabs
        console.log("URL changed to " + changeInfo.url);
        console.log("Status " + changeInfo.status);  // loading ou complete
        // regex url
        if (/https:\/\/www.domain.fr\/path\/.*/.test(changeInfo.url)) { }
    }
});

// listen to tab switching
browser.tabs.onActivated.addListener(() => {
    console.log("tab switching");
});

// active tab
var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true});
gettingActiveTab.then((tabs) => {
    if (tabs[0]) {
        currentTab = tabs[0];
        var tabId = currentTab.id;
        var tabUrl = currentTab.url;
    }
});
content.js
let currentUrl = window.location.href;

Download

background.js
var downloading = browser.downloads.download({
	url: myUrl,
	filename: "image.jpg",  // définit le chemin relatif depuis le dossier des téléchargements (créé les dossiers au besoin)
	saveAs: true,           // affiche la fenêtre de choix de l'emplacement
        incognito: true         // téléchargement dans une navigation privé donc pas affiché dans la liste des téléchargements
});
downloading.then(
(id) => { console.log("ok " + id); }, 
(error) => { console.log(error); });

Request / XMLHttpRequest

background.js
var request = new XMLHttpRequest();
request.withCredentials = true;

// async
request.addEventListener("readystatechange", function () {
  if (this.readyState === 4) {
    console.log(this.responseText);
  }
});
request.open("GET", "http://www.domain.fr/api/controller");

// sync
request.open('GET', 'http://www.domain.fr/api/controller', false);

request.setRequestHeader("Content-Type", "application/json");
request.send(null);
// request.status → 200
// request.response → { ... }

Request / fetch

GET

background.js
fetch(myRequest)
.then(response => {  // récupération de la réponse
    if (response.ok) {  // test du code de retour
        // parser les données
        return response.json();  // json
        return response.text();  // text
        return response.blob();  // blob (image)
    } else {
        throw new Error('Something went wrong on api server!');
    }
})
.then(blob => {  // données récupérées
    console.log(blob);  // Blob { size: 25248, type: "image/jpeg" }
    var objectURL = URL.createObjectURL(blob);  // blob:moz-extension://guid/guid
}).catch(error => {
    console.error(error);
});

async / await

Javascript.svg
async function myFunction() {
    let data = await (await (fetch(url).
        then(response => response.blob())));
    return data;
}

POST

background.js
// json
var data = JSON.stringify({ key: "value" });

// blob
var data = new FormData();
data.append("Content-Type", "multipart/form-data");
data.append("myKey", myBlob);


var myHeaders = new Headers();
myHeaders.append('Cache-Control', 'no-cache');
myHeaders.append('Content-Type', 'application/json');  // json

const myRequest = new Request('http://www.domain.fr/api/images', 
{
    method: 'POST',
    headers: myHeaders, 
    body: data
});
fetch(myRequest).then(response => {
    if (response.ok) {
        return response.json();
    } else {
        throw new Error('Something went wrong on api server!');
    }
})
.then(response => {
    console.debug(response);
}).catch(error => {
    console.error(error);
});

415 Unsupported Media Type response

Javascript.svg
// définir le bon Content-Type
var myHeaders = new Headers();
myHeaders.append('Content-Type', 'application/json');
var myRequest = new Request('http://www.domain.fr/api/items', {method: 'POST', headers: myHeaders, body: { "key": "value" }});

Cross-Origin Request Blocked

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:59383/api/listapi. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

Ajouter les urls dans permissions

manifest.json
"permissions": [ "<all_urls>" ]

Fichiers

Window

background.js
// ouvrir une fenêtre popup contenant un html local
var popupURL = browser.extension.getURL("popup.html");
var creating = browser.windows.create({
    url: popupURL,
    type: "popup",
    state: "maximized",  // maximized et fullscreen doivent être utilisés sans popup, height, width, top, left
    height: 600,
    width: 800,
    left: 100,
    top: 100
});
creating.then((windowInfo) => {
    console.log(windowInfo);
}, (error) => {
    console.log("Error: " + error);
});

Test

web-ext

Par défaut, la commande run crée un profil Firefox temporaire.
La commande run surveille vos fichiers sources et dit à Firefox de recharger l'extension quand vous éditez et enregistrez un fichier.
Powershell.png
# depuis le dossier contenant les fichiers sources
web-ext run
# forcer une version de firefox et un profile
web-ext run --firefox="C:\Program Files\Firefox Developer Edition\firefox.exe" --firefox-profile=MyProfile

# installation
npm i -g web-ext

Load Temporary Add-on

about:debugging → Load Temporary Add-on → sélectionner n'importe quel fichier
Ceci installe l'add-on jusqu'au redémarrage de firefox

Debugging

  1. about:debugging
    1. Enable add-on debugging
    2. Load Temporary Add-on ou web-ext run
    3. Debug (ouvre une fenêtre Developer Tools)

Packager et signer l'extension

Bash.svg
zip -x some-file.ext -r -FS ../MyWebExtension.zip *
# .git est ignoré
  1. Se rendre sur la page Developer Hub → Submit a New Add-on
  2. Une fois l'add-on validée, un lien est donné pour permettre de télécharger l'add-on signé.
On peut signer un add-on sans le publier sur le site de mozilla.

Supprimer une extension publiée dans Developer Hub

  1. Manage My Submissions
  2. Mon Extension → More → Manage Status and Version → Delete Add-on