Firefox webextensions

De Banane Atomic
Révision datée du 17 octobre 2021 à 21:36 par Nicolas (discussion | contributions) (→‎permissions)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
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
{
  // mandatory keys
  "manifest_version": 2,
  "name": "Nom de l'extension",
  "version": "1.0",

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

permissions

manifest.json
{
    "permissions": [
        // permissions d'hôte (fetch, tabs.executeScript, webRequest, cookies)
        "<all_url>", // no restriction, allow access to all urls
        "*://www.domain.net/*",
        "*://localhost/*",
        "tabs",
        "notifications"
    ]
}

Accès au manifest

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

Background script

Background scripts do not get direct access to web pages.
However, they can load content scripts into web pages and can communicate with these content scripts using a message-passing API.

Log only in Browser Console
manifest.json
"background": {
    "scripts": ["background.js"]
}
background.js

Insert CSS

background.js
var css = "body { border: 20px dotted pink; }";

browser.browserAction.onClicked.addListener(() => {

  function onError(error) {
    console.log(`Error: ${error}`);
  }

  var insertingCSS = browser.tabs.insertCSS({code: css});
  var insertingCSS = browser.tabs.insertCSS({file: "content-style.css"});
  insertingCSS.then(null, onError);
});

Content script

Tourne dans le contexte de la page et peut accéder au contenu de la page

Content scripts can read and modify the content of their pages using the standard DOM APIs.
Content scripts can only access a small subset of the WebExtension APIs, but they can communicate with background scripts using a messaging system, and thereby indirectly access the WebExtension APIs.

manifest content_scripts

manifest.json
// charge le script uniquement si le pattern correspond
"content_scripts": [
  {
    "matches": ["*://*.domain.net/*"],
    "js": ["content.js"],
    "css": ["content.css"]
  }
]
content.js
document.body.style.border = "20px dotted pink";
content.css
body { border: 20px dotted pink; }

contentScripts API

tabs.executeScript API

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/*" ]

web_accessible_resources

Security Error: Content at https://www.domain.net 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.runtime.getURL("icons/icon.png");
icon.src = iconUrl;

Communication

Content → Background

content.js
function handleResponse(message) {
    console.log(`Message from the background script:  ${message.response}`);
}

function handleError(error) {
    console.log(`Error: ${error}`);
}

browser.runtime
    .sendMessage({key: "value"})
    .then(handleResponse, handleError)
background.js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
    console.log("Message received " + message.key);
    sendResponse({ response: "Response from background script" });
});

Background → Content

background.js
function onError(error) {
  console.error(`Error: ${error}`);
}

function sendMessageToTabs(tabs) {
  for (let tab of tabs) {
    browser.tabs.sendMessage(
      tab.id,
      {key: "value"}
    ).then(response => {
      console.log("Message from the content script:");
      console.log(response.response);
    }).catch(onError);
  }
}

browser.tabs.query({
  currentWindow: true,
  active: true
}).then(sendMessageToTabs).catch(onError);
content.js
browser.runtime.onMessage.addListener(message => {
    console.log("Message received " + message.key);
    return Promise.resolve({response: "Hi from content script"});
});

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

Js.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

Js.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

Js.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.runtime.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);
});

Context menu items

manifest.json
"permissions": ["contextMenus"]
background.js
browser.contextMenus.create({
  id: "test",
  title: "test"
});

browser.contextMenus.onClicked.addListener(function(info, tab) {
  switch (info.menuItemId) {
    case "test":
      console.log("test menu item clicked !");
      break;
    ...
  }
})

Notifications

background.js
var notificationId = "my-notification"
// Create and display a basic notification
browser.notifications.create(notificationId, {
    "type": "basic",
    "iconUrl": browser.runtime.getURL("icons/icon-96.png"),
    "title": "Title!",
    "message": "Message"
});

Page action

A page action is a clickable icon inside the browser's address bar.

manifest.json
"page_action": {
  "browser_style": true,
  "default_icon": {
    "19": "icons/icon-19.png",
    "38": "icons/icon-38.png"
  },
  "default_title": "Tooltip!",
  "show_matches": [
    "https://www.domain.net/*"
  ]
}
background.js
browser.pageAction.onClicked.addListener(function() {
  browser.tabs.create({
    url:"http://www.domain.net"
  });
});

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.svg
# depuis le dossier contenant les fichiers sources
web-ext run

# utiliser un profile existant
web-ext run --firefox-profile=MyProfile
web-ext run --firefox-profile=~/.mozilla/firefox/MyProfile

Installation

Bash.svg
# pacman
sudo pacman -S web-ext

# npm
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)

Package your extension

Bash.svg
web-ext build
# web-ext-artifacts/my_extension-1.0.zip
# the tool automatically excludes files that are commonly unwanted in packages, such as .git files.
The generated .zip file won't work on Firefox without signing.

Sign your extension

Only signed extensions can be installed on Firefox.
  1. Developer Hub → Submit a New Add-on
  2. Once the extension has been validated, an email is sent with a link to the extension versions list
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 → My Extension
  2. View all → Delete Add-on