From d076a517ba97da60791ded641a7952a784203d59 Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 00:14:50 +0100 Subject: Generic shims for Chromium derivatives. --- src/lib/UA.js | 8 + src/lib/browser-polyfill.js | 1186 +++++++++++++++++++++++++++++++++++++++++++ src/manifest.json | 8 +- src/ui/options.html | 2 + src/ui/popup.html | 2 + src/ui/prompt.html | 2 + src/ui/siteInfo.html | 2 + 7 files changed, 1209 insertions(+), 1 deletion(-) create mode 100644 src/lib/UA.js create mode 100644 src/lib/browser-polyfill.js diff --git a/src/lib/UA.js b/src/lib/UA.js new file mode 100644 index 0000000..0a751a4 --- /dev/null +++ b/src/lib/UA.js @@ -0,0 +1,8 @@ +var UA = { + isMozilla: document.URL.startsWith("moz-"), +} + +if (!UA.isMozilla && typeof chrome === "object" && !chrome.tabs && typeof exportFunction === "undefined") { + // content script shims + window.exportFunction = () => {}; +} diff --git a/src/lib/browser-polyfill.js b/src/lib/browser-polyfill.js new file mode 100644 index 0000000..b3dd82d --- /dev/null +++ b/src/lib/browser-polyfill.js @@ -0,0 +1,1186 @@ +(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; + } +})(this, function (module) { + /* webextension-polyfill - v0.3.1 - Tue Aug 21 2018 10:09:34 */ + /* -*- 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 = () => { + // 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 + } + }, + "panels": { + "create": { + "minArgs": 3, + "maxArgs": 3, + "singleCallbackArg": true + } + } + }, + "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 + }, + "getBrowserInfo": { + "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 + }, + "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 (chrome.runtime.lastError) { + promise.reject(chrome.runtime.lastError); + } else if (metadata.singleCallbackArg || callbackArgs.length <= 1) { + 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} + * A Proxy object for the given method, which invokes the given wrapper + * method in its place. + */ + const wrapMethod = (target, method, wrapper) => { + return new Proxy(method, { + apply(targetMethod, thisObj, args) { + return wrapper.call(thisObj, target, ...args); + } + }); + }; + + let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); + + /** + * Wraps an object in a Proxy which intercepts and wraps certain methods + * based on the given `wrappers` and `metadata` objects. + * + * @param {object} target + * The target object to wrap. + * + * @param {object} [wrappers = {}] + * An object tree containing wrapper functions for special cases. Any + * function present in this object tree is called in place of the + * method in the same location in the `target` object tree. These + * wrapper methods are invoked as described in {@see wrapMethod}. + * + * @param {object} [metadata = {}] + * An object tree containing metadata used to automatically generate + * Promise-based wrapper functions for asynchronous. Any function in + * the `target` object tree which has a corresponding metadata object + * in the same location in the `metadata` tree is replaced with an + * automatically-generated wrapper function, as described in + * {@see wrapAsyncFunction} + * + * @returns {Proxy} + */ + const wrapObject = (target, wrappers = {}, metadata = {}) => { + let cache = Object.create(null); + let handlers = { + has(proxyTarget, prop) { + return prop in target || prop in cache; + }, + + get(proxyTarget, prop, receiver) { + if (prop in cache) { + return cache[prop]; + } + + if (!(prop in target)) { + return undefined; + } + + let value = target[prop]; + + if (typeof value === "function") { + // This is a method on the underlying object. Check if we need to do + // any wrapping. + + if (typeof wrappers[prop] === "function") { + // We have a special-case wrapper for this method. + value = wrapMethod(target, target[prop], wrappers[prop]); + } else if (hasOwnProperty(metadata, prop)) { + // This is an async method that we have metadata for. Create a + // Promise wrapper for it. + let wrapper = wrapAsyncFunction(prop, metadata[prop]); + value = wrapMethod(target, target[prop], wrapper); + } else { + // This is a method that we don't know or care about. Return the + // original method, bound to the underlying object. + value = value.bind(target); + } + } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { + // This is an object that we need to do some wrapping for the children + // of. Create a sub-object wrapper for it with the appropriate child + // metadata. + value = wrapObject(value, wrappers[prop], metadata[prop]); + } else { + // We don't need to do any wrapping for this property, + // so just forward all access to the underlying object. + Object.defineProperty(cache, prop, { + configurable: true, + enumerable: true, + get() { + return target[prop]; + }, + set(value) { + target[prop] = value; + } + }); + + return value; + } + + cache[prop] = value; + return value; + }, + + set(proxyTarget, prop, value, receiver) { + if (prop in cache) { + cache[prop] = value; + } else { + target[prop] = value; + } + return true; + }, + + defineProperty(proxyTarget, prop, desc) { + return Reflect.defineProperty(cache, prop, desc); + }, + + deleteProperty(proxyTarget, prop) { + return Reflect.deleteProperty(cache, prop); + } + }; + + // Per contract of the Proxy API, the "get" proxy handler must return the + // original value of the target if that value is declared read-only and + // non-configurable. For this reason, we create an object with the + // prototype set to `target` instead of using `target` directly. + // Otherwise we cannot return a custom object for APIs that + // are declared read-only and non-configurable, such as `chrome.devtools`. + // + // The proxy handlers themselves will still use the original `target` + // instead of the `proxyTarget`, so that the methods and properties are + // dereferenced via the original targets. + let proxyTarget = Object.create(target); + return new Proxy(proxyTarget, handlers); + }; + + /** + * Creates a set of wrapper functions for an event object, which handles + * wrapping of listener functions that those messages are passed. + * + * A single wrapper is created for each listener function, and stored in a + * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` + * retrieve the original wrapper, so that attempts to remove a + * previously-added listener work as expected. + * + * @param {DefaultWeakMap} wrapperMap + * A DefaultWeakMap object which will create the appropriate wrapper + * for a given listener function when one does not exist, and retrieve + * an existing one when it does. + * + * @returns {object} + */ + const wrapEvent = wrapperMap => ({ + addListener(target, listener, ...args) { + target.addListener(wrapperMap.get(listener), ...args); + }, + + hasListener(target, listener) { + return target.hasListener(wrapperMap.get(listener)); + }, + + removeListener(target, listener) { + target.removeListener(wrapperMap.get(listener)); + } + }); + + // Keep track if the deprecation warning has been logged at least once. + let loggedSendResponseDeprecationWarning = false; + + const onMessageWrappers = new DefaultWeakMap(listener => { + if (typeof listener !== "function") { + return listener; + } + + /** + * Wraps a message listener function so that it may send responses based on + * its return value, rather than by returning a sentinel value and calling a + * callback. If the listener function returns a Promise, the response is + * sent when the promise either resolves or rejects. + * + * @param {*} message + * The message sent by the other end of the channel. + * @param {object} sender + * Details about the sender of the message. + * @param {function(*)} sendResponse + * A callback which, when called with an arbitrary argument, sends + * that value as a response. + * @returns {boolean} + * True if the wrapped listener returned a Promise, which will later + * yield a response. False otherwise. + */ + return function onMessage(message, sender, sendResponse) { + let didCallSendResponse = false; + + let wrappedSendResponse; + let sendResponsePromise = new Promise(resolve => { + wrappedSendResponse = function (response) { + if (!loggedSendResponseDeprecationWarning) { + console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack); + loggedSendResponseDeprecationWarning = true; + } + didCallSendResponse = true; + resolve(response); + }; + }); + + let result; + try { + result = listener(message, sender, wrappedSendResponse); + } catch (err) { + result = Promise.reject(err); + } + + const isResultThenable = result !== true && isThenable(result); + + // If the listener didn't returned true or a Promise, or called + // wrappedSendResponse synchronously, we can exit earlier + // because there will be no response sent from this listener. + if (result !== true && !isResultThenable && !didCallSendResponse) { + return false; + } + + // A small helper to send the message if the promise resolves + // and an error if the promise rejects (a wrapped sendMessage has + // to translate the message into a resolved promise or a rejected + // promise). + const sendPromisedResult = promise => { + promise.then(msg => { + // send the message value. + sendResponse(msg); + }, error => { + // Send a JSON representation of the error if the rejected value + // is an instance of error, or the object itself otherwise. + let message; + if (error && (error instanceof Error || typeof error.message === "string")) { + message = error.message; + } else { + message = "An unexpected error occurred"; + } + + sendResponse({ + __mozWebExtensionPolyfillReject__: true, + message + }); + }).catch(err => { + // Print an error on the console if unable to send the response. + console.error("Failed to send onMessage rejected reply", err); + }); + }; + + // If the listener returned a Promise, send the resolved value as a + // result, otherwise wait the promise related to the wrappedSendResponse + // callback to resolve and send it as a response. + if (isResultThenable) { + sendPromisedResult(result); + } else { + sendPromisedResult(sendResponsePromise); + } + + // Let Chrome know that the listener is replying. + return true; + }; + }); + + const wrappedSendMessageCallback = ({ reject, resolve }, reply) => { + if (chrome.runtime.lastError) { + // Detect when none of the listeners replied to the sendMessage call and resolve + // the promise to undefined as in Firefox. + // See https://github.com/mozilla/webextension-polyfill/issues/130 + if (chrome.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { + resolve(); + } else { + reject(chrome.runtime.lastError); + } + } else if (reply && reply.__mozWebExtensionPolyfillReject__) { + // Convert back the JSON representation of the error into + // an Error instance. + reject(new Error(reply.message)); + } else { + resolve(reply); + } + }; + + const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...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) => { + const wrappedCb = wrappedSendMessageCallback.bind(null, { resolve, reject }); + args.push(wrappedCb); + apiNamespaceObj.sendMessage(...args); + }); + }; + + const staticWrappers = { + runtime: { + onMessage: wrapEvent(onMessageWrappers), + onMessageExternal: wrapEvent(onMessageWrappers), + sendMessage: wrappedSendMessage.bind(null, "sendMessage", { minArgs: 1, maxArgs: 3 }) + }, + tabs: { + sendMessage: wrappedSendMessage.bind(null, "sendMessage", { minArgs: 2, maxArgs: 3 }) + } + }; + const settingMetadata = { + clear: { minArgs: 1, maxArgs: 1 }, + get: { minArgs: 1, maxArgs: 1 }, + set: { minArgs: 1, maxArgs: 1 } + }; + apiMetadata.privacy = { + network: { + networkPredictionEnabled: settingMetadata, + webRTCIPHandlingPolicy: settingMetadata + }, + services: { + passwordSavingEnabled: settingMetadata + }, + websites: { + hyperlinkAuditingEnabled: settingMetadata, + referrersEnabled: settingMetadata + } + }; + + return wrapObject(chrome, staticWrappers, apiMetadata); + }; + + // The build process adds a UMD wrapper around this file, which makes the + // `module` variable available. + module.exports = wrapAPIs(); // eslint-disable-line no-undef + } else { + module.exports = browser; // eslint-disable-line no-undef + } +}); +//# sourceMappingURL=browser-polyfill.js.map diff --git a/src/manifest.json b/src/manifest.json index 43e47dc..f0326ab 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -8,7 +8,7 @@ "strict_min_version": "59.0" } }, - "version": "10.2.2rc3", + "version": "10.5", "description": "__MSG_Description__", "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'none'", @@ -34,6 +34,8 @@ "background": { "persistent": true, "scripts": [ + "lib/UA.js", + "lib/browser-polyfill.js", "lib/uuid.js", "lib/log.js", "lib/include.js", @@ -76,6 +78,8 @@ "match_about_blank": true, "all_frames": true, "js": [ + "lib/UA.js", + "lib/browser-polyfill.js", "lib/log.js", "lib/uuid.js", "lib/sha256.js", @@ -116,11 +120,13 @@ "commands": { "_execute_browser_action": { + "description": "NoScript UI", "suggested_key": { "default": "Alt+Shift+N" } }, "togglePermissions": { + "description": "Toggle permissions", "suggested_key": { "default": "Ctrl+Shift+T" } diff --git a/src/ui/options.html b/src/ui/options.html index 6e2ad0e..4d44d75 100644 --- a/src/ui/options.html +++ b/src/ui/options.html @@ -8,6 +8,8 @@ + + diff --git a/src/ui/popup.html b/src/ui/popup.html index 517b233..5efc9e9 100644 --- a/src/ui/popup.html +++ b/src/ui/popup.html @@ -6,6 +6,8 @@ NoScript Settings + + diff --git a/src/ui/prompt.html b/src/ui/prompt.html index 902b375..3c723e5 100644 --- a/src/ui/prompt.html +++ b/src/ui/prompt.html @@ -5,6 +5,8 @@ + + diff --git a/src/ui/siteInfo.html b/src/ui/siteInfo.html index 0cb24ec..c37fb53 100644 --- a/src/ui/siteInfo.html +++ b/src/ui/siteInfo.html @@ -1,5 +1,7 @@ + + -- cgit v1.2.3 From 781514cfb946fb49bc1e2c52f16f808f26856f41 Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 00:28:33 +0100 Subject: Graceful degradation for missing WebExtensions APIs on Chromium. --- src/bg/ChildPolicies.js | 4 ++++ src/bg/RequestGuard.js | 9 ++++++--- src/bg/deferWebTraffic.js | 2 +- src/ui/Prompts.js | 10 ++++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/bg/ChildPolicies.js b/src/bg/ChildPolicies.js index 74aeccb..5727762 100644 --- a/src/bg/ChildPolicies.js +++ b/src/bg/ChildPolicies.js @@ -51,6 +51,10 @@ } }; + if (!browser.contentScripts) { // #chromium fallback + Scripts.register = () => {}; + } + let flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); let protocolRx = /^(\w+):/i; diff --git a/src/bg/RequestGuard.js b/src/bg/RequestGuard.js index a174eba..d27bacb 100644 --- a/src/bg/RequestGuard.js +++ b/src/bg/RequestGuard.js @@ -20,7 +20,10 @@ var RequestGuard = (() => { media: "media", other: "", }; - const allTypes = Object.keys(policyTypesMap); + const allTypes = UA.isMozilla ? Object.keys(policyTypesMap) + : ["main_frame", "sub_frame", "stylesheet", "script", "image", "font", + "object", "xmlhttprequest", "ping", "csp_report", "media", "websocket", "other"]; + Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types const TabStatus = { map: new Map(), @@ -254,7 +257,7 @@ var RequestGuard = (() => { return redirected; } const ABORT = {cancel: true}, ALLOW = {}; - const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/; + const INTERNAL_SCHEME = /^(?:chrome|resource|(?:moz|chrome)-extension|about):/; const listeners = { onBeforeRequest(request) { try { @@ -326,7 +329,7 @@ var RequestGuard = (() => { capabilities = perms.capabilities; } else { capabilities = perms.capabilities; - if (frameAncestors.length > 0) { + if (frameAncestors && frameAncestors.length > 0) { // cascade top document's restrictions to subframes let topUrl = frameAncestors.pop().url; let topPerms = policy.get(topUrl, topUrl).perms; diff --git a/src/bg/deferWebTraffic.js b/src/bg/deferWebTraffic.js index a384e29..571073a 100644 --- a/src/bg/deferWebTraffic.js +++ b/src/bg/deferWebTraffic.js @@ -31,7 +31,7 @@ function deferWebTraffic(promiseToWaitFor, next) { if (type === "main_frame") { seenTabs.add(tabId); } else if (documentUrl) { - if (frameId !== 0) { + if (frameId !== 0 && request.frameAncestors) { documentUrl = request.frameAncestors.pop().url; } reloadTab(tabId); diff --git a/src/ui/Prompts.js b/src/ui/Prompts.js index 03ea9ee..ed83ac2 100644 --- a/src/ui/Prompts.js +++ b/src/ui/Prompts.js @@ -7,14 +7,16 @@ var Prompts = (() => { async open(data) { promptData = data; this.close(); - this.currentWindow = await browser.windows.create({ + let options = { url: browser.extension.getURL("ui/prompt.html"), type: "panel", - allowScriptsToClose: true, - // titlePreface: "NoScript ", width: data.features.width, height: data.features.height, - }); + }; + if (UA.isMozilla) { + options.allowScriptsToClose = true; + } + this.currentWindow = await browser.windows.create(options); } async close() { if (this.currentWindow) { -- cgit v1.2.3 From 4e5f12a6c29d6adae8ae1a0236edb9d851846998 Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 00:31:00 +0100 Subject: Differentiate Chromium restricted URLs (where extensions cannot operate). --- src/lib/restricted.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lib/restricted.js b/src/lib/restricted.js index b9bddae..840ab69 100644 --- a/src/lib/restricted.js +++ b/src/lib/restricted.js @@ -1,22 +1,22 @@ { // see https://bugzilla.mozilla.org/show_bug.cgi?id=1415644 - let domains = [ - "accounts-static.cdn.mozilla.net", - "accounts.firefox.com", - "addons.cdn.mozilla.net", - "addons.mozilla.org", - "api.accounts.firefox.com", - "content.cdn.mozilla.net", - "content.cdn.mozilla.net", - "discovery.addons.mozilla.org", - "input.mozilla.org", - "install.mozilla.org", - "oauth.accounts.firefox.com", - "profile.accounts.firefox.com", - "support.mozilla.org", - "sync.services.mozilla.com", - "testpilot.firefox.com", - ]; + let domains = UA.isMozilla ? [ + "accounts-static.cdn.mozilla.net", + "accounts.firefox.com", + "addons.cdn.mozilla.net", + "addons.mozilla.org", + "api.accounts.firefox.com", + "content.cdn.mozilla.net", + "content.cdn.mozilla.net", + "discovery.addons.mozilla.org", + "input.mozilla.org", + "install.mozilla.org", + "oauth.accounts.firefox.com", + "profile.accounts.firefox.com", + "support.mozilla.org", + "sync.services.mozilla.com", + "testpilot.firefox.com", + ] : [ "chrome.google.com" ]; function isRestrictedURL(u) { try { -- cgit v1.2.3 From 9b5bd1c7756c37e6760a89b444110cf9352b14fd Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 00:31:43 +0100 Subject: Chromium-compatible UI stylesheets. --- src/ui/popup.css | 4 +++- src/ui/ui-hc.css | 4 ++++ src/ui/ui.css | 11 +++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ui/popup.css b/src/ui/popup.css index ad5e3d0..a0febc6 100644 --- a/src/ui/popup.css +++ b/src/ui/popup.css @@ -16,6 +16,7 @@ body { #top a { appearance: none !important; + webkit-appearance: none !important; -moz-appearance: none !important; width: 2em; height: 2em; @@ -119,8 +120,9 @@ body { } .hider-close { - -moz-appearance: none; appearance: none; + -webkit-appearance: none; + -moz-appearance: none; color: black; background: transparent; padding: 0; diff --git a/src/ui/ui-hc.css b/src/ui/ui-hc.css index e1c1944..eaa5914 100644 --- a/src/ui/ui-hc.css +++ b/src/ui/ui-hc.css @@ -5,10 +5,14 @@ input { } input[type="radio"] { + appearance: radio !important; + -webkit-appearance: radio !important; -moz-appearance: radio !important; padding-right: .2em !important; } input[type="checkbox"] { + appearance: checkbox !important; + -webkit-appearance: checkbox !important; -moz-appearance: checkbox !important; } diff --git a/src/ui/ui.css b/src/ui/ui.css index f59646a..1d40030 100644 --- a/src/ui/ui.css +++ b/src/ui/ui.css @@ -3,6 +3,7 @@ body { font-family: sans-serif; font: -moz-use-system-font; font-size: 12px; + min-width: 600px; } .mobile > body { @@ -139,6 +140,8 @@ input[type="checkbox"] { input.https-only { font-size: 1em; + appearance: none; + -webkit-appearance: none; -moz-appearance: none; background: url(/img/ui-http64.png) no-repeat center; background-size: 1.5em; @@ -186,6 +189,8 @@ span.preset { .presets input.preset { font-size: 1em; + appearance: none; + -webkit-appearance: none; -moz-appearance: none; background: url(/img/ui-no64.png) no-repeat center left; background-size: 1.5em; @@ -264,6 +269,8 @@ input.preset:active, input.preset:focus, input.preset:hover { } button.options { + appearance: none; + -webkit-appearance: none; -moz-appearance: none; border: none; background: none transparent; @@ -276,6 +283,8 @@ button.options { } .preset .options { + appearance: none; + -webkit-appearance: none; -moz-appearance: none; border: 0; @@ -315,6 +324,8 @@ input.preset[value="CUSTOM"] { input.temp { font-size: 1em; + appearance: none; + -webkit-appearance: none; -moz-appearance: none; margin: 0; padding: 0; -- cgit v1.2.3 From 2fa009673f3d51b14389f8e25075de394290b32c Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 01:13:26 +0100 Subject: Conditional CSS toggle for non-mozilla browsers. --- src/lib/UA.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/UA.js b/src/lib/UA.js index 0a751a4..1a1771a 100644 --- a/src/lib/UA.js +++ b/src/lib/UA.js @@ -2,7 +2,11 @@ var UA = { isMozilla: document.URL.startsWith("moz-"), } -if (!UA.isMozilla && typeof chrome === "object" && !chrome.tabs && typeof exportFunction === "undefined") { - // content script shims - window.exportFunction = () => {}; +if (!UA.isMozilla) { + if (typeof chrome === "object" && !chrome.tabs && typeof exportFunction === "undefined") { + // content script shims + window.exportFunction = () => {}; + } +} else { + document.documentElement.classList.add("mozwebext"); } -- cgit v1.2.3 From 20b689d015ea5743099bdafcac0c6ca6519c22db Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 01:16:33 +0100 Subject: Fallback XSS filtering to XSS Auditor since asynchronous webRequest handlers are not supported by Chromium. --- src/ui/options.css | 8 ++++++++ src/ui/options.html | 2 +- src/ui/options.js | 6 +++--- src/xss/XSS.js | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/ui/options.css b/src/ui/options.css index f7db24b..9230eb1 100644 --- a/src/ui/options.css +++ b/src/ui/options.css @@ -185,3 +185,11 @@ input[type="file"] { border-radius: .2em .2em 0 0; padding: .2em .4em; } + +#xss-opt-group { + display: none; +} + +.mozwebext #xss-opt-group { + display: block; +} diff --git a/src/ui/options.html b/src/ui/options.html index 4d44d75..5198795 100644 --- a/src/ui/options.html +++ b/src/ui/options.html @@ -101,7 +101,7 @@

-
+
(__MSG_XssFaq__) diff --git a/src/ui/options.js b/src/ui/options.js index 31cf5c3..a3f7fe8 100644 --- a/src/ui/options.js +++ b/src/ui/options.js @@ -8,9 +8,9 @@ let version = browser.runtime.getManifest().version; document.querySelector("#version").textContent = _("Version", version); // simple general options - + let opt = UI.wireOption; - + opt("global", o => { if (o) { policy.enforced = !o.checked; @@ -96,7 +96,7 @@ } let button = document.querySelector("#btn-delete-xss-choices"); let choices = UI.xssUserChoices; - button.disabled = Object.keys(choices).length === 0; + button.disabled = !choices || Object.keys(choices).length === 0; button.onclick = () => { UI.updateSettings({ xssUserChoices: {} diff --git a/src/xss/XSS.js b/src/xss/XSS.js index f95ea04..7851e98 100644 --- a/src/xss/XSS.js +++ b/src/xss/XSS.js @@ -113,6 +113,8 @@ var XSS = (() => { return { async start() { + if (!UA.isMozilla) return; // async webRequest is supported on Mozilla only + let {onBeforeRequest} = browser.webRequest; if (onBeforeRequest.hasListener(requestListener)) return; -- cgit v1.2.3 From d152a5387197e37bbb9838392f454f041e2478ea Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 01:17:09 +0100 Subject: Removed non-standard uneval() usage. --- src/test/Test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/Test.js b/src/test/Test.js index 8ca2ed7..22145a6 100644 --- a/src/test/Test.js +++ b/src/test/Test.js @@ -26,7 +26,7 @@ var Test = (() => { error(e); } this[r ? "passed" : "failed"]++; - log(`${r ? "PASSED" : "FAILED"} ${msg || uneval(test)}`); + log(`${r ? "PASSED" : "FAILED"} ${msg || test}`); if (typeof callback === "function") try { callback(r, test, msg); } catch(e) { -- cgit v1.2.3 From 0878ad2b0a0d3af5db66cc6a4f7d882e17a13365 Mon Sep 17 00:00:00 2001 From: hackademix Date: Fri, 1 Feb 2019 01:17:58 +0100 Subject: Remove usage of non-standard Array methods. --- src/content/ftp.js | 6 +++--- src/lib/persistent-tabs.js | 4 ++-- src/ui/toolbar.js | 2 +- src/xss/InjectionChecker.js | 9 ++++----- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/content/ftp.js b/src/content/ftp.js index 4b9585e..73ef5e8 100644 --- a/src/content/ftp.js +++ b/src/content/ftp.js @@ -9,7 +9,7 @@ ) { return; } - + gTable = document.getElementsByTagName("table")[0]; gTBody = gTable.tBodies[0]; if (gTBody.rows.length < 2) @@ -31,7 +31,7 @@ headCells[i].addEventListener("click", rowAction(i), true); } if (gUI_showHidden) { - gRows = Array.slice(gTBody.rows); + gRows = Array.from(gTBody.rows); hiddenObjects = gRows.some(row => row.className == "hidden-object"); } gTable.setAttribute("order", ""); @@ -60,7 +60,7 @@ } function orderBy(column) { if (!gRows) - gRows = Array.slice(gTBody.rows); + gRows = Array.from(gTBody.rows); var order; if (gOrderBy == column) { order = gTable.getAttribute("order") == "asc" ? "desc" : "asc"; diff --git a/src/lib/persistent-tabs.js b/src/lib/persistent-tabs.js index 8f7f711..f24c6dd 100644 --- a/src/lib/persistent-tabs.js +++ b/src/lib/persistent-tabs.js @@ -7,12 +7,12 @@ if (typeof flextabs === "function") { let rx = new RegExp(`(?:^|[#;])tab-${id}=(\\d+)(?:;|$)`); let current = location.hash.match(rx); console.log(`persisted %o`, current); - let toggles = tabs.querySelectorAll(".flextabs__toggle"); + let toggles = Array.from(tabs.querySelectorAll(".flextabs__toggle")); let currentToggle = toggles[current && parseInt(current[1]) || 0]; if (currentToggle) currentToggle.click(); for (let toggle of toggles) { toggle.addEventListener("click", e => { - let currentIdx = Array.indexOf(toggles, toggle); + let currentIdx = toggles.indexOf(toggle); location.hash = location.hash.split(";").filter(p => !rx.test(p)) .concat(`tab-${id}=${currentIdx}`).join(";"); }); diff --git a/src/ui/toolbar.js b/src/ui/toolbar.js index d2a2f6e..df3515e 100644 --- a/src/ui/toolbar.js +++ b/src/ui/toolbar.js @@ -87,7 +87,7 @@ } UI.local.toolbarLayout = { left, right, - hidden: Array.map(document.querySelectorAll("#top > .hider > .icon"), el => el.id), + hidden: Array.from(document.querySelectorAll("#top > .hider > .icon")).map(el => el.id), }; debug("%o", UI.local); diff --git a/src/xss/InjectionChecker.js b/src/xss/InjectionChecker.js index 181ea49..45ef29b 100644 --- a/src/xss/InjectionChecker.js +++ b/src/xss/InjectionChecker.js @@ -107,11 +107,10 @@ XSS.InjectionChecker = (async () => { var bs = { nq: new RegExp("[" + def + "]") }; - Array.forEach("'\"`", // special treatment for quotes - function(c) { - bs[c] = new RegExp("[" + def + c + "]"); - } - ); + for (let c of ['"', '"', '`']) { + // special treatment for quotes + bs[c] = new RegExp("[" + def + c + "]"); + } delete this.breakStops; return (this.breakStops = bs); }, -- cgit v1.2.3