Liens
Hierarchie des fichiers
- manifest.json
- main.js
- main.html
- icons
- content_scripts
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"
}
}
|
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"
]
}
|
manifest.json
|
{
"mySetting": "value"
}
|
background.js
|
var mySetting = browser.runtime.getManifest().mySetting;
|
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
|
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);
});
|
|
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; }
|
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/*" ]
|
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;
|
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.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"});
});
|
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"
}
|
manifest.json
|
"background": {
"scripts": ["background.js"]
}
|
background.js
|
function doSomething() { }
browser.browserAction.onClicked.addListener(doSomething);
|
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
|
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;
|
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 → { ... }
|
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
|
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
|
// 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>" ]
|
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);
});
|
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;
...
}
})
|
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"
});
|
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
|
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. |
|
# 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
|
# 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
- about:debugging
- Enable add-on debugging
- Load Temporary Add-on ou web-ext run
- Debug (ouvre une fenêtre Developer Tools)
Package your extension
|
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. |
- Developer Hub → Submit a New Add-on
- 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. |
- Manage My Submissions → My Extension
- View all → Delete Add-on