mirror of
https://github.com/mdn/webextensions-examples.git
synced 2026-04-22 01:02:54 +02:00
Add example using MV3 userScripts API (#576)
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
"use strict";
|
||||
|
||||
// This background.js file is responsible for observing the availability of the
|
||||
// userScripts API, and registering user scripts when needed.
|
||||
//
|
||||
// - The runtime.onInstalled event is used to detect new installations, and
|
||||
// opens a custom extension UI where the user is asked to grant the
|
||||
// "userScripts" permission.
|
||||
//
|
||||
// - The permissions.onAdded and permissions.onRemoved events detect changes to
|
||||
// the "userScripts" permission, whether triggered from the extension UI, or
|
||||
// externally (e.g., through browser UI).
|
||||
//
|
||||
// - The storage.local API is used to store user scripts across extension
|
||||
// updates. This is necessary because the userScripts API clears any
|
||||
// previously registered scripts when an extension is updated.
|
||||
//
|
||||
// - The userScripts API manages script registrations with the browser. The
|
||||
// applyUserScripts() function in this file demonstrates the relevant aspects
|
||||
// to registering and updating user scripts that apply to most extensions
|
||||
// that manage user scripts. To keep this file reasonably small, most of the
|
||||
// application-specific logic is in userscript_manager_logic.mjs.
|
||||
|
||||
function isUserScriptsAPIAvailable() {
|
||||
return !!browser.userScripts;
|
||||
}
|
||||
var userScriptsAvailableAtStartup = isUserScriptsAPIAvailable();
|
||||
|
||||
var managerLogic; // Lazily initialized by ensureManagerLogicLoaded().
|
||||
async function ensureManagerLogicLoaded() {
|
||||
if (!managerLogic) {
|
||||
managerLogic = await import("./userscript_manager_logic.mjs");
|
||||
}
|
||||
}
|
||||
|
||||
browser.runtime.onInstalled.addListener(details => {
|
||||
if (details.reason !== "install") {
|
||||
// Only show the extension's onboarding logic on extension installation,
|
||||
// and not, e.g., on browser or extension updates.
|
||||
return;
|
||||
}
|
||||
if (!isUserScriptsAPIAvailable()) {
|
||||
// The extension needs the "userScripts" permission, but this is not
|
||||
// granted. Open the extension's options_ui page, which implements
|
||||
// onboarding logic, in options.html + options.mjs.
|
||||
browser.runtime.openOptionsPage();
|
||||
}
|
||||
});
|
||||
|
||||
browser.permissions.onRemoved.addListener(permissions => {
|
||||
if (permissions.permissions.includes("userScripts")) {
|
||||
// Pretend that userScripts is not available, to enable permissions.onAdded
|
||||
// to re-initialize when the permission is restored.
|
||||
userScriptsAvailableAtStartup = false;
|
||||
|
||||
// Clear the cached state, so that ensureUserScriptsRegistered() refreshes
|
||||
// the registered user scripts when the permission is granted again.
|
||||
browser.storage.session.remove("didInitScripts");
|
||||
|
||||
// Note: the "userScripts" namespace is unavailable, so we cannot and
|
||||
// should not try to unregister scripts.
|
||||
}
|
||||
});
|
||||
|
||||
browser.permissions.onAdded.addListener(permissions => {
|
||||
if (permissions.permissions.includes("userScripts")) {
|
||||
if (userScriptsAvailableAtStartup) {
|
||||
// If background.js woke up to dispatch permissions.onAdded, it has
|
||||
// detected the availability of the userScripts API and immediately
|
||||
// started initialization. Return now to avoid double-initialization.
|
||||
return;
|
||||
}
|
||||
browser.runtime.onUserScriptMessage.addListener(onUserScriptMessage);
|
||||
ensureUserScriptsRegistered();
|
||||
}
|
||||
});
|
||||
|
||||
// When the user modifies a user script in options.html + options.mjs, the
|
||||
// changes are stored in storage.local and this listener is triggered.
|
||||
browser.storage.local.onChanged.addListener(changes => {
|
||||
if (changes.savedScripts?.newValue && isUserScriptsAPIAvailable()) {
|
||||
// userScripts API is available and there are changes that can be applied.
|
||||
applyUserScripts(changes.savedScripts.newValue);
|
||||
}
|
||||
});
|
||||
|
||||
if (userScriptsAvailableAtStartup) {
|
||||
// Register listener immediately if the API is available, in case the
|
||||
// background.js is woken to dispatch the onUserScriptMessage event.
|
||||
browser.runtime.onUserScriptMessage.addListener(onUserScriptMessage);
|
||||
ensureUserScriptsRegistered();
|
||||
}
|
||||
|
||||
async function onUserScriptMessage(message, sender) {
|
||||
await ensureManagerLogicLoaded();
|
||||
return managerLogic.handleUserScriptMessage(message, sender);
|
||||
}
|
||||
|
||||
async function ensureUserScriptsRegistered() {
|
||||
let { didInitScripts } = await browser.storage.session.get("didInitScripts");
|
||||
if (didInitScripts) {
|
||||
// The scripts are initialized, e.g., by a (previous) startup of this
|
||||
// background script. Skip expensive initialization.
|
||||
return;
|
||||
}
|
||||
let { savedScripts } = await browser.storage.local.get("savedScripts");
|
||||
savedScripts ||= [];
|
||||
try {
|
||||
await applyUserScripts(savedScripts);
|
||||
} finally {
|
||||
// Set a flag to mark the completion of initialization, to avoid running
|
||||
// this logic again at the next startup of this background.js script.
|
||||
await browser.storage.session.set({ didInitScripts: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function applyUserScripts(userScriptTexts) {
|
||||
await ensureManagerLogicLoaded();
|
||||
// Note: assumes userScriptTexts to be valid, validated by options.mjs.
|
||||
let scripts = userScriptTexts.map(str => managerLogic.parseUserScript(str));
|
||||
|
||||
// Registering scripts is expensive. Compare the scripts with the old scripts
|
||||
// to ensure that only modified scripts are updated.
|
||||
let oldScripts = await browser.userScripts.getScripts();
|
||||
|
||||
let {
|
||||
scriptIdsToRemove,
|
||||
scriptsToUpdate,
|
||||
scriptsToRegister,
|
||||
} = managerLogic.computeScriptDifferences(oldScripts, scripts);
|
||||
|
||||
// Now, for the changed scripts, apply the changes in this order:
|
||||
// 1. Unregister obsolete scripts.
|
||||
// 2. Reset or configure worlds.
|
||||
// 3. Update and/or register new scripts.
|
||||
// This order is significant: scripts rely on world configurations, and while
|
||||
// running this asynchronous script updating logic, the browser may try to
|
||||
// execute any of the registered scripts when a website loads in a tab or
|
||||
// iframe, unrelated to the extension execution.
|
||||
// To prevent scripts from executing with the wrong world configuration,
|
||||
// worlds are configured before new scripts are registered.
|
||||
|
||||
// 1. Unregister obsolete scripts.
|
||||
if (scriptIdsToRemove.length) {
|
||||
await browser.userScripts.unregister({ worldIds: scriptIdsToRemove });
|
||||
}
|
||||
|
||||
// 2. Reset or configure worlds.
|
||||
if (scripts.some(s => s.worldId)) {
|
||||
// When userscripts need privileged functionality, run them in a sandbox
|
||||
// (USER_SCRIPT world). To offer privileged functionality, we need
|
||||
// a communication channel between the userscript and this privileged side.
|
||||
// Specifying "messaging:true" exposes runtime.sendMessage() these worlds,
|
||||
// which upon invocation triggers the runtime.onUserScriptMessage event.
|
||||
//
|
||||
// Calling configureWorld without a specific worldId sets the default world
|
||||
// configuration, which is inherit by every other USER_SCRIPT world that
|
||||
// does not have a more specific configuration.
|
||||
//
|
||||
// Since every USER_SCRIPT world in this demo extension has the same world
|
||||
// configuration, we can set the default once, without needing to define
|
||||
// world-specific configurations.
|
||||
await browser.userScripts.configureWorld({ messaging: true });
|
||||
} else {
|
||||
// Reset the default world's configuration.
|
||||
await browser.userScripts.resetWorldConfiguration();
|
||||
}
|
||||
|
||||
// 3. Update and/or register new scripts.
|
||||
if (scriptsToUpdate.length) {
|
||||
await browser.userScripts.update(scriptsToUpdate);
|
||||
}
|
||||
if (scriptsToRegister.length) {
|
||||
await browser.userScripts.register(scriptsToRegister);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user