aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-05-10 18:07:05 +0200
committerWojtek Kosior <wk@koszkonutek-tmp.pl.eu.org>2021-05-10 18:18:52 +0200
commit01937dc9d5215ef96ce756e3ccda51bf29032f58 (patch)
tree609ec5bb48c692796520f7982c06b30633038588
downloadbrowser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.tar.gz
browser-extension-01937dc9d5215ef96ce756e3ccda51bf29032f58.zip
initial commit
-rw-r--r--COPYING.txt4
-rw-r--r--README.txt17
-rw-r--r--TODOS.org51
-rw-r--r--background/ResponseHandler.mjs257
-rw-r--r--background/ResponseMetaData.mjs107
-rw-r--r--background/ResponseProcessor.mjs145
-rw-r--r--background/background.html7
-rw-r--r--background/main.mjs168
-rw-r--r--background/message_server.mjs31
-rw-r--r--background/page_actions_server.mjs145
-rw-r--r--background/policy_smuggler.mjs60
-rw-r--r--background/reverse_use_info.mjs93
-rw-r--r--background/script_injector.mjs122
-rw-r--r--background/sha256.mjs524
-rw-r--r--background/storage.mjs380
-rw-r--r--background/storage_server.mjs58
-rw-r--r--background/url_item.mjs18
-rw-r--r--common/browser.mjs18
-rw-r--r--common/connection_types.mjs21
-rw-r--r--common/is_background.mjs17
-rw-r--r--common/lock.mjs55
-rw-r--r--common/once.mjs42
-rw-r--r--common/storage_client.mjs186
-rw-r--r--common/stored_types.mjs38
-rw-r--r--content/main.js95
-rw-r--r--content/page_actions.mjs58
-rw-r--r--html/display-panel.html11
-rw-r--r--html/display-panel.mjs16
-rw-r--r--html/options.html175
-rw-r--r--html/options_main.mjs398
-rw-r--r--icons/myext.pngbin0 -> 13130 bytes
-rw-r--r--licenses/0bsd.txt12
-rw-r--r--licenses/gpl-3.0.txt674
-rw-r--r--licenses/mit(expat).txt19
-rw-r--r--manifest.json60
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
new file mode 100644
index 0000000..1dd025b
--- /dev/null
+++ b/icons/myext.png
Binary files differ
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"
+ ]
+ }
+ ]
+}