1188 lines
35 KiB
JavaScript
1188 lines
35 KiB
JavaScript
(function (global, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
define('webextension-polyfill', ['module'], factory)
|
|
} else if (typeof exports !== 'undefined') {
|
|
factory(module)
|
|
} else {
|
|
const mod = {
|
|
exports: {}
|
|
}
|
|
factory(mod)
|
|
global.browser = mod.exports
|
|
}
|
|
})(this, function (module) {
|
|
/* webextension-polyfill - v0.5.0 - Thu Sep 26 2019 22:22:26 */
|
|
/* -*- 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
|
|
}
|
|
}
|
|
},
|
|
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
|
|
},
|
|
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<function>}
|
|
* 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)
|
|
}
|
|
})
|
|
}
|
|
|
|
const 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<object>}
|
|
*/
|
|
const wrapObject = (target, wrappers = {}, metadata = {}) => {
|
|
const cache = Object.create(null)
|
|
const 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.
|
|
const 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.
|
|
const 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<function, function>} 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
|
|
const 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(extensionAPIs.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(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
|