mirror of
https://github.com/mdn/webextensions-examples.git
synced 2026-04-20 16:22:52 +02:00
Add example using MV3 userScripts API (#576)
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user