diff options
author | Wojtek Kosior <wk@koszkonutek-tmp.pl.eu.org> | 2021-05-10 18:07:05 +0200 |
---|---|---|
committer | Wojtek Kosior <wk@koszkonutek-tmp.pl.eu.org> | 2021-05-10 18:18:52 +0200 |
commit | 01937dc9d5215ef96ce756e3ccda51bf29032f58 (patch) | |
tree | 609ec5bb48c692796520f7982c06b30633038588 | |
download | browser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.tar.gz browser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.zip |
initial commit
35 files changed, 4082 insertions, 0 deletions
diff --git a/COPYING.txt b/COPYING.txt new file mode 100644 index 0000000..109c665 --- /dev/null +++ b/COPYING.txt @@ -0,0 +1,4 @@ +The extension can be redistributed under the terms of GPLv3+. +Certain parts are available under more permissive licenses. + +See licenses/ directry for legal legal texts. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..571f0df --- /dev/null +++ b/README.txt @@ -0,0 +1,17 @@ +# My extension - Make The Web Great Again! # + +This extension's goal is to allow replacing javascript served by websites +with scripts specified by user. Something like NoScript and Greasemonkey +together. Such facility is necessary to enable browsing World Wide Web +without executing nonfree software. + +Currently, the target browsers for this extension are Ungoogled Chromium +and various forks of Firefox Quantum (right now, Ungoogled Chromium is +being used). + +This extension's development has just started. It doesn't do anything +useful yet. See TODOS.org + +## Installation ## +The extension can be loaded into Ungoogled Chromium as unpacked extension. +As of now, project's main directory is also the extension directory. diff --git a/TODOS.org b/TODOS.org new file mode 100644 index 0000000..2e3c210 --- /dev/null +++ b/TODOS.org @@ -0,0 +1,51 @@ +TODO: +- parallelize fetching of remote scripts +- make it possible to provide backup urls for remote scripts +- make it possible to cache remote scripts +- make it possible to use wildcards or something similar to be able to assign a script set to -- CRUCIAL + a set of domains or to a set of possible queries at a url +- make it possible to automatically download page's served scripts and save them (of course, this by itself -- CRUCIAL + would give little benefit, but it will make it easy to modify this set of scripts - useful, if some of + those scripts are already free, as is often the case) + - also, find some convenient way to automatically re-add "on" events ("onclick" & friends) +- add some good, sane error handling +- implement whitelisting (LibreJS had some code doing it, but we'll see if it's of any use for us) -- CRUCIAL +- make it possible to export page settings in some format -- CRUCIAL +- get rid of those warnings and exceptions in console (many are not even related to this extension; + who invented this thing?) (gecko-only) +- make page settings easily and conveniently editable in popup -- CRUCIAL + - in popup make it possible to edit both main frame page's + settings and settings for pages that currently happen to + live in iframes +- add some nice styling to settings page +- clean up the remnants of LibreJS +- stop using modules (not available on all browsers) -- CRUCIAL +- use non-predictable value in place of "myext-allow", utilizing hashes -- CRUCIAL +- rename the extension to something good +- port to gecko-based browsers -- CRUCIAL +- rename "bundles" to "bags" to avoid confusion with Web Bundles +- make it possible to modify CSP to suit our custom scripts' needs + - find a way to additionally block all other scripts using CSP + as an additional safety measure +- make blocking more torough -- CRUCIAL + - also block intrinsics -- CRUCIAL + - mind the data: urls -- CRUCIAL +- find out how and make it possible to whitelist non-https urls +- create a repository to host scripts + - enable the extension to automatically fetch script substitutes from the repo +- make it possible to inject scripts to arbitrary places in DOM + - make script blocking code omit those scripts +- facilitate waiting for script injection until DOM has loaded +- check if prerendering has to be blocked -- CRUCIAL +- block prefetch +- rearrange files in extension, add some mechanism to build the extension + +DONE: +- find way to also block scripts in non-http pages (e.g. file://) -- DONE 2021-05-07 (via content scripts, may not be perfect) + (NoScript seems to be doing this through CSP) +- make page settings easily and conveniently editable in a separate window/tab -- DONE 2021-05-05 +- replace comparisons with stricter ones (e.g. do `if(foo === undefined)` instead of `if(!foo)`) -- DONE +- make local storage safe (serialize storage accesses in background script) -- DONE +- split main.js into multiple files -- DONE 2021-01-05 +- make it possible to store entire script files in storage (not just links) -- DONE 2021-01-05 + - make it possible to re-use the same script or set of scripts multiple times -- DONE 2021-01-05 diff --git a/background/ResponseHandler.mjs b/background/ResponseHandler.mjs new file mode 100644 index 0000000..6b979e6 --- /dev/null +++ b/background/ResponseHandler.mjs @@ -0,0 +1,257 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* * +* Copyright (C) 2017, 2018 Nathan Nichols +* Copyright (C) 2018 Ruben Rodriguez <ruben@gnu.org> +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** +* This listener gets called as soon as we've got all the HTTP headers, can guess +* content type and encoding, and therefore correctly parse HTML documents +* and external script inclusions in search of crappy JavaScript +*/ + +import inject_scripts from './script_injector.mjs'; +import {ResponseProcessor} from './ResponseProcessor.mjs'; + +"use strict"; + +var ResponseHandler = { + /** + * Enforce white/black lists for url/site early (hashes will be handled later) + */ + async pre(response) { + // TODO: reimplement blacklisting/whitelisting later + if (true) return ResponseProcessor.CONTINUE; + + let {request} = response; + let {url, type, tabId, frameId, documentUrl} = request; + + let fullUrl = url; + url = ListStore.urlItem(url); + let site = ListStore.siteItem(url); + + let blacklistedSite = ListManager.siteMatch(site, blacklist); + let blacklisted = blacklistedSite || blacklist.contains(url); + let topUrl = type === "sub_frame" && request.frameAncestors && request.frameAncestors.pop() || documentUrl; + + if (blacklisted) { + if (type === "script") { + // this shouldn't happen, because we intercept earlier in blockBlacklistedScripts() + return ResponseProcessor.REJECT; + } + if (type === "main_frame") { // we handle the page change here too, since we won't call edit_html() + activityReports[tabId] = await createReport({url: fullUrl, tabId}); + // Go on without parsing the page: it was explicitly blacklisted + let reason = blacklistedSite + ? `All ${blacklistedSite} blacklisted by user` + : "Address blacklisted by user"; + await addReportEntry(tabId, url, {"blacklisted": [blacklistedSite || url, reason], url: fullUrl}); + } + // use CSP to restrict JavaScript execution in the page + request.responseHeaders.unshift({ + name: `Content-security-policy`, + value: `script-src 'none';` + }); + return {responseHeaders: request.responseHeaders}; // let's skip the inline script parsing, since we block by CSP + } else { + + let whitelistedSite = ListManager.siteMatch(site, whitelist); + let whitelisted = response.whitelisted = whitelistedSite || whitelist.contains(url); + if (type === "script") { + if (whitelisted) { + // accept the script and stop processing + addReportEntry(tabId, url, {url: topUrl, + "whitelisted": [url, whitelistedSite ? `User whitelisted ${whitelistedSite}` : "Whitelisted by user"]}); + return ResponseProcessor.ACCEPT; + } else { + let scriptInfo = await ExternalLicenses.check({url: fullUrl, tabId, frameId, documentUrl}); + if (scriptInfo) { + let verdict, ret; + let msg = scriptInfo.toString(); + if (scriptInfo.free) { + verdict = "accepted"; + ret = ResponseProcessor.ACCEPT; + } else { + verdict = "blocked"; + ret = ResponseProcessor.REJECT; + } + addReportEntry(tabId, url, {url, [verdict]: [url, msg]}); + return ret; + } + } + } + } + // it's a page (it's too early to report) or an unknown script: + // let's keep processing + return ResponseProcessor.CONTINUE; + }, + + /** + * Here we do the heavylifting, analyzing unknown scripts + */ + async post(response) { + let {type} = response.request; + return await handle_html(response, response.whitelisted); + } +} + +/** +* Serializes HTMLDocument objects including the root element and +* the DOCTYPE declaration +*/ +function doc2HTML(doc) { + let s = doc.documentElement.outerHTML; + if (doc.doctype) { + let dt = doc.doctype; + let sDoctype = `<!DOCTYPE ${dt.name || "html"}`; + if (dt.publicId) sDoctype += ` PUBLIC "${dt.publicId}"`; + if (dt.systemId) sDoctype += ` "${dt.systemId}"`; + s = `${sDoctype}>\n${s}`; + } + return s; +} + +/** +* Shortcut to create a correctly namespaced DOM HTML elements +*/ +function createHTMLElement(doc, name) { + return doc.createElementNS("http://www.w3.org/1999/xhtml", name); +} + +/** +* Replace any element with a span having the same content (useful to force +* NOSCRIPT elements to visible the same way as NoScript and uBlock do) +*/ +function forceElement(doc, element) { + let replacement = createHTMLElement(doc, "span"); + replacement.innerHTML = element.innerHTML; + element.replaceWith(replacement); + return replacement; +} + +/** +* Forces displaying any element having the "data-librejs-display" attribute and +* <noscript> elements on pages where LibreJS disabled inline scripts (unless +* they have the "data-librejs-nodisplay" attribute). +*/ +function forceNoscriptElements(doc) { + let shown = 0; + // inspired by NoScript's onScriptDisabled.js + for (let noscript of doc.querySelectorAll("noscript:not([data-librejs-nodisplay])")) { + let replacement = forceElement(doc, noscript); + // emulate meta-refresh + let meta = replacement.querySelector('meta[http-equiv="refresh"]'); + if (meta) { + refresh = true; + doc.head.appendChild(meta); + } + shown++; + } + return shown; +} + +/** +* Forces displaying any element having the "data-librejs-display" attribute and +* <noscript> elements on pages where LibreJS disabled inline scripts (unless +* they have the "data-librejs-nodisplay" attribute). +*/ +function showConditionalElements(doc) { + let shown = 0; + for (let element of document.querySelectorAll("[data-librejs-display]")) { + forceElement(doc, element); + shown++; + } + return shown; +} + +/** + +* Reads/changes the HTML of a page and the scripts within it. +*/ +async function editHtml(html, documentUrl, tabId, frameId, whitelisted){ + + var parser = new DOMParser(); + var html_doc = parser.parseFromString(html, "text/html"); + + if (whitelisted) { // don't bother rewriting + return null; + } + + var scripts = html_doc.scripts; + + let findLine = finder => finder.test(html) && html.substring(0, finder.lastIndex).split(/\n/).length || 0; + + let modified = false; + // Deal with intrinsic events + let intrinsicFinder = /<[a-z][^>]*\b(on\w+|href\s*=\s*['"]?javascript:)/gi; + for (let element of html_doc.all) { + let line = -1; + for (let attr of element.attributes) { + let {name, value} = attr; + value = value.trim(); + if (name.startsWith("on")) { + attr.value = "console.log(\"event script blocked by myext\")"; + } else if (name === "href" && value.toLowerCase().startsWith("javascript:")){ + if (line === -1) { + line = findLine(intrinsicFinder); + } + try { + attr.value = `view-source:${documentUrl}#line${line}`; + } catch (e) { + console.error(e); + } + } + } + } + + let modifiedInline = false; + let scriptFinder = /<script\b/ig; + for(let i = 0, len = scripts.length; i < len; i++) { + let script = scripts[i]; + let line = findLine(scriptFinder); + if (!script.src) { + script.textContent = `//script blocked, you can examine it at view-source:${documentUrl}#line${line}`; + } else { + let src = script.src; + script.removeAttribute("src"); + script.setAttribute("blocked-src", src); + script.textContent = "//script blocked"; + } + } + + showConditionalElements(html_doc); + forceNoscriptElements(html_doc); + await inject_scripts(documentUrl, html_doc); + return doc2HTML(html_doc); +} + +/** +* Here we handle html document responses +*/ +async function handle_html(response, whitelisted) { + let {text, request} = response; + let {url, tabId, frameId, type} = request; + if (type === "main_frame") { + //activityReports[tabId] = await createReport({url, tabId}); + //updateBadge(tabId); + } + return await editHtml(text, url, tabId, frameId, whitelisted); +} + +export default ResponseHandler; diff --git a/background/ResponseMetaData.mjs b/background/ResponseMetaData.mjs new file mode 100644 index 0000000..345fc54 --- /dev/null +++ b/background/ResponseMetaData.mjs @@ -0,0 +1,107 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net> +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + This class parses HTTP response headers to extract both the + MIME Content-type and the character set to be used, if specified, + to parse textual data through a decoder. +*/ + +"use strict"; + +const BOM = [0xEF, 0xBB, 0xBF]; +const DECODER_PARAMS = {stream: true}; + +class ResponseMetaData { + constructor(request) { + let {responseHeaders} = request; + this.headers = {}; + for (let h of responseHeaders) { + if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) { + let propertyName = h.name.split("-")[1].trim(); + propertyName = `content${propertyName.charAt(0).toUpperCase()}${propertyName.substring(1).toLowerCase()}`; + this[propertyName] = h.value; + this.headers[propertyName] = h; + } + } + this.computedCharset = ""; + } + + get charset() { + let charset = ""; + if (this.contentType) { + let m = this.contentType.match(/;\s*charset\s*=\s*(\S+)/); + if (m) { + charset = m[1]; + } + } + Object.defineProperty(this, "charset", { value: charset, writable: false, configurable: true }); + return this.computedCharset = charset; + } + + decode(data) { + let charset = this.charset; + let decoder = this.createDecoder(); + let text = decoder.decode(data, DECODER_PARAMS); + if (!charset && /html/i.test(this.contentType)) { + // missing HTTP charset, sniffing in content... + + if (data[0] === BOM[0] && data[1] === BOM[1] && data[2] === BOM[2]) { + // forced UTF-8, nothing to do + return text; + } + + // let's try figuring out the charset from <meta> tags + let parser = new DOMParser(); + let doc = parser.parseFromString(text, "text/html"); + let meta = doc.querySelectorAll('meta[charset], meta[http-equiv="content-type"], meta[content*="charset"]'); + for (let m of meta) { + charset = m.getAttribute("charset"); + if (!charset) { + let match = m.getAttribute("content").match(/;\s*charset\s*=\s*([\w-]+)/i) + if (match) charset = match[1]; + } + if (charset) { + decoder = this.createDecoder(charset, null); + if (decoder) { + this.computedCharset = charset; + return decoder.decode(data, DECODER_PARAMS); + } + } + } + } + return text; + } + + createDecoder(charset = this.charset, def = "latin1") { + if (charset) { + try { + return new TextDecoder(charset); + } catch (e) { + console.error(e); + } + } + return def ? new TextDecoder(def) : null; + } +}; +ResponseMetaData.UTF8BOM = new Uint8Array(BOM); + +export default ResponseMetaData; diff --git a/background/ResponseProcessor.mjs b/background/ResponseProcessor.mjs new file mode 100644 index 0000000..85c2655 --- /dev/null +++ b/background/ResponseProcessor.mjs @@ -0,0 +1,145 @@ +/** +* GNU LibreJS - A browser add-on to block nonfree nontrivial JavaScript. +* +* Copyright (C) 2018 Giorgio Maone <giorgio@maone.net> +* +* This file is part of GNU LibreJS. +* +* GNU LibreJS is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* GNU LibreJS is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with GNU LibreJS. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + An abstraction layer over the StreamFilter API, allowing its clients to process + only the "interesting" HTML and script requests and leaving the other alone +*/ + +import ResponseMetaData from './ResponseMetaData.mjs'; +import browser from '/common/browser.mjs'; + +let listeners = new WeakMap(); +let webRequestEvent = browser.webRequest.onHeadersReceived; + +class ResponseProcessor { + + static install(handler, types = ["main_frame", "sub_frame"]) { + if (listeners.has(handler)) return false; + let listener = + async request => await new ResponseTextFilter(request).process(handler); + listeners.set(handler, listener); + webRequestEvent.addListener( + listener, + {urls: ["<all_urls>"], types}, + ["blocking", "responseHeaders"] + ); + return true; + } + + static uninstall(handler) { + let listener = listeners.get(handler); + if (listener) { + webRequestEvent.removeListener(listener); + } + } +} + +Object.assign(ResponseProcessor, { + // control flow values to be returned by handler.pre() callbacks + ACCEPT: {}, + REJECT: {cancel: true}, + CONTINUE: null +}); + +class ResponseTextFilter { + constructor(request) { + this.request = request; + let {type, statusCode} = request; + let md = this.metaData = new ResponseMetaData(request); + this.canProcess = // we want to process html documents and scripts only + (statusCode < 300 || statusCode >= 400) && // skip redirections + !md.disposition && // skip forced downloads + (type === "script" || /\bhtml\b/i.test(md.contentType)); + } + + async process(handler) { + if (!this.canProcess) return ResponseProcessor.ACCEPT; + let {metaData, request} = this; + let response = {request, metaData}; // we keep it around allowing callbacks to store state + if (typeof handler.pre === "function") { + let res = await handler.pre(response); + if (res) return res; + if (handler.post) handler = handler.post; + if (typeof handler !== "function") return ResponseProcessor.ACCEPT; + } + + return ResponseProcessor.ACCEPT; + + let {requestId, responseHeaders} = request; + let filter = browser.webRequest.filterResponseData(requestId); + let buffer = []; + + filter.ondata = event => { + buffer.push(event.data); + }; + + filter.onstop = async event => { + // concatenate chunks + let size = buffer.reduce((sum, chunk, n) => sum + chunk.byteLength, 0) + let allBytes = new Uint8Array(size); + let pos = 0; + for (let chunk of buffer) { + allBytes.set(new Uint8Array(chunk), pos); + pos += chunk.byteLength; + } + buffer = null; // allow garbage collection + if (allBytes.indexOf(0) !== -1) { + console.debug("Warning: zeroes in bytestream, probable cached encoding mismatch.", request); + if (request.type === "script") { + console.debug("It's a script, trying to refetch it."); + response.text = await (await fetch(request.url, {cache: "reload", credentials: "include"})).text(); + } else { + console.debug("It's a %s, trying to decode it as UTF-16.", request.type); + response.text = new TextDecoder("utf-16be").decode(allBytes, {stream: true}); + } + } else { + response.text = metaData.decode(allBytes); + } + let editedText = null; + try { + editedText = await handler(response); + } catch(e) { + console.error(e); + } + if (editedText !== null) { + // we changed the content, let's re-encode + let encoded = new TextEncoder().encode(editedText); + // pre-pending the UTF-8 BOM will force the charset per HTML 5 specs + allBytes = new Uint8Array(encoded.byteLength + 3); + allBytes.set(ResponseMetaData.UTF8BOM, 0); // UTF-8 BOM + allBytes.set(encoded, 3); + } + filter.write(allBytes); + filter.close(); + } + + return ResponseProcessor.ACCEPT; + } +} + +/* The following was originally in Storage.js */ +function url_item(url) { + let queryPos = url.indexOf("?"); + return queryPos === -1 ? url : url.substring(0, queryPos); +} + +export {ResponseProcessor, url_item}; diff --git a/background/background.html b/background/background.html new file mode 100644 index 0000000..519c2a2 --- /dev/null +++ b/background/background.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script src="./main.mjs" type="module"></script> + </head> +</html> diff --git a/background/main.mjs b/background/main.mjs new file mode 100644 index 0000000..5d32d98 --- /dev/null +++ b/background/main.mjs @@ -0,0 +1,168 @@ +/** +* Myext main background script +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import {TYPE_PREFIX} from '/common/stored_types.mjs'; +import get_storage from './storage.mjs'; +//import {ResponseProcessor} from './ResponseProcessor.mjs'; +//import ResponseHandler from './ResponseHandler.mjs'; +import start_storage_server from './storage_server.mjs'; +import start_page_actions_server from './page_actions_server.mjs'; +import start_policy_smuggler from './policy_smuggler.mjs'; +import browser from '/common/browser.mjs'; + +"use strict"; + +start_storage_server(); +start_page_actions_server(); +start_policy_smuggler(); + +async function init_myext(install_details) +{ + console.log("details:", install_details); + if (install_details.reason != "install") + return; + + let storage = await get_storage(); + + await storage.clear(); + + /* + * Below we add sample settings to the extension. + * Those should be considered example values for viewing in the options + * page. They won't make my.fsf.org work. The only scripts that does + * something useful right now is the opencores one. + */ + + let components = []; + for (let script_data of [ + {url: "http://127.0.0.1:8000/myfsf_define_CRM.js", + hash:"bf0cc81c7e8d5f800877b4bc3f14639f946f5ac6d4dc120255ffac5eba5e48fe"}, + {url: "https://my.fsf.org/misc/jquery.js?v=1.4.4", + hash:"261ae472fa0cbf27c80c9200a1599a60fde581a0e652eee4bf41def8cb61f2d0"}, + {url: "https://my.fsf.org/misc/jquery-extend-3.4.0.js?v=1.4.4", + hash:"c54103ba57ee210ca55c052e70415402707548a4e6a68dd6efb3895019bee392"}, + {url: "https://my.fsf.org/misc/jquery-html-prefilter-3.5.0-backport.js?v=1.4.4", + hash:"fad84efa145fb507e5df9b582fa01b1c4e6313de7f72ebdd55726d92fa4dbf06"}, + {url: "https://my.fsf.org/misc/jquery.once.js?v=1.2", + hash:"1430f42c0d760ba8e05bb3762480502e541f654fec5739ee40625ab22dc38c4f"}, + {url: "https://my.fsf.org/misc/drupal.js?qmaukd", + hash:"2e08dccbd4d8b728a6871562995a4636b89bfe0ed3b8fb0138191c922228b116"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery/dist/jquery.min.js?qmaukd", + hash:"a6d01520d28d15dbe476de84eea90eb3ee2d058722efc062ec73cb5fad78a17b"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-ui/jquery-ui.min.js?qmaukd", + hash:"28ce75d953678c4942df47a11707a15e3c756021cf89090e3e6aa7ad6b6971c3"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/lodash-compat/lodash.min.js?qmaukd", + hash:"f2871cc80c52fe8c04c582c4a49797c9c8fd80391cf1452e47f7fe97835ed5cc"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.mousewheel.min.js?qmaukd", + hash:"f50233e84c2ac7ada37a094d3f7d3b3f7c97716d6b7b47bf69619d93ee4ac1ce"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/select2/select2.min.js?qmaukd", + hash:"ce61298fb9aa4ec49ccd4172d097e36a9e5db3af06a7b82796659368f15b7c1b"}, + {url: "https://my.fsf.org/sites/all/modules/civi crm/packages/jquery/plugins/jquery.form.min.js?qmaukd", + hash:"c90f0e501d2948fbc2b61bffd654fa4ab64741fd48923782419eeb14d3816fb8"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.timeentry.min.js?qmaukd", + hash:"8e85df981e8ad7049d06dfb075277d038734d36a7097c7f021021b2bdccfe9bb"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.blockUI.min.js?qmaukd", + hash:"806aedff52ac822f2adc5797073e1e5c5cec32eb9f15f2319cb32a347dcd232b"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/datatables/media/js/jquery.dataTables.min.js?qmaukd", + hash:"b796504d9b1b422f0dc6ccc2d740ac78a8c9e5078cc3934836d39742b1121925"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/bower_components/jquery-validation/dist/jquery.validate.min.js?qmaukd", + hash:"f0f5373ad203101ea91bf826c5a7ef8f7cd74887f06bad2cb9277a504503b9e2"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/packages/jquery/plugins/jquery.ui.datepicker.validation.min.js?qmaukd", + hash:"c6e6f6bf7f8fff25cca338045774e267e8eaa2d48ac9100540f3d59a6d2b3c61"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/Common.js?qmaukd", + hash:"17aa222a3af2e8958be16accb5e77ef39f67009cb3b500718d8fffd45b399148"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.datepicker.js?qmaukd", + hash:"9bd8d10208aa99c156325f7819da6f0dd62ba221ac4119c3ccd4834e2cf36535"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/crm.ajax.js?qmaukd", + hash:"6401a4e257b7499ae4a00be2c200e4504a2c9b3d6b278a830c31a7b63374f0fe"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/wysiwyg/crm.wysiwyg.js?qmaukd", + hash:"fa962356072a36672c3b4b25bdeb657f020995a067e20a29cd5bb84b05157762"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/js/noconflict.js?qmaukd", + hash:"58d6d9f496a235d23cf891926d71f2104e4f2afe1d14bb4e2b5233f646c35e62"}, + {url: "https://my.fsf.org/sites/all/modules/matomo/matomo.js?qmaukd", + hash:"7f39ccd085f348189cd2fb62ea4d4a658d96f6bba266265880b98605e777e2de"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/global.js?qmaukd", + hash:"aa7983f6b902f9f4415cfc8346e0c3f194cc95b78f52f2ad09ec7effa1326b9c"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.superfish.min.js?qmaukd", + hash:"5ef1f93bf3901227056bf9ed0ed93a148eec4dda30f419756b12bedd1098815e"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.sidr.min.js?qmaukd", + hash:"c4914d415826676c6af2e61f16edb72c5388f8600ba6de9049892aee49d980a0"}, + {url: "https://my.fsf.org/sites/all/themes/fsf_venture/js/jquery.flexslider.min.js?qmaukd", + hash:"cefaf715761b4494913851249b9d40dacb4a8cb61242b0efc859dc586d56e0d4"}, + {url: "http://127.0.0.1:8000/myfsf_crap.js", + hash:"d91ccf21592d0f861ea0ba946bc257fc5d88269327cad0a91387da6cb8ff633e"}, + {url: "https://my.fsf.org/sites/all/modules/civicrm/templates/CRM/Core/BillingBlock.js?r=lp7Di", + hash:"2f25d35e7a0c0060ab0a444a577f09dd3c9934ae898a7ee0eb20b6c986ab5a1c"}, + {url: "https://my.fsf.org/extensions/com.aghstrategies.giftmemberships/js/giftpricefield.js?r=lp7Di", + hash:"f86080e6bd306fe46474039aeca2808235005bce5a2a29416d08210022039a45"}, + {url: "https://my.fsf.org/extensions/com.ginkgostreet.negativenegator/js/negativenegator.js?r=lp7Di", + hash:"d0e87bac832856db70947d82a7ab4e0b7c8b1070d5f1a32335345e033ece3a14"} + ]) { + let name_regex = /\/([^/]+)\.js/; + let name = name_regex.exec(script_data.url)[1]; + await storage.set(TYPE_PREFIX.SCRIPT, name, script_data); + components.push([TYPE_PREFIX.SCRIPT, name]); + } + + await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/join", {components}); + + let hello_script = { + text: "console.log(\"hello, every1!\");\n" + }; + await storage.set(TYPE_PREFIX.SCRIPT, "hello", hello_script); + await storage.set(TYPE_PREFIX.BUNDLE, "hello", + [[TYPE_PREFIX.SCRIPT, "hello"]]); + await storage.set(TYPE_PREFIX.PAGE, "https://my.fsf.org/", { + components: [[TYPE_PREFIX.BUNDLE, "hello"]], + allow: true + }); + + let opencores_script = { + text: `\ +let data = JSON.parse(document.getElementById("__NEXT_DATA__").textContent); +let sections = {}; +for (let h1 of document.getElementsByClassName("cMJCrc")) { + let ul = document.createElement("ul"); + if (h1.nextElementSibling !== null) + h1.parentNode.insertBefore(ul, h1.nextElementSibling); + else + h1.parentNode.appendChild(ul); + + sections[h1.children[1].firstChild.textContent] = ul; +} + +for (let prop of data.props.pageProps.list) { + let ul = sections[prop.category]; + if (ul === undefined) { + console.log(\`unknown category "\${prop.category}" for project "\${prop.title}"\`); + continue; + } + + let li = document.createElement("li"); + let a = document.createElement("a"); + a.setAttribute("href", "/projects/" + prop.slug); + a.textContent = prop.title; + + li.appendChild(a); + ul.appendChild(li); +} +` + }; + + await storage.set(TYPE_PREFIX.SCRIPT, "opencores", opencores_script); + await storage.set(TYPE_PREFIX.PAGE, "https://opencores.org/projects", { + components: [[TYPE_PREFIX.SCRIPT, "opencores"]], + allow: false + }); +} + +browser.runtime.onInstalled.addListener(init_myext); + +console.log("hello, myext"); diff --git a/background/message_server.mjs b/background/message_server.mjs new file mode 100644 index 0000000..e3f83d0 --- /dev/null +++ b/background/message_server.mjs @@ -0,0 +1,31 @@ +/** +* Myext message server +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +import browser from '/common/browser.mjs'; + +var listeners = {}; + +/* magic should be one of the constants from /common/connection_types.mjs */ + +export default function listen_for_connection(magic, cb) +{ + listeners[magic] = cb; +} + +function raw_listen(port) { + if (listeners[port.name] === undefined) + return; + + listeners[port.name](port); +} + +browser.runtime.onConnect.addListener(raw_listen); diff --git a/background/page_actions_server.mjs b/background/page_actions_server.mjs new file mode 100644 index 0000000..5fb4924 --- /dev/null +++ b/background/page_actions_server.mjs @@ -0,0 +1,145 @@ +/** +* Myext serving of page actions to content scripts +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import get_storage from './storage.mjs'; +import {TYPE_PREFIX} from '/common/stored_types.mjs'; +import CONNECTION_TYPE from '/common/connection_types.mjs'; +import browser from '/common/browser.mjs'; +import listen_for_connection from './message_server.mjs'; +import url_item from './url_item.mjs'; +import sha256 from './sha256.mjs'; + +"use strict"; + +var storage; +var handler; + +function send_scripts(url, port) +{ + let settings = storage.get(TYPE_PREFIX.PAGE, url_item(url)); + if (settings === undefined) + return; + + let components = settings.components; + let processed_bundles = new Set(); + + send_scripts_rec(components, port, processed_bundles); +} + +// TODO: parallelize script fetching +async function send_scripts_rec(components, port, processed_bundles) +{ + for (let [prefix, name] of components) { + if (prefix === TYPE_PREFIX.BUNDLE) { + if (processed_bundles.has(name)) { + console.log(`preventing recursive inclusion of bundle ${name}`); + continue; + } + + var bundle = storage.get(TYPE_PREFIX.BUNDLE, name); + + if (bundle === undefined) { + console.log(`no bundle in storage for key ${name}`); + continue; + } + + processed_bundles.add(name); + await send_scripts_rec(bundle, port, processed_bundles); + processed_bundles.delete(name); + } else { + let script_text = await get_script_text(name); + if (script_text === undefined) + continue; + + port.postMessage({inject : [script_text]}); + } + } +} + +async function get_script_text(script_name) +{ + try { + let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name); + if (script_data === undefined) { + console.log(`missing data for ${script_name}`); + return; + } + let script_text = script_data.text; + if (!script_text) + script_text = await fetch_remote_script(script_data); + return script_text; + } catch (e) { + console.log(e); + } +} + +function ajax_callback() +{ + if (this.readyState == 4) + this.resolve_callback(this); +} + +function initiate_ajax_request(resolve, method, url) +{ + var xhttp = new XMLHttpRequest(); + xhttp.resolve_callback = resolve; + xhttp.onreadystatechange = ajax_callback; + xhttp.open(method, url, true); + xhttp.send(); +} + +function make_ajax_request(method, url) +{ + return new Promise((resolve, reject) => + initiate_ajax_request(resolve, method, url)); +} + +async function fetch_remote_script(script_data) +{ + try { + let xhttp = await make_ajax_request("GET", script_data.url); + if (xhttp.status === 200) { + let computed_hash = sha256(xhttp.responseText); + if (computed_hash !== script_data.hash) { + console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`); + return; + } + return xhttp.responseText; + } else { + console.log("script not fetched: " + script_data.url); + return; + } + } catch (e) { + console.log(e); + } +} + +function handle_message(port, message, handler) +{ + port.onMessage.removeListener(handler[0]); + let url = message.url; + console.log({url}); + send_scripts(url, port); +} + +function new_connection(port) +{ + console.log("new page actions connection!"); + let handler = []; + handler.push(m => handle_message(port, m, handler)); + port.onMessage.addListener(handler[0]); +} + +export default async function start() +{ + storage = await get_storage(); + + listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection); +} diff --git a/background/policy_smuggler.mjs b/background/policy_smuggler.mjs new file mode 100644 index 0000000..61f34c5 --- /dev/null +++ b/background/policy_smuggler.mjs @@ -0,0 +1,60 @@ +/** +* Myext smuggling policy to content script through url +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import {TYPE_PREFIX} from '/common/stored_types.mjs'; +import get_storage from './storage.mjs'; +import browser from '/common/browser.mjs'; +import url_item from './url_item.mjs'; + +"use strict"; + +var storage; + +function redirect(request) +{ + let url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; + let match = url_re.exec(request.url); + let base_url = match[1]; + let first_target = match[3]; + let second_target = match[4]; + + if (first_target === "#myext-allow") { + console.log(["not redirecting"]); + return {cancel : false}; + } + + let url = url_item(request.url); + let settings = storage.get(TYPE_PREFIX.PAGE, url); + console.log("got", storage.get(TYPE_PREFIX.PAGE, url), "for", url); + if (settings === undefined || !settings.allow) + return {cancel : false}; + + second_target = (first_target || "") + (second_target || "") + + console.log(["redirecting", request.url, + (base_url + "#myext-allow" + second_target)]); + + return { + redirectUrl : (base_url + "#myext-allow" + second_target) + }; +} + +export default async function start() { + storage = await get_storage(); + + chrome.webRequest.onBeforeRequest.addListener( + redirect, + { + urls: ["<all_urls>"], + types: ["main_frame", "sub_frame"] + }, + ["blocking"] + ); +} diff --git a/background/reverse_use_info.mjs b/background/reverse_use_info.mjs new file mode 100644 index 0000000..c38c52f --- /dev/null +++ b/background/reverse_use_info.mjs @@ -0,0 +1,93 @@ +/** +* Myext scripts and bundles usage index +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import TYPE_PREFIX from '/common/stored_types.mjs'; +import get_storage from './storage.mjs'; +import make_once from '/common/once.mjs'; + +"use strict"; + +/* + * We want to count referenes to scripts and bundles in order to know, + * for example, whether one can be safely deleted. + */ + +var component_uses = new Map(); +var storage; + +function add_use_info_by_item(prefix, item, components) +{ + for (let component of components) { + component = component.join(""); + + let used_by_info = component_uses.get(component); + + if (used_by_info === undefined) { + used_by_info = {}; + component_uses.set(component, used_by_info); + } + + if (used_by_info[prefix] === undefined) + used_by_info[prefix] = new Set(); + + used_by_info[prefix].add(item); + } +} + +function remove_use_info_by_item(prefix, item, components) +{ + for (let component of components) { + used_by_info = component_uses.get(component.join("")); + if (used_by_info === undefined || used_by_info[prefix] === undefined) + return; + + used_by_info[prefix].delete(item); + } +} + +function build_reverse_uses_info(entries_it, type_prefix) +{ + for (let [item, components] of storage.get_all_it(type_prefix)) + add_use_info_by_item(type_prefix, item, components); +} + +function handle_change(change) +{ + if (change.old_val !== undefined) + remove_use_info_by_item(change.prefix, change.item, change.old_val); + + if (change.new_val !== undefined) + add_use_info_by_item(change.prefix, change.item, change.new_val); +} + +function get_uses(arg1, arg2=undefined) +{ + let [prefix, item] = [arg1, arg2]; + + if (arg2 === undefined) + [prefix, item] = arg1; + + return component_uses.get(prefix + item); +} + +async function init() +{ + storage = await get_storage(); + + prefixes = [TYPE_PREFIX.PAGE, TYPE_PREFIX.BUNDLE]; + for (let prefix of prefixes) + build_reverse_uses_info(prefix); + + storage.add_change_listener(handle_change, prefixes); + + return get_uses; +} + +export default make_once(init); diff --git a/background/script_injector.mjs b/background/script_injector.mjs new file mode 100644 index 0000000..3298a43 --- /dev/null +++ b/background/script_injector.mjs @@ -0,0 +1,122 @@ +/** +* Myext script injector +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import {TYPE_PREFIX} from '/common/stored_types.mjs'; +import sha256 from './sha256.mjs'; +import {url_item} from './ResponseProcessor.mjs'; +import get_storage from './storage.mjs'; + +"use strict"; + +var storage; + +function ajax_callback() +{ + if (this.readyState == 4) + this.resolve_callback(this); +} + +function initiate_ajax_request(resolve, method, url) +{ + var xhttp = new XMLHttpRequest(); + xhttp.resolve_callback = resolve; + xhttp.onreadystatechange = ajax_callback; + xhttp.open(method, url, true); + xhttp.send(); +} + +function make_ajax_request(method, url) +{ + return new Promise((resolve, reject) => + initiate_ajax_request(resolve, method, url)); +} + +async function fetch_remote_script(script_data) +{ + try { + let xhttp = await make_ajax_request("GET", script_data.url); + if (xhttp.status === 200) { + let computed_hash = sha256(xhttp.responseText); + if (computed_hash !== script_data.hash) { + console.log(`Bad hash for ${script_data.url}\n got ${computed_hash} instead of ${script_data.hash}`); + return; + } + return xhttp.responseText; + } else { + console.log("script not fetched: " + script_data.url); + return; + } + } catch (e) { + console.log(e); + } +} + +async function get_script_text(script_name) +{ + try { + let script_data = storage.get(TYPE_PREFIX.SCRIPT, script_name); + if (script_data === undefined) { + console.log(`missing data for ${script_name}`); + return; + } + let script_text = script_data.text; + if (!script_text) + script_text = await fetch_remote_script(script_data); + return script_text; + } catch (e) { + console.log(e); + } +} + +// TODO: parallelize script fetching +// TODO: guard against infinite recursion + +async function inject_scripts_rec(components, doc) +{ + for (let [prefix, name] of components) { + if (prefix === TYPE_PREFIX.BUNDLE) { + var bundle = storage.get(TYPE_PREFIX.BUNDLE, name); + + if (bundle === undefined) { + console.log(`no bundle in storage for key ${elem_key}`); + continue; + } + await inject_scripts_rec(bundle, doc); + } else { + let script_text = await get_script_text(name,); + if (script_text === undefined) + continue; + + let script = doc.createElement("script"); + script.textContent = script_text; + doc.body.appendChild(script); + } + } +} + +async function inject_scripts(url, doc) +{ + storage = await get_storage(); + + url = url_item(url); + + let components = storage.get(TYPE_PREFIX.PAGE, url); + + if (components === undefined) { + console.log(`got nothing for ${url}`); + return + } else { + console.log(`got ${components.length} component(s) for ${url}`); + } + + await inject_scripts_rec(components, doc); +} + +export default inject_scripts; diff --git a/background/sha256.mjs b/background/sha256.mjs new file mode 100644 index 0000000..b8d9bda --- /dev/null +++ b/background/sha256.mjs @@ -0,0 +1,524 @@ +/** + * [js-sha256]{@link https://github.com/emn178/js-sha256/} + * + * @version 0.9.0 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2017 + * @license MIT + */ + +var fake_window = {}; + +/*jslint bitwise: true */ +(function () { + 'use strict'; + + console.log('hello, crypto!'); + var ERROR = 'input is invalid type'; + var WINDOW = typeof window === 'object'; + var root = /*WINDOW ? window : {}*/ fake_window; + if (root.JS_SHA256_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var EXTRA = [-2147483648, 8388608, 32768, 128]; + var SHIFT = [24, 16, 8, 0]; + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; + + var blocks = []; + + if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { + ArrayBuffer.isView = function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + }; + } + + var createOutputMethod = function (outputType, is224) { + return function (message) { + return new Sha256(is224, true).update(message)[outputType](); + }; + }; + + var createMethod = function (is224) { + var method = createOutputMethod('hex', is224); + if (NODE_JS) { + method = nodeWrap(method, is224); + } + method.create = function () { + return new Sha256(is224); + }; + method.update = function (message) { + return method.create().update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createOutputMethod(type, is224); + } + return method; + }; + + var nodeWrap = function (method, is224) { + var crypto = eval("require('crypto')"); + var Buffer = eval("require('buffer').Buffer"); + var algorithm = is224 ? 'sha224' : 'sha256'; + var nodeMethod = function (message) { + if (typeof message === 'string') { + return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); + } else { + if (message === null || message === undefined) { + throw new Error(ERROR); + } else if (message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } + } + if (Array.isArray(message) || ArrayBuffer.isView(message) || + message.constructor === Buffer) { + return crypto.createHash(algorithm).update(new Buffer(message)).digest('hex'); + } else { + return method(message); + } + }; + return nodeMethod; + }; + + var createHmacOutputMethod = function (outputType, is224) { + return function (key, message) { + return new HmacSha256(key, is224, true).update(message)[outputType](); + }; + }; + + var createHmacMethod = function (is224) { + var method = createHmacOutputMethod('hex', is224); + method.create = function (key) { + return new HmacSha256(key, is224); + }; + method.update = function (key, message) { + return method.create(key).update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createHmacOutputMethod(type, is224); + } + return method; + }; + + function Sha256(is224, sharedMemory) { + if (sharedMemory) { + blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + this.blocks = blocks; + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + if (is224) { + this.h0 = 0xc1059ed8; + this.h1 = 0x367cd507; + this.h2 = 0x3070dd17; + this.h3 = 0xf70e5939; + this.h4 = 0xffc00b31; + this.h5 = 0x68581511; + this.h6 = 0x64f98fa7; + this.h7 = 0xbefa4fa4; + } else { // 256 + this.h0 = 0x6a09e667; + this.h1 = 0xbb67ae85; + this.h2 = 0x3c6ef372; + this.h3 = 0xa54ff53a; + this.h4 = 0x510e527f; + this.h5 = 0x9b05688c; + this.h6 = 0x1f83d9ab; + this.h7 = 0x5be0cd19; + } + + this.block = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + this.is224 = is224; + } + + Sha256.prototype.update = function (message) { + if (this.finalized) { + return; + } + var notString, type = typeof message; + if (type !== 'string') { + if (type === 'object') { + if (message === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } else if (!Array.isArray(message)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + notString = true; + } + var code, index = 0, i, length = message.length, blocks = this.blocks; + + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + + if (notString) { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += this.bytes / 4294967296 << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; + + Sha256.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + blocks[14] = this.hBytes << 3 | this.bytes >>> 29; + blocks[15] = this.bytes << 3; + this.hash(); + }; + + Sha256.prototype.hash = function () { + var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, + h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; + + for (j = 16; j < 64; ++j) { + // rightrotate + t1 = blocks[j - 15]; + s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); + t1 = blocks[j - 2]; + s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); + blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; + } + + bc = b & c; + for (j = 0; j < 64; j += 4) { + if (this.first) { + if (this.is224) { + ab = 300032; + t1 = blocks[0] - 1413257819; + h = t1 - 150054599 << 0; + d = t1 + 24177077 << 0; + } else { + ab = 704751109; + t1 = blocks[0] - 210244248; + h = t1 - 1521486534 << 0; + d = t1 + 143694565 << 0; + } + this.first = false; + } else { + s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + ab = a & b; + maj = ab ^ (a & c) ^ bc; + ch = (e & f) ^ (~e & g); + t1 = h + s1 + ch + K[j] + blocks[j]; + t2 = s0 + maj; + h = d + t1 << 0; + d = t1 + t2 << 0; + } + s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); + s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); + da = d & a; + maj = da ^ (d & b) ^ ab; + ch = (h & e) ^ (~h & f); + t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; + t2 = s0 + maj; + g = c + t1 << 0; + c = t1 + t2 << 0; + s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); + s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); + cd = c & d; + maj = cd ^ (c & a) ^ da; + ch = (g & h) ^ (~g & e); + t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; + t2 = s0 + maj; + f = b + t1 << 0; + b = t1 + t2 << 0; + s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); + s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); + bc = b & c; + maj = bc ^ (b & d) ^ cd; + ch = (f & g) ^ (~f & h); + t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; + t2 = s0 + maj; + e = a + t1 << 0; + a = t1 + t2 << 0; + } + + this.h0 = this.h0 + a << 0; + this.h1 = this.h1 + b << 0; + this.h2 = this.h2 + c << 0; + this.h3 = this.h3 + d << 0; + this.h4 = this.h4 + e << 0; + this.h5 = this.h5 + f << 0; + this.h6 = this.h6 + g << 0; + this.h7 = this.h7 + h << 0; + }; + + Sha256.prototype.hex = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] + + HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] + + HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] + + HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + + HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] + + HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] + + HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] + + HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + + HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] + + HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] + + HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] + + HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + + HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] + + HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] + + HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] + + HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + + HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] + + HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] + + HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] + + HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + + HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] + + HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] + + HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] + + HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + + HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] + + HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] + + HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] + + HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; + if (!this.is224) { + hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] + + HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] + + HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] + + HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; + } + return hex; + }; + + Sha256.prototype.toString = Sha256.prototype.hex; + + Sha256.prototype.digest = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var arr = [ + (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF, + (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF, + (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF, + (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF, + (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF, + (h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF, + (h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF + ]; + if (!this.is224) { + arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF); + } + return arr; + }; + + Sha256.prototype.array = Sha256.prototype.digest; + + Sha256.prototype.arrayBuffer = function () { + this.finalize(); + + var buffer = new ArrayBuffer(this.is224 ? 28 : 32); + var dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + dataView.setUint32(20, this.h5); + dataView.setUint32(24, this.h6); + if (!this.is224) { + dataView.setUint32(28, this.h7); + } + return buffer; + }; + + function HmacSha256(key, is224, sharedMemory) { + var i, type = typeof key; + if (type === 'string') { + var bytes = [], length = key.length, index = 0, code; + for (i = 0; i < length; ++i) { + code = key.charCodeAt(i); + if (code < 0x80) { + bytes[index++] = code; + } else if (code < 0x800) { + bytes[index++] = (0xc0 | (code >> 6)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else if (code < 0xd800 || code >= 0xe000) { + bytes[index++] = (0xe0 | (code >> 12)); + bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); + bytes[index++] = (0xf0 | (code >> 18)); + bytes[index++] = (0x80 | ((code >> 12) & 0x3f)); + bytes[index++] = (0x80 | ((code >> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } + } + key = bytes; + } else { + if (type === 'object') { + if (key === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { + key = new Uint8Array(key); + } else if (!Array.isArray(key)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + } + + if (key.length > 64) { + key = (new Sha256(is224, true)).update(key).array(); + } + + var oKeyPad = [], iKeyPad = []; + for (i = 0; i < 64; ++i) { + var b = key[i] || 0; + oKeyPad[i] = 0x5c ^ b; + iKeyPad[i] = 0x36 ^ b; + } + + Sha256.call(this, is224, sharedMemory); + + this.update(iKeyPad); + this.oKeyPad = oKeyPad; + this.inner = true; + this.sharedMemory = sharedMemory; + } + HmacSha256.prototype = new Sha256(); + + HmacSha256.prototype.finalize = function () { + Sha256.prototype.finalize.call(this); + if (this.inner) { + this.inner = false; + var innerHash = this.array(); + Sha256.call(this, this.is224, this.sharedMemory); + this.update(this.oKeyPad); + this.update(innerHash); + Sha256.prototype.finalize.call(this); + } + }; + + var exports = createMethod(); + exports.sha256 = exports; + exports.sha224 = createMethod(true); + exports.sha256.hmac = createHmacMethod(); + exports.sha224.hmac = createHmacMethod(true); + + if (COMMON_JS) { + module.exports = exports; + } else { + root.sha256 = exports.sha256; + root.sha224 = exports.sha224; + if (AMD) { + define(function () { + return exports; + }); + } + } +})(); + +export default fake_window.sha256; diff --git a/background/storage.mjs b/background/storage.mjs new file mode 100644 index 0000000..00b1ace --- /dev/null +++ b/background/storage.mjs @@ -0,0 +1,380 @@ +/** +* Myext storage manager +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import {TYPE_PREFIX, TYPE_NAME, list_prefixes} from '/common/stored_types.mjs'; +import {make_lock, lock, unlock} from '/common/lock.mjs'; +import make_once from '/common/once.mjs'; +import browser from '/common/browser.mjs'; + +"use strict"; + +var exports = {}; + +/* We're yet to decide how to handle errors... */ + +/* Here are some basic wrappers for storage API functions */ + +async function get(key) +{ + try { + /* Fix for fact that Chrome does not use promises here */ + let promise = window.browser === undefined ? + new Promise((resolve, reject) => + chrome.storage.local.get(key, val => resolve(val))) : + browser.storage.local.get(key); + + return (await promise)[key]; + } catch (e) { + console.log(e); + } +} + +async function set(key, value) +{ + try { + return browser.storage.local.set({[key]: value}); + } catch (e) { + console.log(e); + } +} + +async function setn(keys_and_values) +{ + let obj = Object(); + while (keys_and_values.length > 1) { + let value = keys_and_values.pop(); + let key = keys_and_values.pop(); + obj[key] = value; + } + + try { + return browser.storage.local.set(obj); + } catch (e) { + console.log(e); + } +} + +async function set_var(name, value) +{ + return set(TYPE_PREFIX.VAR + name, value); +} + +async function get_var(name) +{ + return get(TYPE_PREFIX.VAR + name); +} + +/* A special case of a persisted variable is one that contains list of items. */ + +async function get_list_var(name) +{ + let list = await get_var(name); + + return list === undefined ? [] : list; +} + +/* We maintain in-memory copies of some stored lists. */ + +async function list(prefix) +{ + let name = TYPE_NAME[prefix] + "s"; /* Make plural. */ + let map = new Map(); + + for (let item of await get_list_var(name)) + map.set(item, await get(prefix + item)); + + return {map, prefix, name, listeners : new Set(), lock : make_lock()}; +} + +var pages; +var bundles; +var scripts; + +var list_by_prefix = {}; + +async function init() +{ + for (let prefix of list_prefixes) + list_by_prefix[prefix] = await list(prefix); + + return exports; +} + +/* + * Facilitate listening to changes + */ + +exports.add_change_listener = function (cb, prefixes=list_prefixes) +{ + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.add(cb); +} + +exports.remove_change_listener = function (cb, prefixes=list_prefixes) +{ + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.delete(cb); +} + +function broadcast_change(change, list) +{ + for (let listener_callback of list.listeners) + listener_callback(change); +} + +/* Prepare some hepler functions to get elements of a list */ + +function list_items_it(list, with_values=false) +{ + return with_values ? list.map.entries() : list.map.keys(); +} + +function list_entries_it(list) +{ + return list_items_it(list, true); +} + +function list_items(list, with_values=false) +{ + let array = []; + + for (let item of list_items_it(list, with_values)) + array.push(item); + + return array; +} + +function list_entries(list) +{ + return list_items(list, true); +} + +/* + * Below we make additional effort to update map of given kind of items + * every time an item is added/removed to keep everything coherent. + */ +async function set_item(item, value, list) +{ + await lock(list.lock); + let result = await _set_item(...arguments); + unlock(list.lock) + return result; +} +async function _set_item(item, value, list) +{ + let key = list.prefix + item; + let old_val = list.map.get(item); + if (old_val === undefined) { + let items = list_items(list); + items.push(item); + await setn([key, value, "_" + list.name, items]); + } else { + await set(key, value); + } + + list.map.set(item, value) + + let change = { + prefix : list.prefix, + item, + old_val, + new_val : value + }; + + broadcast_change(change, list); + + return old_val; +} + +// TODO: The actual idea to set value to undefined is good - this way we can +// also set a new list of items in the same API call. But such key +// is still stored in the storage. We need to somehow remove it later. +// For that, we're going to have to store 1 more list of each kind. +async function remove_item(item, list) +{ + await lock(list.lock); + let result = await _remove_item(...arguments); + unlock(list.lock) + return result; +} +async function _remove_item(item, list) +{ + let old_val = list.map.get(item); + if (old_val === undefined) + return; + + let key = list.prefix + item; + let items = list_items(list); + let index = items.indexOf(item); + items.splice(index, 1); + + await setn([key, undefined, "_" + list.name, items]); + + list.map.delete(item); + + let change = { + prefix : list.prefix, + item, + old_val, + new_val : undefined + }; + + broadcast_change(change, list); + + return old_val; +} + +// TODO: same as above applies here +async function replace_item(old_item, new_item, list, new_val=undefined) +{ + await lock(list.lock); + let result = await _replace_item(...arguments); + unlock(list.lock) + return result; +} +async function _replace_item(old_item, new_item, list, new_val=undefined) +{ + let old_val = list.map.get(old_item); + if (new_val === undefined) { + if (old_val === undefined) + return; + new_val = old_val + } else if (new_val === old_val && new_item === old_item) { + return old_val; + } + + if (old_item === new_item || old_val === undefined) { + await _set_item(new_item, new_val, list); + return old_val; + } + + let new_key = list.prefix + new_item; + let old_key = list.prefix + old_item; + let items = list_items(list); + let index = items.indexOf(old_item); + items[index] = new_item; + await setn([old_key, undefined, new_key, new_val, "_" + list.name, items]); + + list.map.delete(old_item); + + let change = { + prefix : list.prefix, + item : old_item, + old_val, + new_val : undefined + }; + + broadcast_change(change, list); + + list.map.set(new_item, new_val); + + change.item = new_item; + change.old_val = undefined; + change.new_val = new_val; + + broadcast_change(change, list); + + return old_val; +} + +/* + * For scripts, item name is chosen by user, data should be an object containing + * - script's url and hash or + * - script's text or + * - all three + */ + +/* + * For bundles, item name is chosen by user, data is an array of 2-element + * arrays with type prefix and script/bundle names. + */ + +/* For pages data argument is same as for bundles. Item name is url. */ + +exports.set = async function (prefix, item, data) +{ + return set_item(item, data, list_by_prefix[prefix]); +} + +exports.get = function (prefix, item) +{ + return list_by_prefix[prefix].map.get(item); +} + +exports.remove = async function (prefix, item) +{ + return remove_item(item, list_by_prefix[prefix]); +} + +exports.replace = async function (prefix, old_item, new_item, + new_data=undefined) +{ + return replace_item(old_item, new_item, list_by_prefix[prefix], new_data); +} + +exports.get_all_names = function (prefix) +{ + return list_items(list_by_prefix[prefix]); +} + +exports.get_all_names_it = function (prefix) +{ + return list_items_it(list_by_prefix[prefix]); +} + +exports.get_all = function (prefix) +{ + return list_entries(list_by_prefix[prefix]); +} + +exports.get_all_it = function (prefix) +{ + return list_entries_it(list_by_prefix[prefix]); +} + +/* Finally, a quick way to wipe all the data. */ +// TODO: maybe delete items in such order that none of them ever references +// an already-deleted one? +exports.clear = async function () +{ + let lists = list_prefixes.map((p) => list_by_prefix[p]); + + for (let list of lists) + await lock(list.lock); + + for (let list of lists) { + + let change = { + prefix : list.prefix, + new_val : undefined + }; + + for (let [item, val] of list_entries_it(list)) { + change.item = item; + change.old_val = val; + broadcast_change(change, list); + } + + list.map = new Map(); + } + + await browser.storage.local.clear(); + + for (let list of lists) + unlock(list.lock); +} + +export default make_once(init); diff --git a/background/storage_server.mjs b/background/storage_server.mjs new file mode 100644 index 0000000..3a141ff --- /dev/null +++ b/background/storage_server.mjs @@ -0,0 +1,58 @@ +/** +* Myext storage through connection (server side) +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import listen_for_connection from './message_server.mjs'; +import get_storage from './storage.mjs'; +import {TYPE_PREFIX} from '/common/stored_types.mjs'; +import CONNECTION_TYPE from '/common/connection_types.mjs'; + +var storage; + +async function handle_remote_call(port, message) +{ + let [call_id, func, args] = message; + + try { + let result = await Promise.resolve(storage[func](...args)); + port.postMessage({call_id, result}); + } catch (error) { + error = error + ''; + port.postMessage({call_id, error}); + } +} + +function remove_storage_listener(cb) { + storage.remove_change_listener(cb); +} + +function new_connection(port) +{ + console.log("new remote storage connection!"); + + port.postMessage({ + [TYPE_PREFIX.SCRIPT] : storage.get_all(TYPE_PREFIX.SCRIPT), + [TYPE_PREFIX.BUNDLE] : storage.get_all(TYPE_PREFIX.BUNDLE), + [TYPE_PREFIX.PAGE] : storage.get_all(TYPE_PREFIX.PAGE) + }); + + let handle_change = change => port.postMessage(change); + + storage.add_change_listener(handle_change); + + port.onMessage.addListener(m => handle_remote_call(port, m)); + port.onDisconnect.addListener(() => remove_storage_listener(handle_change)); +} + +export default async function start() +{ + storage = await get_storage(); + + listen_for_connection(CONNECTION_TYPE.REMOTE_STORAGE, new_connection); +} diff --git a/background/url_item.mjs b/background/url_item.mjs new file mode 100644 index 0000000..2ee1d6d --- /dev/null +++ b/background/url_item.mjs @@ -0,0 +1,18 @@ +/** +* Myext stripping url from query and target +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +export default function url_item(url) +{ + let url_re = /^([^?#]*).*$/; + let match = url_re.exec(url); + return match[1]; +} diff --git a/common/browser.mjs b/common/browser.mjs new file mode 100644 index 0000000..0d1b233 --- /dev/null +++ b/common/browser.mjs @@ -0,0 +1,18 @@ +/** +* Myext WebExtension API access normalization +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +/* + * This module normalizes access to WebExtension apis between + * chrome-based and firefox-based browsers. + */ + +export default (window.browser === undefined) ? chrome : browser; diff --git a/common/connection_types.mjs b/common/connection_types.mjs new file mode 100644 index 0000000..12d6de3 --- /dev/null +++ b/common/connection_types.mjs @@ -0,0 +1,21 @@ +/** +* Myext background scripts message connection types "enum" +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* + * Those need to be strings so they can be used as 'name' parameter + * to browser.runtime.connect() + */ + +const CONNECTION_TYPE = { + REMOTE_STORAGE : "0", + PAGE_ACTIONS : "1" +}; + +export default CONNECTION_TYPE; diff --git a/common/is_background.mjs b/common/is_background.mjs new file mode 100644 index 0000000..ef728a7 --- /dev/null +++ b/common/is_background.mjs @@ -0,0 +1,17 @@ +/** +* Myext programmatic check of where the script is being run +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* This needs to be changed if we ever modify the html file path. */ + +export default function is_background() +{ + return window.location.protocol === "moz-extension:" && + window.location.pathname === "/background/background.html"; +} diff --git a/common/lock.mjs b/common/lock.mjs new file mode 100644 index 0000000..596dd9c --- /dev/null +++ b/common/lock.mjs @@ -0,0 +1,55 @@ +/** +* Myext lock (aka binary semaphore aka mutex) +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* + * Javascript runs single-threaded, with an event loop. Because of that, + * explicit synchronization is often not needed. An exception is when we use + * an API function that must wait. Ajax is an example. Callback passed to ajax + * call doesn't get called immediately, but after some time. In the meantime + * some other piece of code might get to execute and modify some variables. + * Access to WebExtension local storage is another situation where this problem + * can occur. + * + * This is a solution. A lock object, that can be used to delay execution of + * some code until other code finishes its critical work. Locking is wrapped + * in a promise. + */ + +"use strict"; + +export function make_lock() { + return {free: true, queue: []}; +} + +function _lock(lock, cb) { + if (lock.free) { + lock.free = false; + setTimeout(cb); + } else { + lock.queue.push(cb); + } +} + +export function lock(lock) { + return new Promise((resolve, reject) => _lock(lock, resolve)); +} + +export function unlock(lock) { + if (lock.free) + throw new Exception("Attempting to release a free lock"); + + if (lock.queue.length === 0) { + lock.free = true; + } else { + let cb = lock.queue[0]; + lock.queue.splice(0, 1); + setTimeout(cb); + } +} diff --git a/common/once.mjs b/common/once.mjs new file mode 100644 index 0000000..0f76366 --- /dev/null +++ b/common/once.mjs @@ -0,0 +1,42 @@ +/** +* Myext feature initialization promise +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +/* + * This module provides an easy way to wrap an async function into a promise + * so that it only gets executed once. + */ + +async function assign_result(state, result_producer) +{ + state.result = await result_producer(); + state.ready = true; + for (let cb of state.waiting) + setTimeout(cb, 0, state.result); + state.waiting = undefined; +} + +async function get_result(state) +{ + if (state.ready) + return state.result; + + return new Promise((resolve, reject) => state.waiting.push(resolve)); +} + +export function make_once(result_producer) +{ + let state = {waiting : [], ready : false, result : undefined}; + assign_result(state, result_producer); + return () => get_result(state); +} + +export default make_once; diff --git a/common/storage_client.mjs b/common/storage_client.mjs new file mode 100644 index 0000000..8260ad7 --- /dev/null +++ b/common/storage_client.mjs @@ -0,0 +1,186 @@ +/** +* Myext storage through connection (client side) +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +import CONNECTION_TYPE from './connection_types.mjs'; +import {TYPE_PREFIX, list_prefixes} from '/common/stored_types.mjs'; +import make_once from './once.mjs'; +import browser from '/common/browser.mjs'; + +var call_id = 0; +var port; +var calls_waiting = new Map(); + +function set_call_callback(resolve, reject, func, args) +{ + port.postMessage([call_id, func, args]); + calls_waiting.set(call_id++, [resolve, reject]); +} + +async function remote_call(func, args) +{ + return new Promise((resolve, reject) => + set_call_callback(resolve, reject, func, args)); +} + +function handle_message(message) +{ + let callbacks = calls_waiting.get(message.call_id); + if (callbacks === undefined) { + handle_change(message); + return; + } + + let [resolve, reject] = callbacks; + calls_waiting.delete(message.call_id); + if (message.error !== undefined) + setTimeout(reject, 0, message.error); + else + setTimeout(resolve, 0, message.result); +} + +function list(name, prefix) +{ + return {prefix, name, listeners : new Set()}; +} + +var scripts = list("scripts", TYPE_PREFIX.SCRIPT); +var bundles = list("bundles", TYPE_PREFIX.BUNDLE); +var pages = list("pages", TYPE_PREFIX.PAGE); + +const list_by_prefix = { + [TYPE_PREFIX.SCRIPT] : scripts, + [TYPE_PREFIX.BUNDLE] : bundles, + [TYPE_PREFIX.PAGE] : pages +}; + +var resolve_init; + +function handle_first_message(message) +{ + for (let prefix of Object.keys(message)) + list_by_prefix[prefix].map = new Map(message[prefix]); + + port.onMessage.removeListener(handle_first_message); + port.onMessage.addListener(handle_message); + + resolve_init(); +} + +function handle_change(change) +{ + let list = list_by_prefix[change.prefix]; + + if (change.new_val === undefined) + list.map.delete(change.item); + else + list.map.set(change.item, change.new_val); + + for (let listener_callback of list.listeners) + listener_callback(change); +} + +var exports = {}; + +function start_connection(resolve) +{ + resolve_init = resolve; + port = browser.runtime.connect({name : CONNECTION_TYPE.REMOTE_STORAGE}); + port.onMessage.addListener(handle_first_message); +} + +async function init() { + await new Promise((resolve, reject) => start_connection(resolve)); + return exports; +} + +for (let call_name of ["set", "remove", "replace", "clear"]) + exports [call_name] = (...args) => remote_call(call_name, args); + +// TODO: Much of the code below is copy-pasted from /background/storage.mjs. +// This should later be refactored into a separate module +// to avoid duplication. + +/* + * Facilitate listening to changes + */ + +exports.add_change_listener = function (cb, prefixes=list_prefixes) +{ + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.add(cb); +} + +exports.remove_change_listener = function (cb, prefixes=list_prefixes) +{ + if (typeof(prefixes) === "string") + prefixes = [prefixes]; + + for (let prefix of prefixes) + list_by_prefix[prefix].listeners.delete(cb); +} + +/* Prepare some hepler functions to get elements of a list */ + +function list_items_it(list, with_values=false) +{ + return with_values ? list.map.entries() : list.map.keys(); +} + +function list_entries_it(list) +{ + return list_items_it(list, true); +} + +function list_items(list, with_values=false) +{ + let array = []; + + for (let item of list_items_it(list, with_values)) + array.push(item); + + return array; +} + +function list_entries(list) +{ + return list_items(list, true); +} + +exports.get = function (prefix, item) +{ + return list_by_prefix[prefix].map.get(item); +} + +exports.get_all_names = function (prefix) +{ + return list_items(list_by_prefix[prefix]); +} + +exports.get_all_names_it = function (prefix) +{ + return list_items_it(list_by_prefix[prefix]); +} + +exports.get_all = function (prefix) +{ + return list_entries(list_by_prefix[prefix]); +} + +exports.get_all_it = function (prefix) +{ + return list_entries_it(list_by_prefix[prefix]); +} + +export default make_once(init); diff --git a/common/stored_types.mjs b/common/stored_types.mjs new file mode 100644 index 0000000..8545d44 --- /dev/null +++ b/common/stored_types.mjs @@ -0,0 +1,38 @@ +/** +* Myext stored item types "enum" +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +/* + * Key for item that is stored in quantity (script, page) is constructed by + * prepending its name with first letter of its list name. However, we also + * need to store some items that don't belong to any list. Let's call them + * persisted variables. In such case item's key is its "name" prepended with + * an underscore. + */ + +const TYPE_PREFIX = { + PAGE : "p", + BUNDLE : "b", + SCRIPT : "s", + VAR : "_" +}; + +const TYPE_NAME = { + [TYPE_PREFIX.PAGE] : "page", + [TYPE_PREFIX.BUNDLE] : "bundle", + [TYPE_PREFIX.SCRIPT] : "script" +} + +const list_prefixes = [ + TYPE_PREFIX.PAGE, + TYPE_PREFIX.BUNDLE, + TYPE_PREFIX.SCRIPT +]; + +export {TYPE_PREFIX, TYPE_NAME, list_prefixes}; diff --git a/content/main.js b/content/main.js new file mode 100644 index 0000000..12a94c9 --- /dev/null +++ b/content/main.js @@ -0,0 +1,95 @@ +/** +* Myext main content script run in all frames +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +var url_re = /^([^#]*)((#[^#]*)(#.*)?)?$/; +var match = url_re.exec(document.URL); +var base_url = match[1]; +var first_target = match[3]; +var second_target = match[4]; + +var block = true; +if (first_target !== undefined && + first_target === "#myext-allow") { + block = false; + console.log(["allowing", document.URL]); + if (second_target !== undefined) + window.location.href = base_url + second_target; + else + history.replaceState(null, "", base_url); +} else { + console.log(["not allowing", document.URL]); +} + +function handle_mutation(mutations, observer) +{ + if (document.readyState === 'complete') { + console.log("complete"); + observer.disconnect(); + return; + } + for (let mutation of mutations) { + for (let node of mutation.addedNodes) { + if (node.tagName === "SCRIPT") + block_script(node); + else + sanitize_attributes(node); + } + } +} + +function block_script(node) +{ + console.log(node); + + /* + * Disabling scripts this way allows them to still be relatively accessed + * in case they contain some useful data. + */ + if (node.hasAttribute("type")) + node.setAttribute("blocked-type", node.getAttribute("type")); + node.setAttribute("type", "application/json"); +} + +function sanitize_attributes(node) +{ + if (node.attributes === undefined) + return; + + /* We have to do it in 2 loops, removing attribute modifies our iterator */ + let attr_names = []; + for (let attr of node.attributes) { + let attr_name = attr.localName; + if (attr_name.startsWith("on")) + attr_names.push(attr_name); + } + + for (let attr_name of attr_names) { + node.removeAttribute(attr_name); + console.log("sanitized", attr_name); + } +} + +async function run_module() +{ + let src = chrome.runtime.getURL("content/page_actions.mjs"); + let module = await import(src); + module.default(); +} + +if (block) { + var observer = new MutationObserver(handle_mutation); + observer.observe(document.documentElement, { + attributes: true, + childList: true, + subtree: true + }); +} + +run_module(); diff --git a/content/page_actions.mjs b/content/page_actions.mjs new file mode 100644 index 0000000..3ce5b73 --- /dev/null +++ b/content/page_actions.mjs @@ -0,0 +1,58 @@ +/** +* Myext handling of page actions in content scripts +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +import CONNECTION_TYPE from '/common/connection_types.mjs'; +import make_once from '/common/once.mjs'; +import browser from '/common/browser.mjs'; + +var port; +var loaded = false; +var scripts_awaiting = []; + +function handle_message(message) +{ + console.log(["message", message]); + + if (message.inject === undefined) + return; + + for (let script_text of message.inject) { + if (loaded) + add_script(script_text); + else + scripts_awaiting.push(script_text); + } +} + +function document_loaded(event) +{ + console.log("loaded"); + + loaded = true; + + for (let script_text of scripts_awaiting) + add_script(script_text); + + scripts_awaiting = undefined; +} + +function add_script(script_text) +{ + let script = document.createElement("script"); + script.textContent = script_text; + document.body.appendChild(script); +} + +export default function main() { + document.addEventListener("DOMContentLoaded", document_loaded); + port = browser.runtime.connect({name : CONNECTION_TYPE.PAGE_ACTIONS}); + port.onMessage.addListener(handle_message); + port.postMessage({url: document.URL}); +} diff --git a/html/display-panel.html b/html/display-panel.html new file mode 100644 index 0000000..9c8de5e --- /dev/null +++ b/html/display-panel.html @@ -0,0 +1,11 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <title>Myext popup</title> + </head> + <body> + <button id="settings_but" type="button">Settings</button> + <script src="./display-panel.mjs" type="module"></script> + </body> +</html> diff --git a/html/display-panel.mjs b/html/display-panel.mjs new file mode 100644 index 0000000..38f2a42 --- /dev/null +++ b/html/display-panel.mjs @@ -0,0 +1,16 @@ +/** +* Myext display panel logic +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +import browser from '/common/browser.mjs'; + +document.getElementById("settings_but") + .addEventListener("click", (e) => browser.runtime.openOptionsPage()); diff --git a/html/options.html b/html/options.html new file mode 100644 index 0000000..921b423 --- /dev/null +++ b/html/options.html @@ -0,0 +1,175 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"/> + <title>Myext options</title> + <style> + input[type="checkbox"], input[type="radio"], + .hide { + display: none; + } + + /* pages list */ + #page_components_ul { + max-height: 80vh; + overflow-y: auto; + } + #page_components_ul li.dragover_li { + border-top: 2px solid blue; + } + #page_components_ul li { + border-top: 2px solid white; + } + li[draggable=true] * { + pointer-events: none; + } + li[draggable=true] label, + li[draggable=true] button { + pointer-events: auto; + } + + /* tabbed view */ + #show_pages:not(:checked) ~ #pages, + #show_bundles:not(:checked) ~ #bundles, + #show_scripts:not(:checked) ~ #scripts { + display: none; + } + + #show_pages:checked ~ #pages_lbl, + #show_bundles:checked ~ #bundles_lbl, + #show_scripts:checked ~ #scripts_lbl { + border-left: 2px solid green; + border-right: 2px solid green; + } + + body > div { + border-top: 2px solid green; + } + + .tab_head { + display: inline-block; + } + + /* popup window with list of selectable components for adding */ + #select_components_window { + position: fixed; + width: 100vw; + height: 100vh; + left: 0; + top: 0; + background-color: rgba(0,0,0,0.4); + z-index: 1; + overflow: auto; + vertical-align: center; + horizontal-align: center; + } + + #select_components_frame { + background-color: white; + width: 50vw; + } + </style> + </head> + <body> + <!-- The invisible div below is for elements that will be cloned. --> + <div style="display: none;"> + <li id="item_li_template"> + <span></span> + <button> Edit </button> + <button> Remove </button> + </li> + <li id="component_li_template"> + <span></span> + <button> Remove </button> + </li> + <li id="selectable_component_li_template"> + <input type="checkbox" style="display: inline;"></input> + <span></span> + </li> + </div> + + <input type="radio" name="tabs" id="show_pages" checked></input> + <input type="radio" name="tabs" id="show_bundles"></input> + <input type="radio" name="tabs" id="show_scripts"></input> + <label for="show_pages" id="pages_lbl" + class="tab_head"> Pages </label> + <label for="show_bundles" id="bundles_lbl" + class="tab_head"> Bundles </label> + <label for="show_scripts" id="scripts_lbl" + class="tab_head"> Scripts </label> + + <div id="pages"> + <ul id="pages_ul"> + <li id="work_page_li" class="hide"> + <label for="page_url_field">URL: </label> + <input id="page_url_field"></input> + <ul id="page_components_ul"> + <li id="empty_page_component_li" class="hide"></li> + </ul> + <input id="page_allow_chbx" type="checkbox" style="display: inline;"></input> + <label for="page_allow_chbx">Allow native scripts</label> + <button id="page_select_components_but"> + Add scripts + </button> + <br/> + <button id="page_save_but" type="button"> Save </button> + <button id="page_discard_but" type="button"> Cancel </button> + </li> + </ul> + <button id="add_page_but" type="button"> Add page </button> + </div> + + <div id="bundles"> + <ul id="bundles_ul"> + <li id="work_bundle_li" class="hide"> + <label for="bundle_name_field"> Name: </label> + <input id="bundle_name_field"></input> + <ul id="bundle_components_ul"> + <li id="empty_bundle_component_li" class="hide"></li> + </ul> + <button id="bundle_select_components_but"> + Add scripts + </button> + <br/> + <button id="bundle_save_but"> Save </button> + <button id="bundle_discard_but"> Cancel </button> + </li> + </ul> + <button id="add_bundle_but" type="button"> Add bundle </button> + </div> + + <div id="scripts"> + <ul id="scripts_ul"> + <li id="work_script_li" class="hide"> + <label for="script_name_field"> Name: </label> + <input id="script_name_field"></input> + <br/> + <label for="script_url_field"> URL: </label> + <input id="script_url_field"></input> + <br/> + <label for="script_sha256_field"> sha256: </label> + <input id="script_sha256_field"></input> + <br/> + <label for="script_contents_field"> contents: </label> + <textarea id="script_contents_field" rows="20" cols="80"></textarea> + <br/> + <button id="script_save_but"> Save </button> + <button id="script_discard_but"> Cancel </button> + </li> + </ul> + <button id="add_script_but" type="button"> Add script </button> + </div> + + <div id="select_components_window" class="hide" position="absolute"> + <div id="select_components_frame"> + <ul id="selectable_components_ul"> + + </ul> + <button id="commit_components_but"> Add </button> + <button id="cancel_components_but"> Cancel </button> + </div> + </div> + + <script src="./options_main.mjs" type="module"></script> + </body> +</html> diff --git a/html/options_main.mjs b/html/options_main.mjs new file mode 100644 index 0000000..f288971 --- /dev/null +++ b/html/options_main.mjs @@ -0,0 +1,398 @@ +/** +* Myext HTML options page main script +* +* Copyright (C) 2021 Wojtek Kosior +* +* Dual-licensed under: +* - 0BSD license +* - GPLv3 or (at your option) any later version +*/ + +"use strict"; + +import get_storage from '/common/storage_client.mjs'; +import {TYPE_PREFIX, TYPE_NAME, list_prefixes} from '/common/stored_types.mjs'; + +var storage; + +const item_li_template = document.getElementById("item_li_template"); +const component_li_template = document.getElementById("component_li_template"); +const selectable_component_li_template = + document.getElementById("selectable_component_li_template"); +/* Make sure they are later cloned without id. */ +item_li_template.removeAttribute("id"); +component_li_template.removeAttribute("id"); +selectable_component_li_template.removeAttribute("id"); + +function item_li_id(prefix, item) +{ + return `li_${prefix}_${item}`; +} + +function add_li(prefix, item, at_the_end=false) +{ + let ul = ul_by_prefix[prefix]; + let li = item_li_template.cloneNode(true); + li.id = item_li_id(prefix, item); + + let span = li.firstElementChild; + span.textContent = item; + + let edit_button = span.nextElementSibling; + edit_button.addEventListener("click", () => edit_item(prefix, item)); + + let remove_button = edit_button.nextElementSibling; + remove_button.addEventListener("click", () => storage.remove(prefix, item)); + + if (!at_the_end) { + for (let element of ul.ul.children) { + if (element.id < li.id || element.id.startsWith("work_")) + continue; + + ul.ul.insertBefore(li, element); + return; + } + } + + ul.ul.appendChild(li); +} + +const selectable_components_ul = + document.getElementById("selectable_components_ul"); + +function selectable_li_id(prefix, item) +{ + return `sli_${prefix}_${item}`; +} + +function add_selectable(prefix, name) +{ + if (prefix === TYPE_PREFIX.PAGE) + return; + + let li = selectable_component_li_template.cloneNode(true); + li.id = selectable_li_id(prefix, name); + li.setAttribute("data-prefix", prefix); + li.setAttribute("data-name", name); + + let chbx = li.firstElementChild; + let span = chbx.nextElementSibling; + + span.textContent = `${name} (${TYPE_NAME[prefix]})`; + + selectable_components_ul.appendChild(li); +} + +/* + * Used to construct and update components list of edited + * bundle as well as edited page. + */ +function add_components(ul, components) +{ + let components_ul = ul.work_name_input.nextElementSibling; + + for (let component of components) { + let [prefix, name] = component; + let li = component_li_template.cloneNode(true); + li.setAttribute("data-prefix", prefix); + li.setAttribute("data-name", name); + let span = li.firstElementChild; + span.textContent = `${name} (${TYPE_NAME[prefix]})`; + let remove_but = span.nextElementSibling; + remove_but.addEventListener("click", () => + components_ul.removeChild(li)); + components_ul.appendChild(li); + } + + components_ul.appendChild(ul.work_empty_component_li); +} + +/* Used to reset edited bundle as well as edited page. */ +function generic_reset_work_li(ul, item, components) +{ + if (item === undefined) { + item = ""; + components = []; + }; + + ul.work_name_input.value = item; + let old_components_ul = ul.work_name_input.nextElementSibling; + let components_ul = old_components_ul.cloneNode(false); + + ul.work_li.insertBefore(components_ul, old_components_ul); + ul.work_li.removeChild(old_components_ul); + + add_components(ul, components); +} + +function reset_work_page_li(ul, item, settings) +{ + ul.work_page_allow_chbx.checked = !!settings?.allow; + generic_reset_work_li(ul, item, settings?.components); +} + +/* Used to get edited bundle as well as edited page data for saving. */ +function generic_work_li_data(ul) +{ + let components_ul = ul.work_name_input.nextElementSibling; + let component_li = components_ul.firstElementChild; + + let components = []; + + /* Last list element is empty li with id set. */ + while (component_li.id === '') { + components.push([component_li.getAttribute("data-prefix"), + component_li.getAttribute("data-name")]); + component_li = component_li.nextElementSibling; + } + + return [ul.work_name_input.value, components]; +} + +function work_page_li_data(ul) +{ + let [url, components] = generic_work_li_data(ul); + let settings = {components, allow : !!ul.work_page_allow_chbx.checked}; + + return [url, settings]; +} + +const script_url_input = document.getElementById("script_url_field"); +const script_sha256_input = document.getElementById("script_sha256_field"); +const script_contents_field = document.getElementById("script_contents_field"); + +function maybe_string(maybe_defined) +{ + return maybe_defined === undefined ? "" : maybe_defined + ""; +} + +function reset_work_script_li(ul, name, data) +{ + ul.work_name_input.value = maybe_string(name); + script_url_input.value = maybe_string(data?.url); + script_sha256_input.value = maybe_string(data?.hash); + script_contents_field.value = maybe_string(data?.text); +} + +function work_script_li_data(ul) +{ + return [ul.work_name_input.value, { + url : script_url_input.value, + hash : script_sha256_input.value, + text : script_contents_field.value + }]; +} + +function cancel_work(prefix) +{ + let ul = ul_by_prefix[prefix]; + + if (ul.state === UL_STATE.IDLE) + return; + + if (ul.state === UL_STATE.EDITING_ENTRY) { + add_li(prefix, ul.edited_item); + } + + ul.work_li.classList.add("hide"); + ul.state = UL_STATE.IDLE; +} + +function save_work(prefix) +{ + let ul = ul_by_prefix[prefix]; + + if (ul.state === UL_STATE.IDLE) + return; + + let [item, data] = ul.get_work_li_data(ul); + + if (prefix == TYPE_PREFIX.PAGE) + + /* Here we fire promises and return without waiting. */ + + if (ul.state === UL_STATE.EDITING_ENTRY) + storage.replace(prefix, ul.edited_item, item, data); + + if (ul.state === UL_STATE.ADDING_ENTRY) + storage.set(prefix, item, data); + + cancel_work(prefix); +} + +function edit_item(prefix, item) +{ + cancel_work(prefix); + + let ul = ul_by_prefix[prefix]; + let li = document.getElementById(item_li_id(prefix, item)); + ul.reset_work_li(ul, item, storage.get(prefix, item)); + ul.ul.insertBefore(ul.work_li, li); + ul.ul.removeChild(li); + ul.work_li.classList.remove("hide"); + + ul.state = UL_STATE.EDITING_ENTRY; + ul.edited_item = item; +} + +function add_new_item(prefix) +{ + cancel_work(prefix); + + let ul = ul_by_prefix[prefix]; + ul.reset_work_li(ul); + ul.work_li.classList.remove("hide"); + ul.ul.appendChild(ul.work_li); + + ul.state = UL_STATE.ADDING_ENTRY; +} + +const select_components_window = + document.getElementById("select_components_window"); +var select_prefix; + +function select_components(prefix) +{ + select_prefix = prefix; + select_components_window.classList.remove("hide"); + + for (let li of selectable_components_ul.children) { + let chbx = li.firstElementChild; + chbx.checked = false; + } +} + +function commit_components() +{ + let selected = []; + + for (let li of selectable_components_ul.children) { + let chbx = li.firstElementChild; + if (!chbx.checked) + continue; + + selected.push([li.getAttribute("data-prefix"), + li.getAttribute("data-name")]); + } + + add_components(ul_by_prefix[select_prefix], selected); + cancel_components(); +} + +function cancel_components() +{ + select_components_window.classList.add("hide"); +} + +const UL_STATE = { + EDITING_ENTRY : 0, + ADDING_ENTRY : 1, + IDLE : 2 +}; + +const ul_by_prefix = { + [TYPE_PREFIX.PAGE] : { + ul : document.getElementById("pages_ul"), + work_li : document.getElementById("work_page_li"), + work_name_input : document.getElementById("page_url_field"), + work_empty_component_li : + document.getElementById("empty_page_component_li"), + work_page_allow_chbx : document.getElementById("page_allow_chbx"), + reset_work_li : reset_work_page_li, + get_work_li_data : work_page_li_data, + state : UL_STATE.IDLE, + edited_item : undefined, + }, + [TYPE_PREFIX.BUNDLE] : { + ul : document.getElementById("bundles_ul"), + work_li : document.getElementById("work_bundle_li"), + work_name_input : document.getElementById("bundle_name_field"), + work_empty_component_li : + document.getElementById("empty_bundle_component_li"), + reset_work_li : generic_reset_work_li, + get_work_li_data : generic_work_li_data, + state : UL_STATE.IDLE, + edited_item : undefined, + }, + [TYPE_PREFIX.SCRIPT] : { + ul : document.getElementById("scripts_ul"), + work_li : document.getElementById("work_script_li"), + work_name_input : document.getElementById("script_name_field"), + reset_work_li : reset_work_script_li, + get_work_li_data : work_script_li_data, + state : UL_STATE.IDLE, + edited_item : undefined, + } +} + +async function main() +{ + storage = await get_storage(); + + for (let prefix of list_prefixes) { + for (let item of storage.get_all_names(prefix).sort()) { + add_li(prefix, item, true); + add_selectable(prefix, item); + } + } + + storage.add_change_listener(handle_change); + + let commit_components_but = + document.getElementById("commit_components_but"); + let cancel_components_but = + document.getElementById("cancel_components_but"); + commit_components_but.addEventListener("click", commit_components); + cancel_components_but.addEventListener("click", cancel_components); + + for (let prefix of list_prefixes) { + let add_but = document.getElementById(`add_${TYPE_NAME[prefix]}_but`); + let discard_but = + document.getElementById(`${TYPE_NAME[prefix]}_discard_but`); + let save_but = document.getElementById(`${TYPE_NAME[prefix]}_save_but`); + let select_components_but = document.getElementById( + `${TYPE_NAME[prefix]}_select_components_but` + ); + + add_but.addEventListener("click", () => add_new_item(prefix)); + discard_but.addEventListener("click", () => cancel_work(prefix)); + save_but.addEventListener("click", () => save_work(prefix)); + if (select_components_but === null) + continue; + + select_components_but.addEventListener( + "click", + () => select_components(prefix) + ); + } +} + +function handle_change(change) +{ + if (change.old_val === undefined) { + add_li(change.prefix, change.item); + add_selectable(change.prefix, change.item); + return; + } + + if (change.new_val !== undefined) + return; + + let ul = ul_by_prefix[change.prefix]; + if (ul.state === UL_STATE.EDITING_ENTRY && ul.edited_item === change.item) { + ul.state = UL_STATE.ADDING_ENTRY; + return; + } + + let li = document.getElementById(item_li_id(change.prefix, change.item)); + ul.ul.removeChild(li); + + if (change.prefix === TYPE_PREFIX.PAGE) + return; + + let sli = document.getElementById(selectable_li_id(change.prefix, + change.item)); + selectable_components_ul.removeChild(sli); +} + +main(); diff --git a/icons/myext.png b/icons/myext.png Binary files differnew file mode 100644 index 0000000..1dd025b --- /dev/null +++ b/icons/myext.png diff --git a/licenses/0bsd.txt b/licenses/0bsd.txt new file mode 100644 index 0000000..c5dc48b --- /dev/null +++ b/licenses/0bsd.txt @@ -0,0 +1,12 @@ + 0BSD LICENSE + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/licenses/gpl-3.0.txt b/licenses/gpl-3.0.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/licenses/gpl-3.0.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/licenses/mit(expat).txt b/licenses/mit(expat).txt new file mode 100644 index 0000000..bace2d5 --- /dev/null +++ b/licenses/mit(expat).txt @@ -0,0 +1,19 @@ + MIT LICENSE (EXPAT LICENSE) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..d9ff113 --- /dev/null +++ b/manifest.json @@ -0,0 +1,60 @@ +{ + "manifest_version": 2, + "name": "My extension", + "short_name": "Myext", + "version": "0.0.0", + "author": "various", + "description": "Kill the web&js", + "applications": { + "gecko": { + "id": "{6fe13369-88e9-440f-b837-5012fb3bedec}", + "strict_min_version": "60.0" + } + }, + "icons":{ + "64": "icons/myext.png" + }, + "permissions": [ + "contextMenus", + "webRequest", + "webRequestBlocking", + "activeTab", + "notifications", + "sessions", + "storage", + "tabs", + "<all_urls>" + ], + "browser_action": { + "browser_style": true, + "default_icon": { + "64": "icons/myext.png" + }, + "default_title": "Myext", + "default_popup": "html/display-panel.html" + }, + "options_ui": { + "page": "html/options.html", + "open_in_tab": true + }, + "web_accessible_resources": [ + "/common/connection_types.mjs", + "/common/once.mjs", + "/common/browser.mjs", + "/content/page_actions.mjs" + ], + "background": { + "page": "background/background.html" + }, + "content_scripts": [ + { + "run_at": "document_start", + "matches": ["<all_urls>"], + "match_about_blank": true, + "all_frames": true, + "js": [ + "content/main.js" + ] + } + ] +} |