mirror of
https://github.com/mdn/webextensions-examples.git
synced 2026-04-16 06:18:35 +02:00
Add example using MV3 userScripts API (#576)
This commit is contained in:
@@ -567,7 +567,7 @@
|
|||||||
"name": "user-agent-rewriter"
|
"name": "user-agent-rewriter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Illustrates how an extension can register URL-matching user scripts at runtime.",
|
"description": "Illustrates how an extension can register URL-matching user scripts at runtime (Manifest Version 2 only).",
|
||||||
"javascript_apis": [
|
"javascript_apis": [
|
||||||
"userScripts.register",
|
"userScripts.register",
|
||||||
"runtime.onMessage",
|
"runtime.onMessage",
|
||||||
@@ -575,6 +575,28 @@
|
|||||||
],
|
],
|
||||||
"name": "user-script-register"
|
"name": "user-script-register"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "A user script manager demonstrating the userScripts API, permissions API, optional_permissions, and Manifest Version 3 (MV3).",
|
||||||
|
"javascript_apis": [
|
||||||
|
"userScripts.configureWorld",
|
||||||
|
"userScripts.getScripts",
|
||||||
|
"userScripts.register",
|
||||||
|
"userScripts.resetWorldConfiguration",
|
||||||
|
"userScripts.unregister",
|
||||||
|
"userScripts.update",
|
||||||
|
"permissions.onAdded",
|
||||||
|
"permissions.onRemoved",
|
||||||
|
"permissions.request",
|
||||||
|
"runtime.onInstalled",
|
||||||
|
"runtime.onUserScriptMessage",
|
||||||
|
"runtime.openOptionsPage",
|
||||||
|
"runtime.sendMessage",
|
||||||
|
"storage.local",
|
||||||
|
"storage.onChanged",
|
||||||
|
"storage.session"
|
||||||
|
],
|
||||||
|
"name": "userScripts-mv3"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Demonstrates how to use webpack to package npm modules in an extension.",
|
"description": "Demonstrates how to use webpack to package npm modules in an extension.",
|
||||||
"javascript_apis": ["runtime.onMessage", "runtime.sendMessage"],
|
"javascript_apis": ["runtime.onMessage", "runtime.sendMessage"],
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# User script registration
|
# User script registration
|
||||||
|
|
||||||
This extension demonstrates the [`browser.userScripts.register()`](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/Register) API.
|
This extension demonstrates the [legacy `browser.userScripts.register()`](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts_legacy/register) API, available to Manifest Version 2 extensions only.
|
||||||
|
|
||||||
|
> NOTE: See [userScripts-mv3](../userScripts-mv3/) for an example of the cross-browser userScripts API for Manifest Version 3.
|
||||||
|
|
||||||
The extension includes an [API script](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/user_scripts) (`customUserScriptAPIs.js`) that enables user scripts to make use of `browser.storage.local`.
|
The extension includes an [API script](https://wiki.developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/user_scripts) (`customUserScriptAPIs.js`) that enables user scripts to make use of `browser.storage.local`.
|
||||||
|
|
||||||
|
|||||||
10
userScripts-mv3/.eslintrc.json
Normal file
10
userScripts-mv3/.eslintrc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.mjs"],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
130
userScripts-mv3/README.md
Normal file
130
userScripts-mv3/README.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# userScripts-mv3
|
||||||
|
|
||||||
|
The extension is an example of a
|
||||||
|
[user script manager](https://en.wikipedia.org/wiki/Userscript_manager). It
|
||||||
|
The extension is an example of a [user script
|
||||||
|
manager](https://en.wikipedia.org/wiki/Userscript_manager). It demonstrates the
|
||||||
|
`userScripts` API, the `permissions` API, `optional_permissions`, and Manifest
|
||||||
|
Version 3 (MV3).
|
||||||
|
and Manifest Version 3 (MV3).
|
||||||
|
|
||||||
|
This example demonstrates these aspects of extension development:
|
||||||
|
|
||||||
|
- Showing an onboarding UI after installation.
|
||||||
|
|
||||||
|
- Designing background scripts that can restart repeatedly with minimal
|
||||||
|
overhead. This is especially relevant to Manifest Version 3.
|
||||||
|
|
||||||
|
- Minimizing the overhead of background script startup. This is relevant because
|
||||||
|
Manifest Version 3 extensions use an event-based background context.
|
||||||
|
|
||||||
|
- Monitoring grants for an
|
||||||
|
[optional-only](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/optional_permissions#optional-only_permissions)
|
||||||
|
permission (`"userScripts"`), and dynamically registering events and scripts
|
||||||
|
based on its availability.
|
||||||
|
|
||||||
|
- Using the `userScripts` API to register, update, and unregister user script
|
||||||
|
code.
|
||||||
|
|
||||||
|
- Isolating user scripts in individual execution contexts (`USER_SCRIPT`
|
||||||
|
world), and conditionally exposing custom functions to user scripts.
|
||||||
|
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
After loading, the extension detects the new installation and opens the options
|
||||||
|
page embedded in `about:addons`. On the options page:
|
||||||
|
|
||||||
|
1. Click "Grant access to userScripts API" to trigger a permission prompt for
|
||||||
|
the "userScripts" permission.
|
||||||
|
2. Click "Add new user script" to open a form where a new script can be
|
||||||
|
registered.
|
||||||
|
3. Input a user script, by clicking one of the "Example" buttons and input a
|
||||||
|
example from the [userscript_examples](userscript_examples) directory.
|
||||||
|
4. Click "Save" to trigger validation and save the script.
|
||||||
|
|
||||||
|
If the "userScripts" permission is granted, this schedules the execution of the
|
||||||
|
registered user scripts for the websites specified in each user script.
|
||||||
|
|
||||||
|
See [userscript_examples](userscript_examples) for examples of user scripts and
|
||||||
|
what they do.
|
||||||
|
|
||||||
|
If you repeat steps 2-4 for both examples and then visit https://example.com/,
|
||||||
|
you should see this behavior:
|
||||||
|
|
||||||
|
- Show a dialog containing "This is a demo of a user script".
|
||||||
|
- Insert a button with the label "Show user script info", which opens a new tab
|
||||||
|
displaying the extension information.
|
||||||
|
|
||||||
|
# What it shows
|
||||||
|
|
||||||
|
Showing onboarding UI after installation:
|
||||||
|
|
||||||
|
- `background.js` registers the `runtime.onInstalled` listener that calls
|
||||||
|
`runtime.openOptionsPage` after installation.
|
||||||
|
|
||||||
|
Designing background scripts that can restart repeatedly with minimal overhead:
|
||||||
|
|
||||||
|
- This is particularly relevant to Manifest Version 3, because in MV3
|
||||||
|
background scripts are always non-persistent and can suspend on inactivity.
|
||||||
|
- Using `storage.session` to store initialization status, to run expensive
|
||||||
|
initialization only once per browser session.
|
||||||
|
- Registering events at the top level to handle events that are triggered while
|
||||||
|
the background script is asleep.
|
||||||
|
- Using [dynamic import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)
|
||||||
|
to initialize optional JavaScript modules on demand.
|
||||||
|
|
||||||
|
Monitoring an optional permission (`userScripts`), and dynamically registering
|
||||||
|
events and scripts based on its availability:
|
||||||
|
events and scripts based on its availability:
|
||||||
|
|
||||||
|
- The `userScripts` permission is optional and can be granted by the user from:
|
||||||
|
- the options page (`options.html` + `options.mjs`).
|
||||||
|
- the browser UI (where the user can also revoke the permission). See the
|
||||||
|
Mozilla support article [Manage optional permissions for Firefox extensions](https://support.mozilla.org/en-US/kb/manage-optional-permissions-extensions).
|
||||||
|
|
||||||
|
- The `permissions.onAdded` and `permissions.onRemoved` events are used to
|
||||||
|
monitor permission changes and, therefore, the availability of the
|
||||||
|
`userScripts` API.
|
||||||
|
|
||||||
|
- When the `userScripts` API is available when `background.js` starts or
|
||||||
|
`permissions.onAdded` detects that permission has been granted,
|
||||||
|
initialization starts (using the `ensureUserScriptsRegistered` function in
|
||||||
|
`background.js`).
|
||||||
|
|
||||||
|
- When the `userScripts` API is unavailable when `background.js` starts,
|
||||||
|
the extension cannot use the `userScripts` API until `permissions.onAdded` is
|
||||||
|
triggered. The options page stores user scripts in `storage.local` to enable
|
||||||
|
the user to edit scripts even without the `userScripts` permission.
|
||||||
|
|
||||||
|
Using the `userScripts` API to register, update, and unregister code:
|
||||||
|
|
||||||
|
- The `applyUserScripts()` function in `background.js` demonstrates how to use
|
||||||
|
the various `userScripts` APIs to register, update, and unregister scripts.
|
||||||
|
- `userscript_manager_logic.mjs` contains logic specific to user script
|
||||||
|
managers. See [`userscript_manager_logic.mjs`](userscript_manager_logic.mjs)
|
||||||
|
for comments and the conversion logic from a user script string to the format
|
||||||
|
expected by the `userScripts` API
|
||||||
|
([RegisteredUserScript](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts/RegisteredUserScript)).
|
||||||
|
|
||||||
|
Isolating user scripts in individual execution contexts (`USER_SCRIPT` world),
|
||||||
|
and conditionally exposing custom functions to user scripts:
|
||||||
|
|
||||||
|
- Shows the use of `USER_SCRIPT` worlds (with distinct `worldId`) to
|
||||||
|
define sandboxes for scripts to run in (see `registeredUserScript`
|
||||||
|
in `userscript_manager_logic.mjs`).
|
||||||
|
|
||||||
|
- Shows the use of `userScripts.configureWorld()` with the `messaging` flag to
|
||||||
|
enable the `runtime.sendMessage()` method in `USER_SCRIPT` worlds.
|
||||||
|
|
||||||
|
- Shows the use of `runtime.onUserScriptMessage` and `sender.userScriptWorldId`
|
||||||
|
to detect messages and the script that sent messages.
|
||||||
|
|
||||||
|
- Shows how an initial script can use `runtime.sendMessage` to expose custom
|
||||||
|
APIs to user scripts (see `userscript_api.js`).
|
||||||
|
|
||||||
|
# Feature availability
|
||||||
|
|
||||||
|
The `userScripts` API is available from Firefox 136. In Firefox 134 and 135, the
|
||||||
|
functionality is only available if the `extensions.userScripts.mv3.enabled`
|
||||||
|
preference is set to `true` at `about:config` before installing the extension.
|
||||||
176
userScripts-mv3/background.js
Normal file
176
userScripts-mv3/background.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
userScripts-mv3/manifest.json
Normal file
21
userScripts-mv3/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "User Scripts Manager extension",
|
||||||
|
"description": "Demonstrates the userScripts API and optional permission, in MV3.",
|
||||||
|
"version": "0.1",
|
||||||
|
"host_permissions": ["*://*/"],
|
||||||
|
"permissions": ["storage", "unlimitedStorage"],
|
||||||
|
"optional_permissions": ["userScripts"],
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background.js"]
|
||||||
|
},
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options.html"
|
||||||
|
},
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "user-script-manager-example@mozilla.org",
|
||||||
|
"strict_min_version": "134.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
userScripts-mv3/options.css
Normal file
5
userScripts-mv3/options.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#edit_script_dialog .source_text {
|
||||||
|
display: block;
|
||||||
|
width: 80vw;
|
||||||
|
min-height: 10em;
|
||||||
|
}
|
||||||
32
userScripts-mv3/options.html
Normal file
32
userScripts-mv3/options.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width"> <!-- mobile-friendly -->
|
||||||
|
<meta name="color-scheme" content="dark light"><!-- Dark theme support -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="options.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
This page enables you to create, edit and remove user scripts.
|
||||||
|
To run them, please allow the extension to run user scripts by clicking this button:
|
||||||
|
<button id="grant_userScripts_permission"></button>
|
||||||
|
|
||||||
|
<dialog id="edit_script_dialog">
|
||||||
|
<div>
|
||||||
|
Please input a user script and save it.<br>
|
||||||
|
<button id="sample_unprivileged">Example: Unprivileged user script</button>
|
||||||
|
<button id="sample_privileged">Example: Privileged user script</button>
|
||||||
|
</div>
|
||||||
|
<textarea class="source_text"></textarea>
|
||||||
|
<button class="save_button">Save</button>
|
||||||
|
<button class="remove_button">Remove</button>
|
||||||
|
<output class="validation_status"></output>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<ul id="list_of_scripts">
|
||||||
|
<li><button id="add_new">Add new user script</button></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<script src="options.mjs" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
176
userScripts-mv3/options.mjs
Normal file
176
userScripts-mv3/options.mjs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { parseUserScript } from "./userscript_manager_logic.mjs";
|
||||||
|
|
||||||
|
function initializePrefHandlerForUserScriptsPermissions() {
|
||||||
|
const PERM = "userScripts";
|
||||||
|
const button = document.getElementById("grant_userScripts_permission");
|
||||||
|
function renderPermStatus(granted) {
|
||||||
|
if (granted) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = "userScripts permission has been granted";
|
||||||
|
} else {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = "Grant access to userScripts API";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.onclick = async () => {
|
||||||
|
button.disabled = true; // Avoid double-click.
|
||||||
|
button.textContent = "Showing userScripts permission request...";
|
||||||
|
let ok = await browser.permissions.request({ permissions: [PERM] });
|
||||||
|
renderPermStatus(ok);
|
||||||
|
};
|
||||||
|
|
||||||
|
browser.permissions.onAdded.addListener(permissions => {
|
||||||
|
if (permissions.permissions.includes(PERM)) {
|
||||||
|
renderPermStatus(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.permissions.onRemoved.addListener(permissions => {
|
||||||
|
if (permissions.permissions.includes(PERM)) {
|
||||||
|
renderPermStatus(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.permissions.contains({ permissions: [PERM] }).then(renderPermStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidMatchPattern(str) {
|
||||||
|
// This is a bit stricter than what browsers consider a valid match pattern,
|
||||||
|
// but userscripts usually run on http(s) only.
|
||||||
|
return /^(https?|\*):\/\/(\*|(\*\.)?[^*/]+)\/.*$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the form where the user can edit or create a user script.
|
||||||
|
*
|
||||||
|
* @param {string} userScriptText - Non-empty if editing an existing script.
|
||||||
|
*/
|
||||||
|
function showEditDialog(userScriptText) {
|
||||||
|
const edit_script_dialog = document.getElementById("edit_script_dialog");
|
||||||
|
const textarea = edit_script_dialog.querySelector("textarea.source_text");
|
||||||
|
const saveButton = edit_script_dialog.querySelector("button.save_button");
|
||||||
|
const removeButton = edit_script_dialog.querySelector("button.remove_button");
|
||||||
|
const outputStatus = edit_script_dialog.querySelector("output");
|
||||||
|
|
||||||
|
textarea.value = userScriptText;
|
||||||
|
|
||||||
|
outputStatus.value = "";
|
||||||
|
|
||||||
|
saveButton.disabled = false;
|
||||||
|
saveButton.onclick = async () => {
|
||||||
|
saveButton.disabled = true;
|
||||||
|
try {
|
||||||
|
let savedScripts =
|
||||||
|
await getScriptsToSaveIfValid(userScriptText, textarea.value);
|
||||||
|
outputStatus.value = "Applying...";
|
||||||
|
await browser.storage.local.set({ savedScripts });
|
||||||
|
await renderCurrentScripts();
|
||||||
|
edit_script_dialog.close();
|
||||||
|
} catch (e) {
|
||||||
|
outputStatus.value = e.message;
|
||||||
|
} finally {
|
||||||
|
saveButton.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
removeButton.hidden = !userScriptText;
|
||||||
|
removeButton.onclick = async () => {
|
||||||
|
if (confirm("Do you want to remove this script?")) {
|
||||||
|
let { savedScripts } = await browser.storage.local.get("savedScripts");
|
||||||
|
savedScripts = savedScripts.filter(txt => txt !== userScriptText);
|
||||||
|
await browser.storage.local.set({ savedScripts });
|
||||||
|
await renderCurrentScripts();
|
||||||
|
edit_script_dialog.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function suggestExample(sourceUrl) {
|
||||||
|
if (textarea.value && !confirm("Input is not empty. Proceed to replace?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let res = await fetch(sourceUrl);
|
||||||
|
textarea.value = await res.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_script_dialog.querySelector("#sample_privileged").onclick = () => {
|
||||||
|
suggestExample("userscript_examples/privileged.user.js");
|
||||||
|
};
|
||||||
|
edit_script_dialog.querySelector("#sample_unprivileged").onclick = () => {
|
||||||
|
suggestExample("userscript_examples/unprivileged.user.js");
|
||||||
|
};
|
||||||
|
|
||||||
|
edit_script_dialog.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScriptsToSaveIfValid(oldUserScriptText, newUserScriptText) {
|
||||||
|
let newScript = parseUserScript(newUserScriptText);
|
||||||
|
if (!newScript) {
|
||||||
|
throw new Error("Input is not a user script, missing header");
|
||||||
|
}
|
||||||
|
if (!newScript.matches?.length && !newScript.includeGlobs?.length) {
|
||||||
|
throw new Error("At least one @include or @match must be specified");
|
||||||
|
}
|
||||||
|
for (let pattern of newScript.matches || []) {
|
||||||
|
if (!isValidMatchPattern(pattern)) {
|
||||||
|
throw new Error(`Invalid match pattern: @match ${pattern}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let pattern of newScript.excludeMatches || []) {
|
||||||
|
if (!isValidMatchPattern(pattern)) {
|
||||||
|
throw new Error(`Invalid match pattern: @exclude-match ${pattern}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let { savedScripts } = await browser.storage.local.get("savedScripts");
|
||||||
|
savedScripts ||= [];
|
||||||
|
if (oldUserScriptText) {
|
||||||
|
let i = savedScripts.indexOf(oldUserScriptText);
|
||||||
|
if (i === -1) {
|
||||||
|
// This is unexpected, but could happen if the user opened the options
|
||||||
|
// page in a different tab and continued modifying scripts there.
|
||||||
|
throw new Error(
|
||||||
|
"Cannot find old script to replace. Did you modify the script in another tab?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
savedScripts[i] = newUserScriptText;
|
||||||
|
} else {
|
||||||
|
savedScripts.push(newUserScriptText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Script IDs must be unique. The storage should contain valid scripts only,
|
||||||
|
// so getting too many IDs implies that the proposed ID was duplicated.
|
||||||
|
let seenIds = savedScripts.map(s => parseUserScript(s).id);
|
||||||
|
if (seenIds.filter(id => id === newScript.id).length > 1) {
|
||||||
|
throw new Error("@namespace + @name must not be used elsewhere");
|
||||||
|
}
|
||||||
|
|
||||||
|
// All valid! Return the scripts to save.
|
||||||
|
return savedScripts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderCurrentScripts() {
|
||||||
|
const outputList = document.getElementById("list_of_scripts");
|
||||||
|
outputList.querySelector("#add_new").onclick = () => {
|
||||||
|
showEditDialog("");
|
||||||
|
};
|
||||||
|
// Clear any previously rendered results, except for the "#add_new" button.
|
||||||
|
outputList.replaceChildren(outputList.firstElementChild);
|
||||||
|
|
||||||
|
let { savedScripts } = await browser.storage.local.get("savedScripts");
|
||||||
|
savedScripts ||= [];
|
||||||
|
for (let userScriptText of savedScripts) {
|
||||||
|
let registeredUserScript = parseUserScript(userScriptText);
|
||||||
|
|
||||||
|
let editButton = document.createElement("button");
|
||||||
|
editButton.textContent = "Edit";
|
||||||
|
editButton.onclick = () => {
|
||||||
|
showEditDialog(userScriptText);
|
||||||
|
};
|
||||||
|
let li = document.createElement("li");
|
||||||
|
li.append(editButton, `Script ID: ${registeredUserScript.id}`);
|
||||||
|
outputList.append(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializePrefHandlerForUserScriptsPermissions();
|
||||||
|
renderCurrentScripts();
|
||||||
50
userScripts-mv3/userscript_api.js
Normal file
50
userScripts-mv3/userscript_api.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
// userscript_api.js defines a single function that generates the global APIs
|
||||||
|
// that should be made available to scripts running in this USER_SCRIPT sandbox.
|
||||||
|
// This script is scheduled to run before user scripts by the parseUserScript
|
||||||
|
// function in userscript_manager_logic.mjs
|
||||||
|
|
||||||
|
globalThis.initCustomAPIForUserScripts = grants => {
|
||||||
|
// Allow initialization only once.
|
||||||
|
delete globalThis.initCustomAPIForUserScripts;
|
||||||
|
|
||||||
|
// background.js calls userScripts.configureWorld({ messaging: true }), which
|
||||||
|
// exposes runtime.sendMessage here. When this method is called, the
|
||||||
|
// runtime.onUserScriptMessage listener (in background.js) is called, which
|
||||||
|
// in turn calls handleUserScriptMessage in userscript_manager_logic.mjs.
|
||||||
|
const sendMessage = browser.runtime.sendMessage;
|
||||||
|
|
||||||
|
// Clear access to privileged API to prevent userscripts from communicating
|
||||||
|
// to the privileged backend.
|
||||||
|
globalThis.browser = undefined;
|
||||||
|
|
||||||
|
if (grants.includes("GM_info")) {
|
||||||
|
// Example of an API that retrieves information:
|
||||||
|
// https://www.tampermonkey.net/documentation.php#api:GM_info
|
||||||
|
// https://violentmonkey.github.io/api/gm/#gm_info
|
||||||
|
// https://wiki.greasespot.net/GM.info
|
||||||
|
// NOTE: The following implementation of GM_info is async to demonstrate
|
||||||
|
// how one can retrieve information on demand. The actual GM_info function
|
||||||
|
// as defined by full-featured user script managers is synchronous.
|
||||||
|
globalThis.GM_info = async () => {
|
||||||
|
return sendMessage({ userscript_api_name: "GM_info" });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (grants.includes("GM_openInTab")) {
|
||||||
|
// Example of an API that sends information:
|
||||||
|
// https://www.tampermonkey.net/documentation.php#api:GM_openInTab
|
||||||
|
// https://violentmonkey.github.io/api/gm/#gm_openintab
|
||||||
|
// https://wiki.greasespot.net/GM.openInTab
|
||||||
|
globalThis.GM_openInTab = async (url) => {
|
||||||
|
await sendMessage({ userscript_api_name: "GM_openInTab", args: [url] });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement more APIs.
|
||||||
|
|
||||||
|
// After this function returns, the userscript may execute and potentially
|
||||||
|
// change built-in prototypes. To protect against that, make sure to store
|
||||||
|
// any functions that you want to use in a local variable!
|
||||||
|
};
|
||||||
32
userScripts-mv3/userscript_examples/privileged.user.js
Normal file
32
userScripts-mv3/userscript_examples/privileged.user.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name Demo of privileged user script
|
||||||
|
// @description Add button on domains starting with "example" that displays privileged info in a new tab.
|
||||||
|
// @include https://example*
|
||||||
|
// @include http://example*
|
||||||
|
// @grant GM_info
|
||||||
|
// @grant GM_openInTab
|
||||||
|
// @version 1.2.3
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
// To test:
|
||||||
|
// 1. Visit https://example.com/ or http://example.org/
|
||||||
|
// 2. Click on the "Show user script info" button.
|
||||||
|
// 2. Confirm that a new tab opens that displays the script info.
|
||||||
|
|
||||||
|
/* globals GM_info, GM_openInTab */
|
||||||
|
|
||||||
|
if (location.pathname === "/display_userscript_result") {
|
||||||
|
document.body.style.whiteSpace = "pre-wrap";
|
||||||
|
document.body.textContent = decodeURIComponent(location.search.slice(1));
|
||||||
|
} else {
|
||||||
|
let button = document.createElement("button");
|
||||||
|
button.textContent = "Show user script info";
|
||||||
|
document.body.prepend(button);
|
||||||
|
|
||||||
|
button.onclick = async () => {
|
||||||
|
let info = await GM_info();
|
||||||
|
let text = `Result from user script:\n\n${JSON.stringify(info, null, 4)}`;
|
||||||
|
let url = "https://example.com/display_userscript_result?" + encodeURIComponent(text);
|
||||||
|
GM_openInTab(url);
|
||||||
|
};
|
||||||
|
}
|
||||||
20
userScripts-mv3/userscript_examples/unprivileged.user.js
Normal file
20
userScripts-mv3/userscript_examples/unprivileged.user.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name Demo of unprivileged user script
|
||||||
|
// @description Show dialog on MDN and every URL starting with "https://example".
|
||||||
|
// @match https://developer.mozilla.org/*
|
||||||
|
// @include https://example*
|
||||||
|
// @exclude-match https://example.com/display_userscript_result*
|
||||||
|
// @grant none
|
||||||
|
// @version 1.2.3
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
// To test:
|
||||||
|
// 1. Visit https://example.com/ or https://developer.mozilla.org/
|
||||||
|
// 2. Confirm that a dialog shows up.
|
||||||
|
|
||||||
|
alert(`This is a demo of a user script, running at ${document.URL}.`);
|
||||||
|
|
||||||
|
// This user script should not get access to privileged APIs.
|
||||||
|
if (typeof GM_info !== "undefined") {
|
||||||
|
alert("Unexpectedly, GM_info is defined...?");
|
||||||
|
}
|
||||||
240
userScripts-mv3/userscript_manager_logic.mjs
Normal file
240
userScripts-mv3/userscript_manager_logic.mjs
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// A userscript manager runs user-defined scripts (called userscripts) on
|
||||||
|
// websites based on the metadata that is encoded in the userscript source text.
|
||||||
|
// For history, see https://en.wikipedia.org/wiki/Userscript
|
||||||
|
//
|
||||||
|
// This file provides a partial implementation of a userscript manager, and
|
||||||
|
// exports the following functions that are called by code in background.js:
|
||||||
|
//
|
||||||
|
// - parseUserScript() - parses userscript source text to a representation of
|
||||||
|
// the user script for use with the "browser.userScripts" extension API.
|
||||||
|
//
|
||||||
|
// - computeScriptDifferences() - compares two representations of a user script
|
||||||
|
// and returns whether they have changed. This is used for determining which
|
||||||
|
// scripts should be updated, when the actual registrations are compared with
|
||||||
|
// the scripts in the extension storage. As script registration is relatively
|
||||||
|
// expensive, this enables updating only the scripts that have changed.
|
||||||
|
//
|
||||||
|
// - handleUserScriptMessage() - Handles browser.runtime.onUserScriptMessage
|
||||||
|
// event, which is triggered when a script in the USER_SCRIPT world invokes
|
||||||
|
// the browser.runtime.sendMessage() method (see userscript_api.js).
|
||||||
|
|
||||||
|
export function parseUserScript(userScriptText) {
|
||||||
|
let metadata = parseMetadataBlock(userScriptText);
|
||||||
|
if (!metadata) {
|
||||||
|
console.warn("Ignoring non-userscript input");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create object for use with browser.userScripts.register():
|
||||||
|
let registeredUserScript = {
|
||||||
|
// The userScripts API requires each script to have a unique ID that does
|
||||||
|
// not start with "_". The userscript format specifies the identifier of
|
||||||
|
// a script is formed from @namespace and @name:
|
||||||
|
// https://wiki.greasespot.net/Metadata_Block#@name
|
||||||
|
// https://violentmonkey.github.io/api/metadata-block/#name
|
||||||
|
id: JSON.stringify([ metadata.get("@namespace"), metadata.get("@name") ]),
|
||||||
|
js: null, // js is a required array and will be set below.
|
||||||
|
// All of the following fields are optional.
|
||||||
|
allFrames: !metadata.has("@noframes"),
|
||||||
|
matches: metadata.getArray("@match"),
|
||||||
|
excludeMatches: metadata.getArray("@exclude-match"),
|
||||||
|
includeGlobs: metadata.getArray("@include"),
|
||||||
|
excludeGlobs: metadata.getArray("@exclude"),
|
||||||
|
runAt: parseRunAt(metadata.get("@run-at")),
|
||||||
|
world: null, // "MAIN" or "USER_SCRIPT", set below.
|
||||||
|
worldId: null, // Can only be set if world is "USER_SCRIPT", set below.
|
||||||
|
};
|
||||||
|
|
||||||
|
// See https://wiki.greasespot.net/@grant
|
||||||
|
// When "@grant" is used, the userscript requests access to extra APIs that
|
||||||
|
// are not available to regular web pages.
|
||||||
|
let grants = parseGrants(metadata.getArray("@grant"));
|
||||||
|
if (grants.length === 0) {
|
||||||
|
// When the userscript does not request any additional APIs, we only need
|
||||||
|
// to execute one script: the original userscript source code.
|
||||||
|
registeredUserScript.js = [{ code: userScriptText }];
|
||||||
|
// When no additional APIs are granted, it is not strictly necessary to
|
||||||
|
// isolate the code from the web page, and therefore we use the "MAIN"
|
||||||
|
// world.
|
||||||
|
registeredUserScript.world = "MAIN";
|
||||||
|
registeredUserScript.worldId = "";
|
||||||
|
} else {
|
||||||
|
// When the userscript defines "@grant", it requests access to several
|
||||||
|
// userscript-specific APIs. These are not provided by the browser, but
|
||||||
|
// the responsibility of the user script manager extension.
|
||||||
|
|
||||||
|
// See userscript_api.js for an explanation of this logic.
|
||||||
|
registeredUserScript.js = [
|
||||||
|
{ file: "userscript_api.js" },
|
||||||
|
// initCustomAPIForUserScripts is defined in userscript_api.js
|
||||||
|
{ code: `initCustomAPIForUserScripts(${JSON.stringify(grants)})`},
|
||||||
|
{ code: userScriptText },
|
||||||
|
];
|
||||||
|
|
||||||
|
// If extra APIs are requested, we need to define a sandbox that isolates
|
||||||
|
// the execution environment of the userscript from the web page, or else
|
||||||
|
// the page can try to interfere with the userscript, and at worst abuse
|
||||||
|
// privileged functionality.
|
||||||
|
registeredUserScript.world = "USER_SCRIPT";
|
||||||
|
// To isolate different userscript scripts from each other, we create a
|
||||||
|
// unique world for each user script. This enables us to effectively
|
||||||
|
// implement access control per script.
|
||||||
|
registeredUserScript.worldId = Math.random().toString();
|
||||||
|
}
|
||||||
|
return registeredUserScript;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMetadataBlock(userScriptText) {
|
||||||
|
// Parse userscript metadata block, which is in the following format:
|
||||||
|
// // ==UserScript==
|
||||||
|
// // @key value
|
||||||
|
// // ==/UserScript==
|
||||||
|
// See https://wiki.greasespot.net/Metadata_Block
|
||||||
|
let header = `\n${userScriptText}\n`.split("\n// ==UserScript==\n", 2)[1];
|
||||||
|
if (!header) {
|
||||||
|
console.error("UserScript header start not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
header = header.split("\n// ==/UserScript==\n")[0];
|
||||||
|
if (!header) {
|
||||||
|
console.error("UserScript header end not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let metadata = new Map();
|
||||||
|
for (let line of header.split("\n")) {
|
||||||
|
let match = /^\/\/ (@\S+)(\s+.*)?$/.exec(line);
|
||||||
|
if (!match) {
|
||||||
|
console.warn(`Skipping invalid UserScript header line: ${line}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let [, key, value] = match;
|
||||||
|
if (!metadata.has(key)) {
|
||||||
|
metadata.set(key, []);
|
||||||
|
}
|
||||||
|
metadata.get(key).push(value.trim());
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rawHeader: header,
|
||||||
|
has: key => metadata.has(key),
|
||||||
|
get: key => metadata.get(key)?.[0],
|
||||||
|
getArray: key => metadata.get(key) || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRunAt(runAtFromUserScriptMetadata) {
|
||||||
|
// Transforms some of the supported @run-at values to the values
|
||||||
|
// https://wiki.greasespot.net/Metadata_Block#.40run-at
|
||||||
|
// https://www.tampermonkey.net/documentation.php#meta:run_at
|
||||||
|
// https://violentmonkey.github.io/api/metadata-block/#run-at
|
||||||
|
switch (runAtFromUserScriptMetadata) {
|
||||||
|
case "document-start": return "document_start";
|
||||||
|
case "document-end": return "document_end";
|
||||||
|
case "document-idle": return "document_idle";
|
||||||
|
// Default if unspecified or not recognized. Some userscript managers
|
||||||
|
// support more values, the extension API only recognizes the above three.
|
||||||
|
default: return "document_idle";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameRegisteredUserScript(oldScript, newScript) {
|
||||||
|
// In general, to test whether two RegisteredUserScript are equal, we have to
|
||||||
|
// compare each property (and if undefined/null, use the default value).
|
||||||
|
//
|
||||||
|
// In this demo, the parseUserScripts function generated all of the script's
|
||||||
|
// properties from an input code string, which is also the last item of the
|
||||||
|
// "js" array. Comparing these is enough to test whether they are the same.
|
||||||
|
return oldScript.js.at(-1).code === newScript.js.at(-1).code;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeScriptDifferences(oldScripts, newScripts) {
|
||||||
|
let scriptIdsToRemove = [];
|
||||||
|
let scriptsToUpdate = [];
|
||||||
|
let scriptsToRegister = [];
|
||||||
|
|
||||||
|
for (let script of oldScripts) {
|
||||||
|
if (!newScripts.some(s => s.id === script.id)) {
|
||||||
|
// old script no longer exists. We should remove it.
|
||||||
|
scriptIdsToRemove.push(script.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let script of newScripts) {
|
||||||
|
let oldScript = oldScripts.find(s => s.id === script.id);
|
||||||
|
if (!oldScript) {
|
||||||
|
scriptsToRegister.push(script);
|
||||||
|
} else if (!isSameRegisteredUserScript(script, oldScript)) {
|
||||||
|
// Script was updated, remove old one and register new one.
|
||||||
|
scriptsToUpdate.push(script);
|
||||||
|
} else {
|
||||||
|
// oldScript is kept when we do not update or remove it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { scriptIdsToRemove, scriptsToUpdate, scriptsToRegister };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGrants(grantsFromUserScriptMetadata) {
|
||||||
|
// Userscripts may access privileged APIs as defined in:
|
||||||
|
// https://wiki.greasespot.net/@grant
|
||||||
|
// https://violentmonkey.github.io/api/metadata-block/#grant
|
||||||
|
// https://www.tampermonkey.net/documentation.php#meta:grant
|
||||||
|
let grants = [];
|
||||||
|
for (let grant of grantsFromUserScriptMetadata) {
|
||||||
|
if (grant === "none") {
|
||||||
|
// "@grant none" is equivalent to no grants.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Although there are many APIs, we only support a small subset in this
|
||||||
|
// demo. ee handleUserScriptMessage() below and userscript_api.js.
|
||||||
|
if (grant === "GM_info" || grant === "GM_openInTab") {
|
||||||
|
grants.push(grant);
|
||||||
|
} else {
|
||||||
|
console.warn(`@grant ${grant} is not implemented`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return grants;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleUserScriptMessage(message, sender) {
|
||||||
|
// This is the runtime.onUserScriptMessage handler that implements the
|
||||||
|
// privileged functionality to support functionality in userscript_api.js
|
||||||
|
|
||||||
|
// TODO: Validate that the message is allowed.
|
||||||
|
// sender.userScriptWorldId can be used to look up the world and the scripts
|
||||||
|
// that execute within.
|
||||||
|
|
||||||
|
if (message.userscript_api_name === "GM_openInTab") {
|
||||||
|
await browser.tabs.create({ url: message.args[0] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.userscript_api_name === "GM_info") {
|
||||||
|
// In parseUserScripts(), each generated script has a unique worldId, so if
|
||||||
|
// the script is still registered, we can discover the script registration.
|
||||||
|
// For simplicity, we extract the original userscript source text from it.
|
||||||
|
let scripts = await browser.userScripts.getScripts();
|
||||||
|
let script = scripts.find(s => s.worldId === sender.userScriptWorldId);
|
||||||
|
let userScriptText = script.js.at(-1).code;
|
||||||
|
|
||||||
|
// Minimal implementation of GM_info, based on:
|
||||||
|
// https://wiki.greasespot.net/GM.info
|
||||||
|
let metadata = parseMetadataBlock(userScriptText);
|
||||||
|
return {
|
||||||
|
script: {
|
||||||
|
description: metadata.get("@description"),
|
||||||
|
excludes: metadata.getArray("@exclude"),
|
||||||
|
includes: metadata.getArray("@include"),
|
||||||
|
matches: metadata.getArray("@match"),
|
||||||
|
name: metadata.get("@name"),
|
||||||
|
namespace: metadata.get("@namespace"),
|
||||||
|
"run-at": metadata.get("@run-at"),
|
||||||
|
version: metadata.get("@version"),
|
||||||
|
},
|
||||||
|
scriptMetaStr: metadata.rawHeader,
|
||||||
|
scriptHandler: "[Name of my Userscript Manager]",
|
||||||
|
version: browser.runtime.getManifest().version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Unexpected message", message);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user