mirror of
https://github.com/mdn/webextensions-examples.git
synced 2026-04-16 06:18:35 +02:00
177 lines
6.0 KiB
JavaScript
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();
|