diff --git a/src/bg/LifeCycle.js b/src/bg/LifeCycle.js index a3dd748..6e4d4e2 100644 --- a/src/bg/LifeCycle.js +++ b/src/bg/LifeCycle.js @@ -229,7 +229,7 @@ var LifeCycle = (() => { if (!previousVersion) return; - await include("/lib/Ver.js"); + await include("/nscl/common/Ver.js"); previousVersion = new Ver(previousVersion); let currentVersion = new Ver(browser.runtime.getManifest().version); let upgrading = Ver.is(previousVersion, "<=", currentVersion); @@ -263,7 +263,7 @@ var LifeCycle = (() => { // user doesn't want us to remember temporary settings across updates: bail out return; } - await include("/lib/Ver.js"); + await include("/nscl/common/Ver.js"); if (Ver.is(details.version, "<", browser.runtime.getManifest().version)) { // downgrade: temporary survival might not be supported, and we don't care return; diff --git a/src/bg/Settings.js b/src/bg/Settings.js index 180e5c0..daf1288 100644 --- a/src/bg/Settings.js +++ b/src/bg/Settings.js @@ -141,7 +141,7 @@ var Settings = { ? Object.assign(ns[storage], settings[storage]) : ns[storage] = Object.assign({}, ns.defaults[storage])) )); if (ns.local.debug !== oldDebug) { - await include("/lib/log.js"); + await include("/nscl/common/log.js"); if (oldDebug) debug = () => {}; } diff --git a/src/bg/main.js b/src/bg/main.js index 5b9f77e..2176ead 100644 --- a/src/bg/main.js +++ b/src/bg/main.js @@ -310,7 +310,7 @@ debug("Collected seen", seen); return seen; } catch (e) { - await include("/lib/restricted.js"); + await include("/nscl/common/restricted.js"); if (!isRestrictedURL((await browser.tabs.get(tabId)).url)) { // probably a page where content scripts cannot run, let's open the options instead error(e, "Cannot collect noscript activity data"); diff --git a/src/common/CapsCSP.js b/src/common/CapsCSP.js deleted file mode 100644 index 4ac91f2..0000000 --- a/src/common/CapsCSP.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; - -function CapsCSP(baseCSP = new CSP()) { - return Object.assign(baseCSP, { - types: ["script", "object", "media", "font"], - dataUriTypes: ["font", "media", "object"], - buildFromCapabilities(capabilities, blockHttp = false) { - let forbidData = new Set(this.dataUriTypes.filter(t => !capabilities.has(t))); - let blockedTypes = new Set(this.types.filter(t => !capabilities.has(t))); - if(!capabilities.has("script")) { - blockedTypes.add({name: "script-src-elem"}); - blockedTypes.add({name: "script-src-attr"}); - blockedTypes.add("worker"); - if (!blockedTypes.has("object")) { - // data: URIs loaded in objects may run scripts - blockedTypes.add({type: "object", value: "http:"}); - } - } - - if (!blockHttp) { - // HTTP is blocked in onBeforeRequest, let's allow it only and block - // for instance data: and blob: URIs - for (let type of this.dataUriTypes) { - if (blockedTypes.delete(type)) { - blockedTypes.add({type, value: "http:"}); - } - } - } - - return blockedTypes.size ? this.buildBlocker(...blockedTypes) : null; - } - }); -} diff --git a/src/common/Storage.js b/src/common/Storage.js deleted file mode 100644 index 78c481b..0000000 --- a/src/common/Storage.js +++ /dev/null @@ -1,169 +0,0 @@ -"use strict"; -var Storage = (() => { - - let chunksKey = k => `${k}/CHUNKS`; - - async function safeOp(op, type, keys) { - let sync = type === "sync"; - - try { - if (sync) { - let remove = op === "remove"; - if (remove || op === "get") { - keys = [].concat(keys); // don't touch the passed argument - let mergeResults = {}; - let localFallback = await getLocalFallback(); - if (localFallback.size) { - let localKeys = keys.filter(k => localFallback.has(k)); - if (localKeys.length) { - if (remove) { - await browser.storage.local.remove(localKeys); - for (let k of localKeys) { - localFallback.delete(k); - } - await setLocalFallback(localFallback); - } else { - mergeResults = await browser.storage.local.get(localKeys); - } - keys = keys.filter(k => !localFallback.has(k)); - } - } - - if (keys.length) { // we may not have non-fallback keys anymore - let chunkCounts = Object.entries(await browser.storage.sync.get( - keys.map(chunksKey))) - .map(([k, count]) => [k.split("/")[0], count]); - if (chunkCounts.length) { - let chunkedKeys = []; - for (let [k, count] of chunkCounts) { - // prepare to fetch all the chunks at once - while (count-- > 0) chunkedKeys.push(`${k}/${count}`); - } - if (remove) { - let doomedKeys = keys - .concat(chunkCounts.map(([k, count]) => chunksKey(k))) - .concat(chunkedKeys); - return await browser.storage.sync.remove(doomedKeys); - } else { - let chunks = await browser.storage.sync.get(chunkedKeys); - for (let [k, count] of chunkCounts) { - let orderedChunks = []; - for (let j = 0; j < count; j++) { - orderedChunks.push(chunks[`${k}/${j}`]); - } - let whole = orderedChunks.join(''); - try { - mergeResults[k] = JSON.parse(whole); - keys.splice(keys.indexOf(k), 1); // remove from "main" keys - } catch (e) { - error(e, "Could not parse chunked storage key %s (%s).", k, whole); - } - } - } - } - } - return keys.length ? - Object.assign(mergeResults, await browser.storage.sync[op](keys)) - : mergeResults; - } else if (op === "set") { - keys = Object.assign({}, keys); // don't touch the passed argument - const MAX_ITEM_SIZE = 4096; - // Firefox Sync's max object BYTEs size is 16384, Chrome's 8192. - // Rather than mesuring actual bytes, we play it safe by halving then - // lowest to cope with escapes / multibyte characters. - let removeKeys = []; - for (let k of Object.keys(keys)) { - let s = JSON.stringify(keys[k]); - let chunksCountKey = chunksKey(k); - let oldCount = await browser.storage.sync.get(chunksCountKey)[chunksCountKey] || 0; - let count; - if (s.length > MAX_ITEM_SIZE) { - count = Math.ceil(s.length / MAX_ITEM_SIZE); - let chunks = { - [chunksCountKey]: count - }; - for(let j = 0, o = 0; j < count; ++j, o += MAX_ITEM_SIZE) { - chunks[`${k}/${j}`] = s.substr(o, MAX_ITEM_SIZE); - } - await browser.storage.sync.set(chunks); - keys[k] = "[CHUNKED]"; - } else { - count = 0; - removeKeys.push(chunksCountKey); - } - if (oldCount-- > count) { - do { - removeKeys.push(`${k}${oldCount}`); - } while(oldCount-- > count); - } - } - await browser.storage.sync.remove(removeKeys); - } - } - - let ret = await browser.storage[type][op](keys); - if (sync && op === "set") { - let localFallback = await getLocalFallback(); - let size = localFallback.size; - if (size > 0) { - for (let k of Object.keys(keys)) { - localFallback.delete(k); - } - if (size > localFallback.size) { - await setLocalFallback(localFallback); - } - } - } - return ret; - } catch (e) { - error(e, "%s.%s(%o)", type, op, keys); - if (sync) { - debug("Sync disabled? Falling back to local storage (%s %o)", op, keys); - let localFallback = await getLocalFallback(); - let failedKeys = Array.isArray(keys) ? keys - : typeof keys === "string" ? [keys] : Object.keys(keys); - for (let k of failedKeys) { - localFallback.add(k); - } - await setLocalFallback(localFallback); - } else { - throw e; - } - } - - return await browser.storage.local[op](keys); - } - - const LFK_NAME = "__fallbackKeys"; - async function setLocalFallback(keys) { - return await browser.storage.local.set({[LFK_NAME]: [...keys]}); - } - async function getLocalFallback() { - let keys = (await browser.storage.local.get(LFK_NAME))[LFK_NAME]; - return new Set(Array.isArray(keys) ? keys : []); - } - - return { - async get(type, keys) { - return await safeOp("get", type, keys); - }, - - async set(type, keys) { - return await safeOp("set", type, keys); - }, - - async remove(type, keys) { - return await safeOp("remove", type, keys); - }, - - async hasLocalFallback(key) { - return (await getLocalFallback()).has(key); - }, - - async isChunked(key) { - let ccKey = chunksKey(key); - let data = await browser.storage.sync.get([key, ccKey]); - return data[key] === "[CHUNKED]" && parseInt(data[ccKey]); - } - }; -})() diff --git a/src/common/SyntaxChecker.js b/src/common/SyntaxChecker.js deleted file mode 100644 index 8646901..0000000 --- a/src/common/SyntaxChecker.js +++ /dev/null @@ -1,29 +0,0 @@ -class SyntaxChecker { - constructor() { - this.lastError = null; - this.lastFunction = null; - this.lastScript = ""; - } - check(script) { - this.lastScript = script; - try { - return !!(this.lastFunction = new Function(script)); - } catch(e) { - this.lastError = e; - this.lastFunction = null; - } - return false; - } - unquote(s, q) { - // check that this is really a double or a single quoted string... - if (s.length > 1 && s.startsWith(q) && s.endsWith(q) && - // if nothing is left if you remove all he escapes and all the stuff between quotes - s.replace(/\\./g, '').replace(/^(['"])[^\n\r]*?\1/, '') === '') { - try { - return eval(s); - } catch (e) { - } - } - return null; - } -} diff --git a/src/common/locale.js b/src/common/locale.js deleted file mode 100644 index 60498a2..0000000 --- a/src/common/locale.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; -var _ = browser.i18n.getMessage; -var i18n = (() => { - var i18n = { - // derived from http://github.com/piroor/webextensions-lib-l10n - - updateString(aString) { - return aString.replace(/__MSG_(.+?)__/g, function(aMatched) { - var key = aMatched.slice(6, -2); - return _(key); - }); - }, - updateDOM(rootNode = document) { - var texts = document.evaluate( - 'descendant::text()[contains(self::text(), "__MSG_")]', - rootNode, - null, - XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, - null - ); - for (let i = 0, maxi = texts.snapshotLength; i < maxi; i++) - { - let text = texts.snapshotItem(i); - text.nodeValue = this.updateString(text.nodeValue); - } - - var attributes = document.evaluate( - 'descendant::*/attribute::*[contains(., "__MSG_")]', - rootNode, - null, - XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, - null - ); - for (let i = 0, maxi = attributes.snapshotLength; i < maxi; i++) - { - let attribute = attributes.snapshotItem(i); - debug('apply', attribute); - attribute.value = this.updateString(attribute.value); - } - } - }; - - document.addEventListener('DOMContentLoaded', e => i18n.updateDOM()); - return i18n; -})() diff --git a/src/content/DocumentCSP.js b/src/content/DocumentCSP.js deleted file mode 100644 index a7b5251..0000000 --- a/src/content/DocumentCSP.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; -class DocumentCSP { - constructor(document) { - this.document = document; - this.builder = new CapsCSP(); - } - - apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) { - let {document} = this; - if (!capabilities.has("script")) { - // safety net for XML (especially SVG) documents and synchronous scripts running - // while inserting the CSP element. - document.defaultView.addEventListener("beforescriptexecute", e => { - if (!e.isTrusted) return; - e.preventDefault(); - debug("Fallback beforexecutescript listener blocked ", e.target); - }, true); - } - - let csp = this.builder; - let blocker = csp.buildFromCapabilities(capabilities, embedding); - if (!blocker) return null; - - let createHTMLElement = - tagName => document.createElementNS("http://www.w3.org/1999/xhtml", tagName); - - let header = csp.asHeader(blocker); - - let meta = createHTMLElement("meta"); - meta.setAttribute("http-equiv", header.name); - meta.setAttribute("content", header.value); - - let root = document.documentElement; - try { - if (!(document instanceof HTMLDocument)) { - if (!(document instanceof XMLDocument)) { - return null; // nothing to do with ImageDocument, for instance - } - // non-HTML XML documents ignore CSP unless wrapped in - // -
on Gecko - // - just on Chromium - console.debug("XML Document: temporary replacing %o with ", root); - let htmlDoc = document.implementation.createHTMLDocument(); - let htmlRoot = document.importNode(htmlDoc.documentElement, true); - document.replaceChild(htmlRoot, root); - } - - let {head} = document; - let parent = head || - document.documentElement.insertBefore(createHTMLElement("head"), - document.documentElement.firstElementChild); - - - parent.insertBefore(meta, parent.firstElementChild); - debug(`Failsafe CSP inserted in %s: "%s"`, document.URL, header.value); - meta.remove(); - if (!head) parent.remove(); - if (document.documentElement !== root) - { - - document.replaceChild(root, document.documentElement); - } - } catch (e) { - error(e, "Error inserting CSP %s in %s", document.URL, header && header.value); - return null; - } - return CSP.normalize(header.value); - } -} diff --git a/src/content/PlaceHolder.js b/src/content/PlaceHolder.js deleted file mode 100644 index e12d56a..0000000 --- a/src/content/PlaceHolder.js +++ /dev/null @@ -1,225 +0,0 @@ -var PlaceHolder = (() => { - const HANDLERS = new Map(); - const CLASS_NAME = "__NoScript_PlaceHolder__"; - const SELECTOR = `a.${CLASS_NAME}`; - let checkStyle = async () => { - checkStyle = () => {}; - if (!ns.embeddingDocument) return; - let replacement = document.querySelector(SELECTOR); - if (!replacement) return; - if (window.getComputedStyle(replacement, null).opacity !== "0.8") { - document.head.appendChild(createHTMLElement("style")).textContent = await - (await fetch(browser.extension.getURL("/content/content.css"))).text(); - } - } - - class Handler { - constructor(type, selector) { - this.type = type; - this.selector = selector; - this.placeHolders = new Map(); - HANDLERS.set(type, this); - } - filter(element, request) { - if (request.embeddingDocument) { - return document.URL === request.url; - } - let url = request.initialUrl || request.url; - return "data" in element ? element.data === url : element.src === url; - } - selectFor(request) { - return [...document.querySelectorAll(this.selector)] - .filter(element => this.filter(element, request)) - } - } - - new Handler("frame", "iframe"); - new Handler("object", "object, embed"); - new Handler("media", "video, audio, source"); - - function cloneStyle(src, dest, - props = ["width", "height", "position", "*", "margin*"]) { - var suffixes = ["Top", "Right", "Bottom", "Left"]; - for (let i = props.length; i-- > 0;) { - let p = props[i]; - if (p.endsWith("*")) { - let prefix = p.substring(0, p.length - 1); - props.splice(i, 1, ... - (suffixes.map(prefix ? (suffix => prefix + suffix) : - suffix => suffix.toLowerCase()))); - } - }; - - let srcStyle = window.getComputedStyle(src, null); - let destStyle = dest.style; - for (let p of props) { - destStyle[p] = srcStyle[p]; - } - for (let size of ["width", "height"]) { - if (/^0(?:\D|$)/.test(destStyle[size])) { - destStyle[size] = ""; - } - } - if (src.offsetTop < 0 && src.offsetTop <= (-src.offsetHeight)) { - destStyle.top = "0"; // fixes video player off-display position on Youtube - } - destStyle.display = srcStyle.display !== "block" ? "inline-block" : "block"; - } - - class PlaceHolder { - - static create(policyType, request) { - return new PlaceHolder(policyType, request); - } - static canReplace(policyType) { - return HANDLERS.has(policyType); - } - static handlerFor(policyType) { - return HANDLERS.get(policyType); - } - - static listen() { - PlaceHolder.listen = () => {}; - window.addEventListener("click", ev => { - if (ev.button === 0 && ev.isTrusted) { - let ph, replacement; - for (let e of document.elementsFromPoint(ev.clientX, ev.clientY)) { - if (ph = e._placeHolderObj) { - replacement = e; - break; - } - if (replacement = e._placeHolderReplacement) { - ph = replacement._placeHolderObj; - break; - } - } - if (ph) { - ev.preventDefault(); - ev.stopPropagation(); - if (ev.target.value === "close") { - ph.close(replacement); - } else { - ph.enable(replacement); - } - } - } - }, true, false); - } - - constructor(policyType, request) { - this.policyType = policyType; - this.request = request; - this.replacements = new Set(); - this.handler = PlaceHolder.handlerFor(policyType); - if (this.handler) { - [...document.querySelectorAll(this.handler.selector)] - .filter(element => this.handler.filter(element, request)) - .forEach(element => this.replace(element)); - }; - if (this.replacements.size) { - PlaceHolder.listen(); - checkStyle(); - } - } - - replace(element) { - if (!element.parentElement) return; - if (element.parentElement instanceof HTMLMediaElement) { - this.replace(element.parentElement); - return; - } - let { - url - } = this.request; - let objUrl = new URL(url) - this.origin = objUrl.origin; - if (this.origin === "null") { - this.origin = objUrl.protocol; - } - let TYPE = `<${this.policyType.toUpperCase()}>`; - - let replacement = createHTMLElement("a"); - replacement.className = CLASS_NAME; - cloneStyle(element, replacement); - if (ns.embeddingDocument) { - replacement.classList.add("__ns__document"); - window.stop(); - } - - replacement.href = url; - replacement.title = `${TYPE}@${url}`; - - let inner = replacement.appendChild(createHTMLElement("span")); - inner.className = replacement.className; - - let button = inner.appendChild(createHTMLElement("button")); - button.className = replacement.className; - button.setAttribute("aria-label", button.title = _("Close")); - button.value = "close"; - button.textContent = "×"; - - let description = inner.appendChild(createHTMLElement("span")); - description.textContent = `${TYPE}@${this.origin}`; - - replacement._placeHolderObj = this; - replacement._placeHolderElement = element; - for (let e of replacement.querySelectorAll("*")) { - e._placeHolderReplacement = replacement; - } - - element.replaceWith(replacement); - - // do our best to bring it to front - for (let p = replacement; p = p.parentElement;) { - p.classList.add("__ns__pop2top"); - }; - - this.replacements.add(replacement); - } - - async enable(replacement) { - debug("Enabling %o", this.request, this.policyType); - let ret = await Messages.send("blockedObjects", { - url: this.request.url, - policyType: this.policyType, - documentUrl: document.URL - }); - debug("Received response", ret); - if (!ret) return; - // bring back ancestors - for (let p = replacement; p = p.parentElement;) { - p.classList.remove("__ns__pop2top"); - }; - if (ret.collapse) { - for (let collapsing of (ret.collapse === "all" ? document.querySelectorAll(SELECTOR) : [replacement])) { - this.replacements.delete(collapsing); - collapsing.remove(); - } - return; - } - if (this.request.embeddingDocument) { - window.location.reload(); - return; - } - try { - let element = replacement._placeHolderElement; - replacement.replaceWith(element.cloneNode(true)); - this.replacements.delete(replacement); - } catch (e) { - error(e, "While replacing"); - } - } - - close(replacement) { - replacement.classList.add("__ns__closing"); - this.replacements.delete(replacement); - window.setTimeout(() => { - for (let p = replacement; p = p.parentElement;) { - p.classList.remove("__ns__pop2top"); - }; - replacement.remove() - }, 500); - } - } - return PlaceHolder; -})(); diff --git a/src/content/media.js b/src/content/media.js deleted file mode 100644 index a75ccef..0000000 --- a/src/content/media.js +++ /dev/null @@ -1,106 +0,0 @@ -if ("MediaSource" in window) { - let mediaBlocker; - let notify = allowed => { - let request = { - id: "noscript-media", - type: "media", - url: document.URL, - documentUrl: document.URL, - embeddingDocument: true, - }; - seen.record({policyType: "media", request, allowed}); - debug("MSE notification", document.URL); // DEV_ONLY - notifyPage(); - return request; - }; - let createPlaceholder = (mediaElement, request) => { - try { - let ph = PlaceHolder.create("media", request); - ph.replace(mediaElement); - PlaceHolder.listen(); - debug("MSE placeholder for %o", mediaElement); // DEV_ONLY - } catch (e) { - error(e); - } - }; - if ("SecurityPolicyViolationEvent" in window) { - // "Modern" browsers - let createPlaceholders = () => { - let request = notify(false); - for (let me of document.querySelectorAll("video,audio")) { - if (!(me.src || me.currentSrc) || me.src.startsWith("blob")) { - createPlaceholder(me, request); - } - } - } - let processedURIs = new Set(); - addEventListener("securitypolicyviolation", e => { - let {blockedURI, violatedDirective, originalPolicy} = e; - if (!(e.isTrusted && violatedDirective === "media-src" && CSP.isMediaBlocker(originalPolicy))) return; - if (mediaBlocker === undefined && /^data\b/.test(blockedURI)) { // Firefox 81 reports just "data" - debug("mediaBlocker set via CSP listener.") - mediaBlocker = true; - e.stopImmediatePropagation(); - return; - } - if (blockedURI.startsWith("blob") && - !processedURIs.has(blockedURI)) { - processedURIs.add(blockedURI); - setTimeout(createPlaceholders, 0); - } - }, true); - } - - if (typeof exportFunction === "function") { - // Fallback: Mozilla does not seem to trigger CSP media-src http: for blob: URIs assigned in MSE - window.wrappedJSObject.document.createElement("video").src = "data:"; // triggers early mediaBlocker initialization via CSP - ns.on("capabilities", e => { - mediaBlocker = !ns.allows("media"); - if (mediaBlocker) debug("mediaBlocker set via fetched policy.") - }); - - let unpatched = new Map(); - function patch(obj, methodName, replacement) { - let methods = unpatched.get(obj) || {}; - methods[methodName] = obj[methodName]; - exportFunction(replacement, obj, {defineAs: methodName}); - unpatched.set(obj, methods); - } - let urlMap = new WeakMap(); - patch(window.URL, "createObjectURL", function(o, ...args) { - let url = unpatched.get(window.URL).createObjectURL.call(this, o, ...args); - if (o instanceof MediaSource) { - let urls = urlMap.get(o); - if (!urls) urlMap.set(o, urls = new Set()); - urls.add(url); - } - return url; - }); - - patch(window.MediaSource.prototype, "addSourceBuffer", function(mime, ...args) { - let ms = this; - let urls = urlMap.get(ms); - let request = notify(!mediaBlocker); - if (mediaBlocker) { - let exposedMime = `${mime} (MSE)`; - setTimeout(() => { - try { - let allMedia = [...document.querySelectorAll("video,audio")]; - let me = allMedia.find(e => e.srcObject === ms || - urls && (urls.has(e.currentSrc) || urls.has(e.src))) || - // throwing may cause src not to be assigned at all: - allMedia.find(e => !(e.src || e.currentSrc || e.srcObject)); - if (me) createPlaceholder(me, request); - } catch (e) { - error(e); - } - }, 0); - let msg = `${exposedMime} blocked by NoScript`; - log(msg); - throw new Error(msg); - } - - return unpatched.get(window.MediaSource.prototype).addSourceBuffer.call(ms, mime, ...args); - }); - } -} diff --git a/src/content/sanitizePaste.js b/src/content/sanitizePaste.js deleted file mode 100644 index bf42755..0000000 --- a/src/content/sanitizePaste.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; -{ - let urlAttributes = ['href', 'to', 'from', 'by', 'values']; - let selector = urlAttributes.map(a => `[${a}]`).join(','); - - for (let evType of ["drop", "paste"]) window.addEventListener(evType, e => { - let container = e.target; - let editing = false; - for (let el = container; el; el = el.parentElement) { - if (el.setRangeText || el.contentEditable) { - editing = true; - break; - } - } - if (!editing) return; - - // we won't touch DOM elements which are already there - let oldNodes = new Set(container.querySelectorAll(selector + ",form")); - window.setTimeout(() => { - // we delay our custom sanitization after the browser performed the paste - // or drop job, rather than replacing it, in order to avoid interferences - // with built-in sanitization - try { - let html = container.innerHTML; - if (sanitizeExtras(container, oldNodes)) { - let t = e.type.toUpperCase(); - console.log(`[NoScript] Sanitized\n<${t}>\n${html}\n${t}>\nto\n<${t}>\n${container.innerHTML}\n${t}>`, container); - } - } catch(ex) { - console.log(ex); - } - }, 0); - }, true); - - function removeAttribute(node, name, value = node.getAttribute(name)) { - node.setAttribute(`data-noscript-removed-${name}`, value); - node.removeAttribute(name); - } - - function sanitizeExtras(container, oldNodes = []) { - let ret = false; - - // remove attributes from forms - for (let f of container.getElementsByTagName("form")) { - if (oldNodes.has(f)) continue; - for (let a of [...f.attributes]) { - removeAttribute(f, a.name); - } - } - - for (let node of container.querySelectorAll(selector)) { - if (oldNodes.has(node)) continue; - for (let name of urlAttributes) { - let value = node.getAttribute(name); - if (/^\W*(?:(?:javascript|data):|https?:[\s\S]+[[(<])/i.test(unescape(value))) { - removeAttribute(node, name, value); - ret = true; - } - } - } - return ret; - } -} diff --git a/src/content/webglHook.js b/src/content/webglHook.js deleted file mode 100644 index fc1beeb..0000000 --- a/src/content/webglHook.js +++ /dev/null @@ -1,43 +0,0 @@ -// depends on nscl/content/patchWindow.js -"use strict"; -ns.on("capabilities", event => { - debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY - if (ns.allows("webgl")) return; - let env = {eventName: `nsWebgl:${uuid()}`}; - window.addEventListener(env.eventName, e => { - let request = { - id: "noscript-webgl", - type: "webgl", - url: document.URL, - documentUrl: document.URL, - embeddingDocument: true, - }; - seen.record({policyType: "webgl", request, allowed: false}); - let canvas = e.target; - if (canvas instanceof HTMLCanvasElement) { - try { - let ph = PlaceHolder.create("webgl", request); - ph.replace(canvas); - PlaceHolder.listen(); - } catch (e) { - error(e); - } - } - notifyPage(); - }, true); - - function modifyGetContext(win, env) { - let proto = win.HTMLCanvasElement.prototype; - let getContext = proto.getContext; - exportFunction(function(type, ...rest) { - if (type && type.toLowerCase().includes("webgl")) { - let target = document.contains(this) ? this : window; - target.dispatchEvent(new Event(env.eventName, {composed: true})); - return null; - } - return getContext.call(this, type, ...rest); - }, proto, {defineAs: "getContext"}); - } - - patchWindow(modifyGetContext, env); -}); diff --git a/src/lib/CSP.js b/src/lib/CSP.js deleted file mode 100644 index 8549047..0000000 --- a/src/lib/CSP.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; - -class CSP { - static isMediaBlocker(csp) { - return /(?:^|[\s;])media-src (?:'none'|http:)(?:;|$)/.test(csp); - } - static normalize(csp) { - return csp.replace(/\s*;\s*/g, ';').replace(/\b(script-src\s+'none'.*?;)(?:script-src-\w+\s+'none';)+/, '$1'); - } - - build(...directives) { - return directives.join(';'); - } - - buildBlocker(...types) { - return this.build(...(types.map(t => `${t.name || `${t.type || t}-src`} ${t.value || "'none'"}`))); - } - - blocks(header, type) { - return `;${header};`.includes(`;${type}-src 'none';`) - } - - asHeader(value) { - return {name: CSP.headerName, value}; - } -} - -CSP.isEmbedType = type => /\b(?:application|video|audio)\b/.test(type) && type !== "application/xhtml+xml"; -CSP.headerName = "content-security-policy"; -CSP.patchDataURI = (uri, blocker) => { - let parts = /^data:(?:[^,;]*ml|unknown-content-type)(;[^,]*)?,/i.exec(uri); - if (!(blocker && parts)) { - // not an interesting data: URI, return as it is - return uri; - } - if (parts[1]) { - // extra encoding info, let's bailout (better safe than sorry) - return "data:"; - } - // It's a HTML/XML page, let's prepend our CSP blocker to the document - let patch = parts[0] + encodeURIComponent( - ``); - return uri.startsWith(patch) ? uri : patch + uri.substring(parts[0].length); -} diff --git a/src/lib/LastListener.js b/src/lib/LastListener.js deleted file mode 100644 index b93edbc..0000000 --- a/src/lib/LastListener.js +++ /dev/null @@ -1,49 +0,0 @@ -/** -* Wrapper around listeners on various WebExtensions -* APIs (e.g. webRequest.on*), as a best effort to -* let them run last by removing and re-adding them -* on each call (swapping 2 copies because -* addListener() calls are asynchronous). -* Note: we rely on implementation details Like -* listeners being called in addition order; also, -* clients should ensure they're not called twice for -* the same event, if that's important. -} -*/ - -class LastListener { - constructor(observed, listener, ...extras) { - this.observed = observed; - this.listener = listener; - this.extras = extras; - let ww = this._wrapped = [listener, listener].map(l => { - let w = (...args) => { - if (this.observed.hasListener(w._other)) { - this.observed.removeListener(w); - } else if (this.installed) { - this.observed.addListener(w._other, ...this.extras); - } - debug("Running listener", w === ww[0] ? 0 : 1, ...args); - return this.installed ? this.listener(...args) - : this.defaultResult; - } - return w; - }); - - ww[0]._other = ww[1]; - ww[1]._other = ww[0]; - this.installed = false; - this.defaultResult = null; - } - - install() { - if (this.installed) return; - this.observed.addListener(this._wrapped[0], ...this.extras); - this.installed = true; - } - - uninstall() { - this.installed = false; - for (let l of this._wrapped) this.observed.removeListener(l); - } -} diff --git a/src/lib/NetCSP.js b/src/lib/NetCSP.js deleted file mode 100644 index 4891900..0000000 --- a/src/lib/NetCSP.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; - -class NetCSP extends CSP { - constructor(start) { - super(); - this.start = start; - } - - isMine(header) { - let {name, value} = header; - return name.toLowerCase() === CSP.headerName && - value.split(/,\s*/).some(v => v.startsWith(this.start)); - } - - unmergeExtras(header) { - let {name, value} = header; - return value.split(/,\s*/).filter(v => !v.startsWith(this.start)) - .map(value => {name, value}); - } - - build(...directives) { - return `${this.start};${super.build(...directives)}`; - } - - cleanup(headers) { - } -} diff --git a/src/lib/TabCache.js b/src/lib/TabCache.js deleted file mode 100644 index 16d6e1f..0000000 --- a/src/lib/TabCache.js +++ /dev/null @@ -1,24 +0,0 @@ -var TabCache = (() => { - - let cache = new Map(); - - browser.tabs.onUpdated.addListener(tab => { - cache.set(tab.id, tab); - }); - - browser.tabs.onRemoved.addListener(tab => { - cache.delete(tab.id); - }); - - (async () => { - for (let tab of await browser.tabs.query({})) { - cache.set(tab.id, tab); - } - })(); - - return { - get(tabId) { - return cache.get(tabId); - } - }; -})(); diff --git a/src/lib/Timing.js b/src/lib/Timing.js deleted file mode 100644 index 7bc00fa..0000000 --- a/src/lib/Timing.js +++ /dev/null @@ -1,49 +0,0 @@ -class Timing { - - constructor(workSlot = 10, longTime = 20000, pauseTime = 20) { - this.workSlot = workSlot; - this.longTime = longTime; - this.pauseTime = pauseTime; - this.interrupted = false; - this.fatalTimeout = false; - this.maxCalls = 1000; - this.reset(); - } - - static sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - async pause() { - if (this.interrupted) throw new TimingException("Timing: interrupted"); - let now = Date.now(); - this.calls++; - let sinceLastCall = now - this.lastCall; - if (sinceLastCall > this.workSlot && this.calls > 1000) { - // low resolution (100ms) timer? Let's cap approximating by calls number - this.maxCalls = this.calls / sinceLastCall * this.workSlot; - } - this.lastCall = now; - this.elapsed = now - this.timeOrigin; - if (now - this.lastPause > this.workSlot || this.calls > this.maxCalls) { - this.tooLong = this.elapsed >= this.longTime; - if (this.tooLong && this.fatalTimeout) { - throw new TimingException(`Timing: exceeded ${this.longTime}ms timeout`); - } - this.calls = 0; - if (this.pauseTime > 0) await Timing.sleep(this.pauseTime); - this.lastPause = Date.now(); - return true; - } - return false; - } - - reset() { - this.elapsed = 0; - this.calls = 0; - this.timeOrigin = this.lastPause = this.lastCall = Date.now(); - this.tooLong = false; - } -} - -class TimingException extends Error {}; diff --git a/src/lib/UA.js b/src/lib/UA.js deleted file mode 100644 index 57e98ae..0000000 --- a/src/lib/UA.js +++ /dev/null @@ -1,22 +0,0 @@ -{ - let mozWebExtUrl = document.URL.startsWith("moz-"); - let isMozilla = mozWebExtUrl || typeof window.wrappedJSObject === "object"; - let mobile = false; - if (isMozilla) { - if (mozWebExtUrl) { - // help browser-specific UI styling - document.documentElement.classList.add("mozwebext"); - mobile = !("windows" in browser); - } - } else { - // shims for non-Mozilla browsers - if (typeof chrome === "object" && !chrome.tabs) { - // content script shims - } - } - - var UA = { - isMozilla, - mobile, - }; -} diff --git a/src/lib/Ver.js b/src/lib/Ver.js deleted file mode 100644 index 3d86140..0000000 --- a/src/lib/Ver.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; -class Ver { - constructor(version) { - if (version instanceof Ver) { - this.versionString = version.versionString; - this.parts = version.parts; - } else { - this.versionString = version.toString(); - this.parts = this.versionString.split("."); - } - } - toString() { - return this.versionString; - } - compare(other) { - if (!(other instanceof Ver)) other = new Ver(other); - let p1 = this.parts, p2 = other.parts; - let maxParts = Math.max(p1.length, p2.length); - for (let j = 0; j < maxParts; j++) { - let s1 = p1[j] || "0"; - let s2 = p2[j] || "0"; - if (s1 === s2) continue; - let n1 = parseInt(s1); - let n2 = parseInt(s2); - if (n1 > n2) return 1; - if (n1 < n2) return -1; - // if numeric part is the same, an alphabetic suffix decreases value - // so a "pure number" wins - if (!/\D/.test(s1)) return 1; - if (!/\D/.test(s2)) return -1; - // both have an alhpabetic suffix, let's compare lexicographycally - if (s1 > s2) return 1; - if (s1 < s2) return -1; - } - return 0; - } - static is(ver1, op, ver2) { - let res = new Ver(ver1).compare(ver2); - - return op.includes("!=") && res !== 0 || - op.includes("=") && res === 0 || - op.includes("<") && res === -1 || - op.includes(">") && res === 1; - } -} \ No newline at end of file diff --git a/src/lib/browser-polyfill.js b/src/lib/browser-polyfill.js deleted file mode 100644 index 5fef28a..0000000 --- a/src/lib/browser-polyfill.js +++ /dev/null @@ -1,1237 +0,0 @@ -(function (global, factory) { - if (typeof define === "function" && define.amd) { - define("webextension-polyfill", ["module"], factory); - } else if (typeof exports !== "undefined") { - factory(module); - } else { - var mod = { - exports: {} - }; - factory(mod); - global.browser = mod.exports; - } -})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) { - /* webextension-polyfill - v0.7.0 - Tue Nov 10 2020 20:24:04 */ - - /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ - - /* vim: set sts=2 sw=2 et tw=80: */ - - /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; - - if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.prototype) { - const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; - const SEND_RESPONSE_DEPRECATION_WARNING = "Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)"; // Wrapping the bulk of this polyfill in a one-time-use function is a minor - // optimization for Firefox. Since Spidermonkey does not fully parse the - // contents of a function until the first time it's called, and since it will - // never actually need to be called, this allows the polyfill to be included - // in Firefox nearly for free. - - const wrapAPIs = extensionAPIs => { - // NOTE: apiMetadata is associated to the content of the api-metadata.json file - // at build time by replacing the following "include" with the content of the - // JSON file. - const apiMetadata = { - "alarms": { - "clear": { - "minArgs": 0, - "maxArgs": 1 - }, - "clearAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "bookmarks": { - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getChildren": { - "minArgs": 1, - "maxArgs": 1 - }, - "getRecent": { - "minArgs": 1, - "maxArgs": 1 - }, - "getSubTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTree": { - "minArgs": 0, - "maxArgs": 0 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "browserAction": { - "disable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "enable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "getBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1 - }, - "getBadgeText": { - "minArgs": 1, - "maxArgs": 1 - }, - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "openPopup": { - "minArgs": 0, - "maxArgs": 0 - }, - "setBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setBadgeText": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "browsingData": { - "remove": { - "minArgs": 2, - "maxArgs": 2 - }, - "removeCache": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCookies": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeDownloads": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFormData": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeHistory": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeLocalStorage": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePasswords": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePluginData": { - "minArgs": 1, - "maxArgs": 1 - }, - "settings": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "commands": { - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "contextMenus": { - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "cookies": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAllCookieStores": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "devtools": { - "inspectedWindow": { - "eval": { - "minArgs": 1, - "maxArgs": 2, - "singleCallbackArg": false - } - }, - "panels": { - "create": { - "minArgs": 3, - "maxArgs": 3, - "singleCallbackArg": true - }, - "elements": { - "createSidebarPane": { - "minArgs": 1, - "maxArgs": 1 - } - } - } - }, - "downloads": { - "cancel": { - "minArgs": 1, - "maxArgs": 1 - }, - "download": { - "minArgs": 1, - "maxArgs": 1 - }, - "erase": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFileIcon": { - "minArgs": 1, - "maxArgs": 2 - }, - "open": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "pause": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFile": { - "minArgs": 1, - "maxArgs": 1 - }, - "resume": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "extension": { - "isAllowedFileSchemeAccess": { - "minArgs": 0, - "maxArgs": 0 - }, - "isAllowedIncognitoAccess": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "history": { - "addUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "deleteRange": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "getVisits": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "i18n": { - "detectLanguage": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAcceptLanguages": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "identity": { - "launchWebAuthFlow": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "idle": { - "queryState": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "management": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getSelf": { - "minArgs": 0, - "maxArgs": 0 - }, - "setEnabled": { - "minArgs": 2, - "maxArgs": 2 - }, - "uninstallSelf": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "notifications": { - "clear": { - "minArgs": 1, - "maxArgs": 1 - }, - "create": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPermissionLevel": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "pageAction": { - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "hide": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "permissions": { - "contains": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "request": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "runtime": { - "getBackgroundPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPlatformInfo": { - "minArgs": 0, - "maxArgs": 0 - }, - "openOptionsPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "requestUpdateCheck": { - "minArgs": 0, - "maxArgs": 0 - }, - "sendMessage": { - "minArgs": 1, - "maxArgs": 3 - }, - "sendNativeMessage": { - "minArgs": 2, - "maxArgs": 2 - }, - "setUninstallURL": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "sessions": { - "getDevices": { - "minArgs": 0, - "maxArgs": 1 - }, - "getRecentlyClosed": { - "minArgs": 0, - "maxArgs": 1 - }, - "restore": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "storage": { - "local": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "managed": { - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "sync": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - } - }, - "tabs": { - "captureVisibleTab": { - "minArgs": 0, - "maxArgs": 2 - }, - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "detectLanguage": { - "minArgs": 0, - "maxArgs": 1 - }, - "discard": { - "minArgs": 0, - "maxArgs": 1 - }, - "duplicate": { - "minArgs": 1, - "maxArgs": 1 - }, - "executeScript": { - "minArgs": 1, - "maxArgs": 2 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 0 - }, - "getZoom": { - "minArgs": 0, - "maxArgs": 1 - }, - "getZoomSettings": { - "minArgs": 0, - "maxArgs": 1 - }, - "goBack": { - "minArgs": 0, - "maxArgs": 1 - }, - "goForward": { - "minArgs": 0, - "maxArgs": 1 - }, - "highlight": { - "minArgs": 1, - "maxArgs": 1 - }, - "insertCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "query": { - "minArgs": 1, - "maxArgs": 1 - }, - "reload": { - "minArgs": 0, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "sendMessage": { - "minArgs": 2, - "maxArgs": 3 - }, - "setZoom": { - "minArgs": 1, - "maxArgs": 2 - }, - "setZoomSettings": { - "minArgs": 1, - "maxArgs": 2 - }, - "update": { - "minArgs": 1, - "maxArgs": 2 - } - }, - "topSites": { - "get": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "webNavigation": { - "getAllFrames": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFrame": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "webRequest": { - "handlerBehaviorChanged": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "windows": { - "create": { - "minArgs": 0, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 1 - }, - "getLastFocused": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - } - }; - - if (Object.keys(apiMetadata).length === 0) { - throw new Error("api-metadata.json has not been included in browser-polyfill"); - } - /** - * A WeakMap subclass which creates and stores a value for any key which does - * not exist when accessed, but behaves exactly as an ordinary WeakMap - * otherwise. - * - * @param {function} createItem - * A function which will be called in order to create the value for any - * key which does not exist, the first time it is accessed. The - * function receives, as its only argument, the key being created. - */ - - - class DefaultWeakMap extends WeakMap { - constructor(createItem, items = undefined) { - super(items); - this.createItem = createItem; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.createItem(key)); - } - - return super.get(key); - } - - } - /** - * Returns true if the given object is an object with a `then` method, and can - * therefore be assumed to behave as a Promise. - * - * @param {*} value The value to test. - * @returns {boolean} True if the value is thenable. - */ - - - const isThenable = value => { - return value && typeof value === "object" && typeof value.then === "function"; - }; - /** - * Creates and returns a function which, when called, will resolve or reject - * the given promise based on how it is called: - * - * - If, when called, `chrome.runtime.lastError` contains a non-null object, - * the promise is rejected with that value. - * - If the function is called with exactly one argument, the promise is - * resolved to that value. - * - Otherwise, the promise is resolved to an array containing all of the - * function's arguments. - * - * @param {object} promise - * An object containing the resolution and rejection functions of a - * promise. - * @param {function} promise.resolve - * The promise's resolution function. - * @param {function} promise.rejection - * The promise's rejection function. - * @param {object} metadata - * Metadata about the wrapped method which has created the callback. - * @param {integer} metadata.maxResolvedArgs - * The maximum number of arguments which may be passed to the - * callback created by the wrapped async function. - * - * @returns {function} - * The generated callback function. - */ - - - const makeCallback = (promise, metadata) => { - return (...callbackArgs) => { - if (extensionAPIs.runtime.lastError) { - promise.reject(extensionAPIs.runtime.lastError); - } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) { - promise.resolve(callbackArgs[0]); - } else { - promise.resolve(callbackArgs); - } - }; - }; - - const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; - /** - * Creates a wrapper function for a method with the given name and metadata. - * - * @param {string} name - * The name of the method which is being wrapped. - * @param {object} metadata - * Metadata about the method being wrapped. - * @param {integer} metadata.minArgs - * The minimum number of arguments which must be passed to the - * function. If called with fewer than this number of arguments, the - * wrapper will raise an exception. - * @param {integer} metadata.maxArgs - * The maximum number of arguments which may be passed to the - * function. If called with more than this number of arguments, the - * wrapper will raise an exception. - * @param {integer} metadata.maxResolvedArgs - * The maximum number of arguments which may be passed to the - * callback created by the wrapped async function. - * - * @returns {function(object, ...*)} - * The generated wrapper function. - */ - - - const wrapAsyncFunction = (name, metadata) => { - return function asyncFunctionWrapper(target, ...args) { - if (args.length < metadata.minArgs) { - throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); - } - - if (args.length > metadata.maxArgs) { - throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); - } - - return new Promise((resolve, reject) => { - if (metadata.fallbackToNoCallback) { - // This API method has currently no callback on Chrome, but it return a promise on Firefox, - // and so the polyfill will try to call it with a callback first, and it will fallback - // to not passing the callback if the first call fails. - try { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } catch (cbError) { - console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); - target[name](...args); // Update the API method metadata, so that the next API calls will not try to - // use the unsupported callback anymore. - - metadata.fallbackToNoCallback = false; - metadata.noCallback = true; - resolve(); - } - } else if (metadata.noCallback) { - target[name](...args); - resolve(); - } else { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } - }); - }; - }; - /** - * Wraps an existing method of the target object, so that calls to it are - * intercepted by the given wrapper function. The wrapper function receives, - * as its first argument, the original `target` object, followed by each of - * the arguments passed to the original method. - * - * @param {object} target - * The original target object that the wrapped method belongs to. - * @param {function} method - * The method being wrapped. This is used as the target of the Proxy - * object which is created to wrap the method. - * @param {function} wrapper - * The wrapper function which is called in place of a direct invocation - * of the wrapped method. - * - * @returns {Proxy