From bf80404aa9e46ba5158a3f3bb854f12ae3e90909 Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Thu, 27 May 2021 17:33:49 +0200 Subject: [PATCH] feat: Support Chrome --- browser-polyfill.js | 1350 ++++++++++++++++++++++++++++++++++ browser_action/extension.css | 555 ++++++++++++++ browser_action/index.css | 4 + browser_action/index.html | 2 + manifest.json | 6 +- 5 files changed, 1914 insertions(+), 3 deletions(-) create mode 100644 browser-polyfill.js create mode 100644 browser_action/extension.css diff --git a/browser-polyfill.js b/browser-polyfill.js new file mode 100644 index 0000000..25c9e47 --- /dev/null +++ b/browser-polyfill.js @@ -0,0 +1,1350 @@ +(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.8.0 - Tue Apr 20 2021 11:27:38 */ + + /* -*- 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.reject + * The promise's rejection function. + * @param {object} metadata + * Metadata about the wrapped method which has created the callback. + * @param {boolean} metadata.singleCallbackArg + * Whether or not the promise is resolved with only the first + * argument of the callback, alternatively an array of all the + * callback arguments is resolved. By default, if the callback + * function is invoked with only a single argument, that will be + * resolved to the promise, while all arguments will be resolved as + * an array if multiple are given. + * + * @returns {function} + * The generated callback function. + */ + + const makeCallback = (promise, metadata) => { + return (...callbackArgs) => { + if (extensionAPIs.runtime.lastError) { + promise.reject( + new Error(extensionAPIs.runtime.lastError.message) + ); + } 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 {boolean} metadata.singleCallbackArg + * Whether or not the promise is resolved with only the first + * argument of the callback, alternatively an array of all the + * callback arguments is resolved. By default, if the callback + * function is invoked with only a single argument, that will be + * resolved to the promise, while all arguments will be resolved as + * an array if multiple are given. + * + * @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 if (hasOwnProperty(metadata, "*")) { + // Wrap all properties in * namespace. + value = wrapObject(value, wrappers[prop], metadata["*"]); + } 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)); + }, + }); + + const onRequestFinishedWrappers = new DefaultWeakMap((listener) => { + if (typeof listener !== "function") { + return listener; + } + /** + * Wraps an onRequestFinished listener function so that it will return a + * `getContent()` property which returns a `Promise` rather than using a + * callback API. + * + * @param {object} req + * The HAR entry object representing the network request. + */ + + return function onRequestFinished(req) { + const wrappedReq = wrapObject( + req, + {}, + /* wrappers */ + { + getContent: { + minArgs: 0, + maxArgs: 0, + }, + } + ); + listener(wrappedReq); + }; + }); // 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 (extensionAPIs.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 ( + extensionAPIs.runtime.lastError.message === + CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE + ) { + resolve(); + } else { + reject(new Error(extensionAPIs.runtime.lastError.message)); + } + } 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 = { + devtools: { + network: { + onRequestFinished: wrapEvent(onRequestFinishedWrappers), + }, + }, + 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: { + "*": settingMetadata, + }, + services: { + "*": settingMetadata, + }, + websites: { + "*": settingMetadata, + }, + }; + return wrapObject(extensionAPIs, staticWrappers, apiMetadata); + }; + + if ( + typeof chrome != "object" || + !chrome || + !chrome.runtime || + !chrome.runtime.id + ) { + throw new Error( + "This script should only be loaded in a browser extension." + ); + } // The build process adds a UMD wrapper around this file, which makes the + // `module` variable available. + + module.exports = wrapAPIs(chrome); + } else { + module.exports = browser; + } + } +); +//# sourceMappingURL=browser-polyfill.js.map diff --git a/browser_action/extension.css b/browser_action/extension.css new file mode 100644 index 0000000..e63c9ed --- /dev/null +++ b/browser_action/extension.css @@ -0,0 +1,555 @@ +/* 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/. */ + +/* Cela a été copié depuis + * https://hg.mozilla.org/mozilla-central/raw-file/tip/browser/components/extensions/extension.css + * */ + +/* stylelint-disable property-no-vendor-prefix */ +/* stylelint-disable property-no-vendor-prefix */ + +/* Global */ +html, +body { + background: transparent; + box-sizing: border-box; + color: #222426; + cursor: default; + display: flex; + flex-direction: column; + font: caption; + margin: 0; + padding: 0; + user-select: none; +} + +body * { + box-sizing: border-box; + text-align: start; +} + +.browser-style { + appearance: none; + margin-bottom: 6px; + text-align: left; +} + +/* stylelint-disable property-no-vendor-prefix */ +/* Buttons */ +button.browser-style, +select.browser-style { + background-color: #fbfbfb; + border: 1px solid #b1b1b1; + box-shadow: 0 0 0 0 transparent; + font: caption; + height: 24px; + outline: 0 !important; + padding: 0 8px 0; + transition-duration: 250ms; + transition-property: box-shadow, border; +} + +select.browser-style { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==); + background-position: calc(100% - 4px) center; + background-repeat: no-repeat; + padding-inline-end: 24px; + text-overflow: ellipsis; +} + +label.browser-style-label { + font: caption; +} + +button.browser-style::-moz-focus-inner { + border: 0; + outline: 0; +} + +/* Dropdowns */ +select.browser-style { + background-color: #fbfbfb; + border: 1px solid #b1b1b1; + box-shadow: 0 0 0 0 transparent; + font: caption; + height: 24px; + outline: 0 !important; + padding: 0 8px 0; + transition-duration: 250ms; + transition-property: box-shadow, border; +} + +select.browser-style { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==); + background-position: calc(100% - 4px) center; + background-repeat: no-repeat; + padding-inline-end: 24px; + text-overflow: ellipsis; +} + +select.browser-style:-moz-focusring { + color: transparent; + text-shadow: 0 0 0 #000; +} + +select.browser-style:-moz-focusring * { + color: #000; + text-shadow: none; +} + +button.browser-style.hover, +select.browser-style.hover { + background-color: #ebebeb; + border: 1px solid #b1b1b1; +} + +button.browser-style.pressed, +select.browser-style.pressed { + background-color: #d4d4d4; + border: 1px solid #858585; +} + +button.browser-style:disabled, +select.browser-style:disabled { + color: #999; + opacity: 0.5; +} + +button.browser-style.focused, +select.browser-style.focused { + border-color: #fff; + box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75); +} + +button.browser-style.default { + background-color: #0996f8; + border-color: #0670cc; + color: #fff; +} + +button.browser-style.default.hover { + background-color: #0670cc; + border-color: #005bab; +} + +button.browser-style.default.pressed { + background-color: #005bab; + border-color: #004480; +} + +button.browser-style.default.focused { + border-color: #fff; +} + +.browser-style > label { + user-select: none; +} + +.browser-style.disabled > label { + color: #999; + opacity: 0.5; +} + +/* Radio Buttons */ +.browser-style > input[type="radio"] { + appearance: none; + background-color: #fff; + background-position: center; + border: 1px solid #b1b1b1; + border-radius: 50%; + content: ""; + display: inline-block; + height: 16px; + margin-right: 6px; + vertical-align: text-top; + width: 16px; +} + +.browser-style > input[type="radio"]:hover, +.browser-style.hover > input[type="radio"]:not(:active) { + background-color: #fbfbfb; + border-color: #b1b1b1; +} + +.browser-style > input[type="radio"]:hover:active, +.browser-style.pressed > input[type="radio"]:not(:active) { + background-color: #ebebeb; + border-color: #858585; +} + +.browser-style > input[type="radio"]:checked { + background-color: #0996f8; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8Y2lyY2xlIGN4PSI4IiBjeT0iOCIgcj0iNCIgZmlsbD0iI2ZmZiIgLz4KPC9zdmc+Cg==); + border-color: #0670cc; +} + +.browser-style > input[type="radio"]:checked:hover, +.browser-style.hover > input[type="radio"]:checked:not(:active) { + background-color: #0670cc; + border-color: #005bab; +} + +.browser-style > input[type="radio"]:checked:hover:active, +.browser-style.pressed > input[type="radio"]:checked:not(:active) { + background-color: #005bab; + border-color: #004480; +} + +.browser-style.focused > input[type="radio"] { + border-color: #0996f8; + box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75); +} + +.browser-style.focused > input[type="radio"]:checked { + border-color: #fff; +} + +/* Checkboxes */ +.browser-style > input[type="checkbox"] { + appearance: none; + background-color: #fff; + background-position: center; + border: 1px solid #b1b1b1; + content: ""; + display: inline-block; + height: 16px; + margin-right: 6px; + vertical-align: text-top; + width: 16px; +} + +.browser-style > input[type="checkbox"]:hover, +.browser-style.hover > input[type="checkbox"]:not(:active) { + background-color: #fbfbfb; + border-color: #b1b1b1; +} + +.browser-style > input[type="checkbox"]:hover:active, +.browser-style.pressed > input[type="checkbox"]:not(:active) { + background-color: #ebebeb; + border-color: #858585; +} + +.browser-style > input[type="checkbox"]:checked { + background-color: #0996f8; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNNy43LDEyLjkgQzcuNCwxMy4zIDYuOCwxMy40IDYuNCwxMyBMMy4yLDkuOCBDMi44LDkuNCAyLjgsOC42IDMuMiw4LjIgQzMuNiw3LjggNC40LDcuOCA0LjgsOC4yIEw2LjksMTAuMyBMMTEuMSw0LjQgQzExLjUsMy45IDEyLjIsMy44IDEyLjcsNC4xIEMxMy4yLDQuNSAxMy4zLDUuMiAxMyw1LjcgTDcuNywxMi45IEw3LjcsMTIuOSBaIiBmaWxsPSIjZmZmIiAvPgo8L3N2Zz4K); + border-color: #0670cc; +} + +.browser-style > input[type="checkbox"]:checked:hover, +.browser-style.hover > input[type="checkbox"]:checked:not(:active) { + background-color: #0670cc; + border-color: #005bab; +} + +.browser-style > input[type="checkbox"]:checked:hover:active, +.browser-style.pressed > input[type="checkbox"]:checked:not(:active) { + background-color: #005bab; + border-color: #004480; +} + +.browser-style.focused > input[type="checkbox"] { + border-color: #0996f8; + box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75); +} + +.browser-style.focused > input[type="checkbox"]:checked { + border-color: #fff; +} + +/* Expander Button */ +button.browser-style.expander { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNOCwxMkwzLDcsNCw2bDQsNCw0LTQsMSwxWiIgZmlsbD0iIzZBNkE2QSIgLz4KPC9zdmc+Cg==); + background-position: center; + background-repeat: no-repeat; + height: 24px; + padding: 0; + width: 24px; +} + +/* Interactive States */ +button.browser-style:enabled:hover:not(.pressed, .focused), +select.browser-style:enabled:hover:not(.pressed, .focused) { + background-color: #ebebeb; + border: 1px solid #b1b1b1; +} + +button.browser-style:enabled:hover:active:not(.hover, .focused), +select.browser-style:enabled:hover:active:not(.hover, .focused) { + background-color: #d4d4d4; + border: 1px solid #858585; +} + +button.browser-style.default:enabled:hover:not(.pressed, .focused) { + background-color: #0670cc; + border-color: #005bab; +} + +button.browser-style.default:enabled:hover:active:not(.hover, .focused) { + background-color: #005bab; + border-color: #004480; +} + +button.browser-style:focus:enabled { + border-color: #fff !important; + box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75); +} + +/* Fields */ +.browser-style > input[type="text"], +textarea.browser-style { + background-color: #fff; + border: 1px solid #b1b1b1; + box-shadow: 0 0 0 0 rgba(97, 181, 255, 0); + font: caption; + padding: 0 6px 0; + transition-duration: 250ms; + transition-property: box-shadow; +} + +.browser-style > input[type="text"] { + height: 24px; +} + +.browser-style > input[type="text"].hover, +textarea.browser-style.hover { + border: 1px solid #858585; +} + +.browser-style > input[type="text"]:disabled, +textarea.browser-style:disabled { + color: #999; + opacity: 0.5; +} + +.browser-style > input[type="text"].focused, +textarea.browser-style.focused { + border-color: #0996f8; + box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75); +} + +/* Interactive States */ +.browser-style > input[type="text"]:enabled:hover, +textarea.browser-style:enabled:hover { + border: 1px solid #858585; +} + +.browser-style > input[type="text"]:focus, +.browser-style > input[type="text"]:focus:hover, +textarea.browser-style:focus, +textarea.browser-style:focus:hover { + border-color: #0996f8; + box-shadow: 0 0 0 2px rgba(97, 181, 255, 0.75); +} + +.browser-style > input[type="text"]:invalid:not(:focus), +textarea.browser-style:invalid:not(:focus) { + border-color: var(--red-60); + box-shadow: 0 0 0 1px var(--red-60), 0 0 0 4px rgba(251, 0, 34, 0.3); +} + +/* stylelint-disable property-no-vendor-prefix */ +.panel-section { + display: flex; + flex-direction: row; +} + +.panel-section-separator { + background-color: rgba(0, 0, 0, 0.15); + min-height: 1px; +} + +/* Panel Section - Header */ +.panel-section-header { + border-bottom: 1px solid rgba(0, 0, 0, 0.15); + padding: 16px; +} + +.panel-section-header > .icon-section-header { + background-position: center center; + background-repeat: no-repeat; + height: 32px; + margin-right: 16px; + position: relative; + width: 32px; +} + +.panel-section-header > .text-section-header { + align-self: center; + font-size: 1.385em; + font-weight: lighter; +} + +/* Panel Section - List */ +.panel-section-list { + flex-direction: column; + padding: 4px 0; +} + +.panel-list-item { + align-items: center; + display: flex; + flex-direction: row; + height: 24px; + padding: 0 16px; +} + +.panel-list-item:not(.disabled):hover { + background-color: rgba(0, 0, 0, 0.06); + border-block: 1px solid rgba(0, 0, 0, 0.1); +} + +.panel-list-item:not(.disabled):hover:active { + background-color: rgba(0, 0, 0, 0.1); +} + +.panel-list-item.disabled { + color: #999; +} + +.panel-list-item > .icon { + flex-grow: 0; + flex-shrink: 0; +} + +.panel-list-item > .text { + flex-grow: 10; +} + +.panel-list-item > .text-shortcut { + color: #808080; + font-family: "Lucida Grande", caption; + font-size: 0.847em; + justify-content: flex-end; +} + +.panel-section-list .panel-section-separator { + margin: 4px 0; +} + +/* Panel Section - Form Elements */ +.panel-section-formElements { + display: flex; + flex-direction: column; + padding: 16px; +} + +.panel-formElements-item { + align-items: center; + display: flex; + flex-direction: row; + margin-bottom: 12px; +} + +.panel-formElements-item:last-child { + margin-bottom: 0; +} + +.panel-formElements-item label { + flex-shrink: 0; + margin-right: 6px; + text-align: right; +} + +.panel-formElements-item input[type="text"], +.panel-formElements-item select.browser-style { + flex-grow: 1; +} + +/* Panel Section - Footer */ +.panel-section-footer { + background-color: rgba(0, 0, 0, 0.06); + border-top: 1px solid rgba(0, 0, 0, 0.15); + color: #1a1a1a; + display: flex; + flex-direction: row; + height: 41px; + margin-top: -1px; + padding: 0; +} + +.panel-section-footer-button { + flex: 1 1 auto; + height: 100%; + margin: 0 -1px; + padding: 12px; + text-align: center; +} + +.panel-section-footer-button > .text-shortcut { + color: #808080; + font-family: "Lucida Grande", caption; + font-size: 0.847em; +} + +.panel-section-footer-button:hover { + background-color: rgba(0, 0, 0, 0.06); +} + +.panel-section-footer-button:hover:active { + background-color: rgba(0, 0, 0, 0.1); +} + +.panel-section-footer-button.default { + background-color: #0996f8; + box-shadow: 0 1px 0 #0670cc inset; + color: #fff; +} + +.panel-section-footer-button.default:hover { + background-color: #0670cc; + box-shadow: 0 1px 0 #005bab inset; +} + +.panel-section-footer-button.default:hover:active { + background-color: #005bab; + box-shadow: 0 1px 0 #004480 inset; +} + +.panel-section-footer-separator { + background-color: rgba(0, 0, 0, 0.1); + width: 1px; + z-index: 99; +} + +/* Panel Section - Tabs */ +.panel-section-tabs { + color: #1a1a1a; + display: flex; + flex-direction: row; + height: 41px; + margin-bottom: -1px; + padding: 0; +} + +.panel-section-tabs-button { + flex: 1 1 auto; + height: 100%; + margin: 0 -1px; + padding: 12px; + text-align: center; +} + +.panel-section-tabs-button:hover { + background-color: rgba(0, 0, 0, 0.06); +} + +.panel-section-tabs-button:hover:active { + background-color: rgba(0, 0, 0, 0.1); +} + +.panel-section-tabs-button.selected { + box-shadow: 0 -1px 0 #0670cc inset, 0 -4px 0 #0996f8 inset; + color: #0996f8; +} + +.panel-section-tabs-button.selected:hover { + color: #0670cc; +} + +.panel-section-tabs-separator { + background-color: rgba(0, 0, 0, 0.1); + width: 1px; + z-index: 99; +} diff --git a/browser_action/index.css b/browser_action/index.css index ee1643e..a1e6d53 100644 --- a/browser_action/index.css +++ b/browser_action/index.css @@ -1,3 +1,7 @@ +html { + min-width: 800px; /* nécessaire pour que Chrome affiche une popup de taille suffisante */ +} + h1 { display: flex; justify-content: center; diff --git a/browser_action/index.html b/browser_action/index.html index 93d5310..59d56fa 100644 --- a/browser_action/index.html +++ b/browser_action/index.html @@ -5,6 +5,7 @@ vaccin.click : mon vaccin COVID-19 en 1 click ! + @@ -125,6 +126,7 @@ + diff --git a/manifest.json b/manifest.json index 9c80ad1..54fcbc4 100644 --- a/manifest.json +++ b/manifest.json @@ -20,16 +20,16 @@ "content_scripts": [ { "matches": ["*://*.doctolib.fr/*"], - "js": ["content_scripts/doctolib/search.js"] + "js": ["browser-polyfill.js", "content_scripts/doctolib/search.js"] }, { "matches": ["*://*.doctolib.fr/*"], - "js": ["content_scripts/doctolib/book.js"], + "js": ["browser-polyfill.js", "content_scripts/doctolib/book.js"], "all_frames": true } ], "background": { - "scripts": ["background_scripts/index.js"] + "scripts": ["browser-polyfill.js", "background_scripts/index.js"] }, "browser_specific_settings": { "gecko": {