Files
webextensions-examples/userScripts-mv3/options.mjs
2025-02-24 12:13:40 +13:00

177 lines
6.0 KiB
JavaScript

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();