diff options
Diffstat (limited to 'toolkit/components/webextensions/ExtensionUtils.jsm')
-rw-r--r-- | toolkit/components/webextensions/ExtensionUtils.jsm | 1216 |
1 files changed, 0 insertions, 1216 deletions
diff --git a/toolkit/components/webextensions/ExtensionUtils.jsm b/toolkit/components/webextensions/ExtensionUtils.jsm deleted file mode 100644 index 04e767cb5c..0000000000 --- a/toolkit/components/webextensions/ExtensionUtils.jsm +++ /dev/null @@ -1,1216 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["ExtensionUtils"]; - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -const INTEGER = /^[1-9]\d*$/; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", - "resource://gre/modules/AddonManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI", - "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector", - "resource:///modules/translation/LanguageDetector.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Locale", - "resource://gre/modules/Locale.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", - "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Preferences", - "resource://gre/modules/Preferences.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Schemas", - "resource://gre/modules/Schemas.jsm"); - -XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService", - "@mozilla.org/content/style-sheet-service;1", - "nsIStyleSheetService"); - -function getConsole() { - return new ConsoleAPI({ - maxLogLevelPref: "extensions.webextensions.log.level", - prefix: "WebExtensions", - }); -} - -XPCOMUtils.defineLazyGetter(this, "console", getConsole); - -let nextId = 0; -const {uniqueProcessID} = Services.appinfo; - -function getUniqueId() { - return `${nextId++}-${uniqueProcessID}`; -} - -/** - * An Error subclass for which complete error messages are always passed - * to extensions, rather than being interpreted as an unknown error. - */ -class ExtensionError extends Error {} - -function filterStack(error) { - return String(error.stack).replace(/(^.*(Task\.jsm|Promise-backend\.js).*\n)+/gm, "<Promise Chain>\n"); -} - -// Run a function and report exceptions. -function runSafeSyncWithoutClone(f, ...args) { - try { - return f(...args); - } catch (e) { - dump(`Extension error: ${e} ${e.fileName} ${e.lineNumber}\n[[Exception stack\n${filterStack(e)}Current stack\n${filterStack(Error())}]]\n`); - Cu.reportError(e); - } -} - -// Run a function and report exceptions. -function runSafeWithoutClone(f, ...args) { - if (typeof(f) != "function") { - dump(`Extension error: expected function\n${filterStack(Error())}`); - return; - } - - Promise.resolve().then(() => { - runSafeSyncWithoutClone(f, ...args); - }); -} - -// Run a function, cloning arguments into context.cloneScope, and -// report exceptions. |f| is expected to be in context.cloneScope. -function runSafeSync(context, f, ...args) { - if (context.unloaded) { - Cu.reportError("runSafeSync called after context unloaded"); - return; - } - - try { - args = Cu.cloneInto(args, context.cloneScope); - } catch (e) { - Cu.reportError(e); - dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`); - } - return runSafeSyncWithoutClone(f, ...args); -} - -// Run a function, cloning arguments into context.cloneScope, and -// report exceptions. |f| is expected to be in context.cloneScope. -function runSafe(context, f, ...args) { - try { - args = Cu.cloneInto(args, context.cloneScope); - } catch (e) { - Cu.reportError(e); - dump(`runSafe failure: cloning into ${context.cloneScope}: ${e}\n\n${filterStack(Error())}`); - } - if (context.unloaded) { - dump(`runSafe failure: context is already unloaded ${filterStack(new Error())}\n`); - return undefined; - } - return runSafeWithoutClone(f, ...args); -} - -function getInnerWindowID(window) { - return window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .currentInnerWindowID; -} - -// Return true if the given value is an instance of the given -// native type. -function instanceOf(value, type) { - return {}.toString.call(value) == `[object ${type}]`; -} - -// Extend the object |obj| with the property descriptors of each object in -// |args|. -function extend(obj, ...args) { - for (let arg of args) { - let props = [...Object.getOwnPropertyNames(arg), - ...Object.getOwnPropertySymbols(arg)]; - for (let prop of props) { - let descriptor = Object.getOwnPropertyDescriptor(arg, prop); - Object.defineProperty(obj, prop, descriptor); - } - } - - return obj; -} - -/** - * Similar to a WeakMap, but creates a new key with the given - * constructor if one is not present. - */ -class DefaultWeakMap extends WeakMap { - constructor(defaultConstructor, init) { - super(init); - this.defaultConstructor = defaultConstructor; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.defaultConstructor(key)); - } - return super.get(key); - } -} - -class DefaultMap extends Map { - constructor(defaultConstructor, init) { - super(init); - this.defaultConstructor = defaultConstructor; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.defaultConstructor(key)); - } - return super.get(key); - } -} - -class SpreadArgs extends Array { - constructor(args) { - super(); - this.push(...args); - } -} - -// Manages icon details for toolbar buttons in the |pageAction| and -// |browserAction| APIs. -let IconDetails = { - // Normalizes the various acceptable input formats into an object - // with icon size as key and icon URL as value. - // - // If a context is specified (function is called from an extension): - // Throws an error if an invalid icon size was provided or the - // extension is not allowed to load the specified resources. - // - // If no context is specified, instead of throwing an error, this - // function simply logs a warning message. - normalize(details, extension, context = null) { - let result = {}; - - try { - if (details.imageData) { - let imageData = details.imageData; - - if (typeof imageData == "string") { - imageData = {"19": imageData}; - } - - for (let size of Object.keys(imageData)) { - if (!INTEGER.test(size)) { - throw new ExtensionError(`Invalid icon size ${size}, must be an integer`); - } - result[size] = imageData[size]; - } - } - - if (details.path) { - let path = details.path; - if (typeof path != "object") { - path = {"19": path}; - } - - let baseURI = context ? context.uri : extension.baseURI; - - for (let size of Object.keys(path)) { - if (!INTEGER.test(size)) { - throw new ExtensionError(`Invalid icon size ${size}, must be an integer`); - } - - let url = baseURI.resolve(path[size]); - - // The Chrome documentation specifies these parameters as - // relative paths. We currently accept absolute URLs as well, - // which means we need to check that the extension is allowed - // to load them. This will throw an error if it's not allowed. - try { - Services.scriptSecurityManager.checkLoadURIStrWithPrincipal( - extension.principal, url, - Services.scriptSecurityManager.DISALLOW_SCRIPT); - } catch (e) { - throw new ExtensionError(`Illegal URL ${url}`); - } - - result[size] = url; - } - } - } catch (e) { - // Function is called from extension code, delegate error. - if (context) { - throw e; - } - // If there's no context, it's because we're handling this - // as a manifest directive. Log a warning rather than - // raising an error. - extension.manifestError(`Invalid icon data: ${e}`); - } - - return result; - }, - - // Returns the appropriate icon URL for the given icons object and the - // screen resolution of the given window. - getPreferredIcon(icons, extension = null, size = 16) { - const DEFAULT = "chrome://browser/content/extension.svg"; - - let bestSize = null; - if (icons[size]) { - bestSize = size; - } else if (icons[2 * size]) { - bestSize = 2 * size; - } else { - let sizes = Object.keys(icons) - .map(key => parseInt(key, 10)) - .sort((a, b) => a - b); - - bestSize = sizes.find(candidate => candidate > size) || sizes.pop(); - } - - if (bestSize) { - return {size: bestSize, icon: icons[bestSize]}; - } - - return {size, icon: DEFAULT}; - }, - - convertImageURLToDataURL(imageURL, contentWindow, browserWindow, size = 18) { - return new Promise((resolve, reject) => { - let image = new contentWindow.Image(); - image.onload = function() { - let canvas = contentWindow.document.createElement("canvas"); - let ctx = canvas.getContext("2d"); - let dSize = size * browserWindow.devicePixelRatio; - - // Scales the image while maintaing width to height ratio. - // If the width and height differ, the image is centered using the - // smaller of the two dimensions. - let dWidth, dHeight, dx, dy; - if (this.width > this.height) { - dWidth = dSize; - dHeight = image.height * (dSize / image.width); - dx = 0; - dy = (dSize - dHeight) / 2; - } else { - dWidth = image.width * (dSize / image.height); - dHeight = dSize; - dx = (dSize - dWidth) / 2; - dy = 0; - } - - ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight); - resolve(canvas.toDataURL("image/png")); - }; - image.onerror = reject; - image.src = imageURL; - }); - }, -}; - -const LISTENERS = Symbol("listeners"); - -class EventEmitter { - constructor() { - this[LISTENERS] = new Map(); - } - - /** - * Adds the given function as a listener for the given event. - * - * The listener function may optionally return a Promise which - * resolves when it has completed all operations which event - * dispatchers may need to block on. - * - * @param {string} event - * The name of the event to listen for. - * @param {function(string, ...any)} listener - * The listener to call when events are emitted. - */ - on(event, listener) { - if (!this[LISTENERS].has(event)) { - this[LISTENERS].set(event, new Set()); - } - - this[LISTENERS].get(event).add(listener); - } - - /** - * Removes the given function as a listener for the given event. - * - * @param {string} event - * The name of the event to stop listening for. - * @param {function(string, ...any)} listener - * The listener function to remove. - */ - off(event, listener) { - if (this[LISTENERS].has(event)) { - let set = this[LISTENERS].get(event); - - set.delete(listener); - if (!set.size) { - this[LISTENERS].delete(event); - } - } - } - - /** - * Triggers all listeners for the given event, and returns a promise - * which resolves when all listeners have been called, and any - * promises they have returned have likewise resolved. - * - * @param {string} event - * The name of the event to emit. - * @param {any} args - * Arbitrary arguments to pass to the listener functions, after - * the event name. - * @returns {Promise} - */ - emit(event, ...args) { - let listeners = this[LISTENERS].get(event) || new Set(); - - let promises = Array.from(listeners, listener => { - return runSafeSyncWithoutClone(listener, event, ...args); - }); - - return Promise.all(promises); - } -} - -function LocaleData(data) { - this.defaultLocale = data.defaultLocale; - this.selectedLocale = data.selectedLocale; - this.locales = data.locales || new Map(); - this.warnedMissingKeys = new Set(); - - // Map(locale-name -> Map(message-key -> localized-string)) - // - // Contains a key for each loaded locale, each of which is a - // Map of message keys to their localized strings. - this.messages = data.messages || new Map(); - - if (data.builtinMessages) { - this.messages.set(this.BUILTIN, data.builtinMessages); - } -} - - -LocaleData.prototype = { - // Representation of the object to send to content processes. This - // should include anything the content process might need. - serialize() { - return { - defaultLocale: this.defaultLocale, - selectedLocale: this.selectedLocale, - messages: this.messages, - locales: this.locales, - }; - }, - - BUILTIN: "@@BUILTIN_MESSAGES", - - has(locale) { - return this.messages.has(locale); - }, - - // https://developer.chrome.com/extensions/i18n - localizeMessage(message, substitutions = [], options = {}) { - let defaultOptions = { - locale: this.selectedLocale, - defaultValue: "", - cloneScope: null, - }; - - options = Object.assign(defaultOptions, options); - - let locales = new Set([this.BUILTIN, options.locale, this.defaultLocale] - .filter(locale => this.messages.has(locale))); - - // Message names are case-insensitive, so normalize them to lower-case. - message = message.toLowerCase(); - for (let locale of locales) { - let messages = this.messages.get(locale); - if (messages.has(message)) { - let str = messages.get(message); - - if (!Array.isArray(substitutions)) { - substitutions = [substitutions]; - } - - let replacer = (matched, index, dollarSigns) => { - if (index) { - // This is not quite Chrome-compatible. Chrome consumes any number - // of digits following the $, but only accepts 9 substitutions. We - // accept any number of substitutions. - index = parseInt(index, 10) - 1; - return index in substitutions ? substitutions[index] : ""; - } - // For any series of contiguous `$`s, the first is dropped, and - // the rest remain in the output string. - return dollarSigns; - }; - return str.replace(/\$(?:([1-9]\d*)|(\$+))/g, replacer); - } - } - - // Check for certain pre-defined messages. - if (message == "@@ui_locale") { - return this.uiLocale; - } else if (message.startsWith("@@bidi_")) { - let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry); - let rtl = registry.isLocaleRTL("global"); - - if (message == "@@bidi_dir") { - return rtl ? "rtl" : "ltr"; - } else if (message == "@@bidi_reversed_dir") { - return rtl ? "ltr" : "rtl"; - } else if (message == "@@bidi_start_edge") { - return rtl ? "right" : "left"; - } else if (message == "@@bidi_end_edge") { - return rtl ? "left" : "right"; - } - } - - if (!this.warnedMissingKeys.has(message)) { - let error = `Unknown localization message ${message}`; - if (options.cloneScope) { - error = new options.cloneScope.Error(error); - } - Cu.reportError(error); - this.warnedMissingKeys.add(message); - } - return options.defaultValue; - }, - - // Localize a string, replacing all |__MSG_(.*)__| tokens with the - // matching string from the current locale, as determined by - // |this.selectedLocale|. - // - // This may not be called before calling either |initLocale| or - // |initAllLocales|. - localize(str, locale = this.selectedLocale) { - if (!str) { - return str; - } - - return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => { - return this.localizeMessage(message, [], {locale, defaultValue: matched}); - }); - }, - - // Validates the contents of a locale JSON file, normalizes the - // messages into a Map of message key -> localized string pairs. - addLocale(locale, messages, extension) { - let result = new Map(); - - // Chrome does not document the semantics of its localization - // system very well. It handles replacements by pre-processing - // messages, replacing |$[a-zA-Z0-9@_]+$| tokens with the value of their - // replacements. Later, it processes the resulting string for - // |$[0-9]| replacements. - // - // Again, it does not document this, but it accepts any number - // of sequential |$|s, and replaces them with that number minus - // 1. It also accepts |$| followed by any number of sequential - // digits, but refuses to process a localized string which - // provides more than 9 substitutions. - if (!instanceOf(messages, "Object")) { - extension.packagingError(`Invalid locale data for ${locale}`); - return result; - } - - for (let key of Object.keys(messages)) { - let msg = messages[key]; - - if (!instanceOf(msg, "Object") || typeof(msg.message) != "string") { - extension.packagingError(`Invalid locale message data for ${locale}, message ${JSON.stringify(key)}`); - continue; - } - - // Substitutions are case-insensitive, so normalize all of their names - // to lower-case. - let placeholders = new Map(); - if (instanceOf(msg.placeholders, "Object")) { - for (let key of Object.keys(msg.placeholders)) { - placeholders.set(key.toLowerCase(), msg.placeholders[key]); - } - } - - let replacer = (match, name) => { - let replacement = placeholders.get(name.toLowerCase()); - if (instanceOf(replacement, "Object") && "content" in replacement) { - return replacement.content; - } - return ""; - }; - - let value = msg.message.replace(/\$([A-Za-z0-9@_]+)\$/g, replacer); - - // Message names are also case-insensitive, so normalize them to lower-case. - result.set(key.toLowerCase(), value); - } - - this.messages.set(locale, result); - return result; - }, - - get acceptLanguages() { - let result = Preferences.get("intl.accept_languages", "", Ci.nsIPrefLocalizedString); - return result.split(/\s*,\s*/g); - }, - - - get uiLocale() { - // Return the browser locale, but convert it to a Chrome-style - // locale code. - return Locale.getLocale().replace(/-/g, "_"); - }, -}; - -// This is a generic class for managing event listeners. Example usage: -// -// new EventManager(context, "api.subAPI", fire => { -// let listener = (...) => { -// // Fire any listeners registered with addListener. -// fire(arg1, arg2); -// }; -// // Register the listener. -// SomehowRegisterListener(listener); -// return () => { -// // Return a way to unregister the listener. -// SomehowUnregisterListener(listener); -// }; -// }).api() -// -// The result is an object with addListener, removeListener, and -// hasListener methods. |context| is an add-on scope (either an -// ExtensionContext in the chrome process or ExtensionContext in a -// content process). |name| is for debugging. |register| is a function -// to register the listener. |register| is only called once, even if -// multiple listeners are registered. |register| should return an -// unregister function that will unregister the listener. -function EventManager(context, name, register) { - this.context = context; - this.name = name; - this.register = register; - this.unregister = null; - this.callbacks = new Set(); -} - -EventManager.prototype = { - addListener(callback) { - if (typeof(callback) != "function") { - dump(`Expected function\n${Error().stack}`); - return; - } - if (this.context.unloaded) { - dump(`Cannot add listener to ${this.name} after context unloaded`); - return; - } - - if (!this.callbacks.size) { - this.context.callOnClose(this); - - let fireFunc = this.fire.bind(this); - let fireWithoutClone = this.fireWithoutClone.bind(this); - fireFunc.withoutClone = fireWithoutClone; - this.unregister = this.register(fireFunc); - } - this.callbacks.add(callback); - }, - - removeListener(callback) { - if (!this.callbacks.size) { - return; - } - - this.callbacks.delete(callback); - if (this.callbacks.size == 0) { - this.unregister(); - this.unregister = null; - - this.context.forgetOnClose(this); - } - }, - - hasListener(callback) { - return this.callbacks.has(callback); - }, - - fire(...args) { - this._fireCommon("runSafe", args); - }, - - fireWithoutClone(...args) { - this._fireCommon("runSafeWithoutClone", args); - }, - - _fireCommon(runSafeMethod, args) { - for (let callback of this.callbacks) { - Promise.resolve(callback).then(callback => { - if (this.context.unloaded) { - dump(`${this.name} event fired after context unloaded.\n`); - } else if (!this.context.active) { - dump(`${this.name} event fired while context is inactive.\n`); - } else if (this.callbacks.has(callback)) { - this.context[runSafeMethod](callback, ...args); - } - }); - } - }, - - close() { - if (this.callbacks.size) { - this.unregister(); - } - this.callbacks.clear(); - this.register = null; - this.unregister = null; - }, - - api() { - return { - addListener: callback => this.addListener(callback), - removeListener: callback => this.removeListener(callback), - hasListener: callback => this.hasListener(callback), - }; - }, -}; - -// Similar to EventManager, but it doesn't try to consolidate event -// notifications. Each addListener call causes us to register once. It -// allows extra arguments to be passed to addListener. -function SingletonEventManager(context, name, register) { - this.context = context; - this.name = name; - this.register = register; - this.unregister = new Map(); -} - -SingletonEventManager.prototype = { - addListener(callback, ...args) { - let wrappedCallback = (...args) => { - if (this.context.unloaded) { - dump(`${this.name} event fired after context unloaded.\n`); - } else if (this.unregister.has(callback)) { - return callback(...args); - } - }; - - let unregister = this.register(wrappedCallback, ...args); - this.unregister.set(callback, unregister); - this.context.callOnClose(this); - }, - - removeListener(callback) { - if (!this.unregister.has(callback)) { - return; - } - - let unregister = this.unregister.get(callback); - this.unregister.delete(callback); - unregister(); - }, - - hasListener(callback) { - return this.unregister.has(callback); - }, - - close() { - for (let unregister of this.unregister.values()) { - unregister(); - } - }, - - api() { - return { - addListener: (...args) => this.addListener(...args), - removeListener: (...args) => this.removeListener(...args), - hasListener: (...args) => this.hasListener(...args), - }; - }, -}; - -// Simple API for event listeners where events never fire. -function ignoreEvent(context, name) { - return { - addListener: function(callback) { - let id = context.extension.id; - let frame = Components.stack.caller; - let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`; - let scriptError = Cc["@mozilla.org/scripterror;1"] - .createInstance(Ci.nsIScriptError); - scriptError.init(msg, frame.filename, null, frame.lineNumber, - frame.columnNumber, Ci.nsIScriptError.warningFlag, - "content javascript"); - let consoleService = Cc["@mozilla.org/consoleservice;1"] - .getService(Ci.nsIConsoleService); - consoleService.logMessage(scriptError); - }, - removeListener: function(callback) {}, - hasListener: function(callback) {}, - }; -} - -// Copy an API object from |source| into the scope |dest|. -function injectAPI(source, dest) { - for (let prop in source) { - // Skip names prefixed with '_'. - if (prop[0] == "_") { - continue; - } - - let desc = Object.getOwnPropertyDescriptor(source, prop); - if (typeof(desc.value) == "function") { - Cu.exportFunction(desc.value, dest, {defineAs: prop}); - } else if (typeof(desc.value) == "object") { - let obj = Cu.createObjectIn(dest, {defineAs: prop}); - injectAPI(desc.value, obj); - } else { - Object.defineProperty(dest, prop, desc); - } - } -} - -/** - * Returns a Promise which resolves when the given document's DOM has - * fully loaded. - * - * @param {Document} doc The document to await the load of. - * @returns {Promise<Document>} - */ -function promiseDocumentReady(doc) { - if (doc.readyState == "interactive" || doc.readyState == "complete") { - return Promise.resolve(doc); - } - - return new Promise(resolve => { - doc.addEventListener("DOMContentLoaded", function onReady(event) { - if (event.target === event.currentTarget) { - doc.removeEventListener("DOMContentLoaded", onReady, true); - resolve(doc); - } - }, true); - }); -} - -/** - * Returns a Promise which resolves when the given document is fully - * loaded. - * - * @param {Document} doc The document to await the load of. - * @returns {Promise<Document>} - */ -function promiseDocumentLoaded(doc) { - if (doc.readyState == "complete") { - return Promise.resolve(doc); - } - - return new Promise(resolve => { - doc.defaultView.addEventListener("load", function onReady(event) { - doc.defaultView.removeEventListener("load", onReady); - resolve(doc); - }); - }); -} - -/** - * Returns a Promise which resolves when the given event is dispatched to the - * given element. - * - * @param {Element} element - * The element on which to listen. - * @param {string} eventName - * The event to listen for. - * @param {boolean} [useCapture = true] - * If true, listen for the even in the capturing rather than - * bubbling phase. - * @param {Event} [test] - * An optional test function which, when called with the - * observer's subject and data, should return true if this is the - * expected event, false otherwise. - * @returns {Promise<Event>} - */ -function promiseEvent(element, eventName, useCapture = true, test = event => true) { - return new Promise(resolve => { - function listener(event) { - if (test(event)) { - element.removeEventListener(eventName, listener, useCapture); - resolve(event); - } - } - element.addEventListener(eventName, listener, useCapture); - }); -} - -/** - * Returns a Promise which resolves the given observer topic has been - * observed. - * - * @param {string} topic - * The topic to observe. - * @param {function(nsISupports, string)} [test] - * An optional test function which, when called with the - * observer's subject and data, should return true if this is the - * expected notification, false otherwise. - * @returns {Promise<object>} - */ -function promiseObserved(topic, test = () => true) { - return new Promise(resolve => { - let observer = (subject, topic, data) => { - if (test(subject, data)) { - Services.obs.removeObserver(observer, topic); - resolve({subject, data}); - } - }; - Services.obs.addObserver(observer, topic, false); - }); -} - -function getMessageManager(target) { - if (target instanceof Ci.nsIFrameLoaderOwner) { - return target.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; - } - return target.QueryInterface(Ci.nsIMessageSender); -} - -function flushJarCache(jarFile) { - Services.obs.notifyObservers(jarFile, "flush-cache-entry", null); -} - -const PlatformInfo = Object.freeze({ - os: (function() { - let os = AppConstants.platform; - if (os == "macosx") { - os = "mac"; - } - return os; - })(), - arch: (function() { - let abi = Services.appinfo.XPCOMABI; - let [arch] = abi.split("-"); - if (arch == "x86") { - arch = "x86-32"; - } else if (arch == "x86_64") { - arch = "x86-64"; - } - return arch; - })(), -}); - -function detectLanguage(text) { - return LanguageDetector.detectLanguage(text).then(result => ({ - isReliable: result.confident, - languages: result.languages.map(lang => { - return { - language: lang.languageCode, - percentage: lang.percent, - }; - }), - })); -} - -/** - * Convert any of several different representations of a date/time to a Date object. - * Accepts several formats: - * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as - * either a number or a string. - * - * @param {Date|string|number} date - * The date to convert. - * @returns {Date} - * A Date object - */ -function normalizeTime(date) { - // Of all the formats we accept the "number of milliseconds since the epoch as a string" - // is an outlier, everything else can just be passed directly to the Date constructor. - return new Date((typeof date == "string" && /^\d+$/.test(date)) - ? parseInt(date, 10) : date); -} - -const stylesheetMap = new DefaultMap(url => { - let uri = NetUtil.newURI(url); - return styleSheetService.preloadSheet(uri, styleSheetService.AGENT_SHEET); -}); - -/** - * Defines a lazy getter for the given property on the given object. The - * first time the property is accessed, the return value of the getter - * is defined on the current `this` object with the given property name. - * Importantly, this means that a lazy getter defined on an object - * prototype will be invoked separately for each object instance that - * it's accessed on. - * - * @param {object} object - * The prototype object on which to define the getter. - * @param {string|Symbol} prop - * The property name for which to define the getter. - * @param {function} getter - * The function to call in order to generate the final property - * value. - */ -function defineLazyGetter(object, prop, getter) { - let redefine = (obj, value) => { - Object.defineProperty(obj, prop, { - enumerable: true, - configurable: true, - writable: true, - value, - }); - return value; - }; - - Object.defineProperty(object, prop, { - enumerable: true, - configurable: true, - - get() { - return redefine(this, getter.call(this)); - }, - - set(value) { - redefine(this, value); - }, - }); -} - -function findPathInObject(obj, path, printErrors = true) { - let parent; - for (let elt of path.split(".")) { - if (!obj || !(elt in obj)) { - if (printErrors) { - let appname = Services.appinfo.name; - Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by ${appname}).`); - } - return null; - } - - parent = obj; - obj = obj[elt]; - } - - if (typeof obj === "function") { - return obj.bind(parent); - } - return obj; -} - -/** - * Acts as a proxy for a message manager or message manager owner, and - * tracks docShell swaps so that messages are always sent to the same - * receiver, even if it is moved to a different <browser>. - * - * @param {nsIMessageSender|Element} target - * The target message manager on which to send messages, or the - * <browser> element which owns it. - */ -class MessageManagerProxy { - constructor(target) { - this.listeners = new DefaultMap(() => new Map()); - - if (target instanceof Ci.nsIMessageSender) { - Object.defineProperty(this, "messageManager", { - value: target, - configurable: true, - writable: true, - }); - } else { - this.addListeners(target); - } - } - - /** - * Disposes of the proxy object, removes event listeners, and drops - * all references to the underlying message manager. - * - * Must be called before the last reference to the proxy is dropped, - * unless the underlying message manager or <browser> is also being - * destroyed. - */ - dispose() { - if (this.eventTarget) { - this.removeListeners(this.eventTarget); - this.eventTarget = null; - } else { - this.messageManager = null; - } - } - - /** - * Returns true if the given target is the same as, or owns, the given - * message manager. - * - * @param {nsIMessageSender|MessageManagerProxy|Element} target - * The message manager, MessageManagerProxy, or <browser> - * element agaisnt which to match. - * @param {nsIMessageSender} messageManager - * The message manager against which to match `target`. - * - * @returns {boolean} - * True if `messageManager` is the same object as `target`, or - * `target` is a MessageManagerProxy or <browser> element that - * is tied to it. - */ - static matches(target, messageManager) { - return target === messageManager || target.messageManager === messageManager; - } - - /** - * @property {nsIMessageSender|null} messageManager - * The message manager that is currently being proxied. This - * may change during the life of the proxy object, so should - * not be stored elsewhere. - */ - get messageManager() { - return this.eventTarget && this.eventTarget.messageManager; - } - - /** - * Sends a message on the proxied message manager. - * - * @param {array} args - * Arguments to be passed verbatim to the underlying - * sendAsyncMessage method. - * @returns {undefined} - */ - sendAsyncMessage(...args) { - if (this.messageManager) { - return this.messageManager.sendAsyncMessage(...args); - } - /* globals uneval */ - Cu.reportError(`Cannot send message: Other side disconnected: ${uneval(args)}`); - } - - /** - * Adds a message listener to the current message manager, and - * transfers it to the new message manager after a docShell swap. - * - * @param {string} message - * The name of the message to listen for. - * @param {nsIMessageListener} listener - * The listener to add. - * @param {boolean} [listenWhenClosed = false] - * If true, the listener will receive messages which were sent - * after the remote side of the listener began closing. - */ - addMessageListener(message, listener, listenWhenClosed = false) { - this.messageManager.addMessageListener(message, listener, listenWhenClosed); - this.listeners.get(message).set(listener, listenWhenClosed); - } - - /** - * Adds a message listener from the current message manager. - * - * @param {string} message - * The name of the message to stop listening for. - * @param {nsIMessageListener} listener - * The listener to remove. - */ - removeMessageListener(message, listener) { - this.messageManager.removeMessageListener(message, listener); - - let listeners = this.listeners.get(message); - listeners.delete(listener); - if (!listeners.size) { - this.listeners.delete(message); - } - } - - /** - * @private - * Iterates over all of the currently registered message listeners. - */ - * iterListeners() { - for (let [message, listeners] of this.listeners) { - for (let [listener, listenWhenClosed] of listeners) { - yield {message, listener, listenWhenClosed}; - } - } - } - - /** - * @private - * Adds docShell swap listeners to the message manager owner. - * - * @param {Element} target - * The target element. - */ - addListeners(target) { - target.addEventListener("SwapDocShells", this); - - for (let {message, listener, listenWhenClosed} of this.iterListeners()) { - target.addMessageListener(message, listener, listenWhenClosed); - } - - this.eventTarget = target; - } - - /** - * @private - * Removes docShell swap listeners to the message manager owner. - * - * @param {Element} target - * The target element. - */ - removeListeners(target) { - target.removeEventListener("SwapDocShells", this); - - for (let {message, listener} of this.iterListeners()) { - target.removeMessageListener(message, listener); - } - } - - handleEvent(event) { - if (event.type == "SwapDocShells") { - this.removeListeners(this.eventTarget); - this.addListeners(event.detail); - } - } -} - -this.ExtensionUtils = { - defineLazyGetter, - detectLanguage, - extend, - findPathInObject, - flushJarCache, - getConsole, - getInnerWindowID, - getMessageManager, - getUniqueId, - ignoreEvent, - injectAPI, - instanceOf, - normalizeTime, - promiseDocumentLoaded, - promiseDocumentReady, - promiseEvent, - promiseObserved, - runSafe, - runSafeSync, - runSafeSyncWithoutClone, - runSafeWithoutClone, - stylesheetMap, - DefaultMap, - DefaultWeakMap, - EventEmitter, - EventManager, - ExtensionError, - IconDetails, - LocaleData, - MessageManagerProxy, - PlatformInfo, - SingletonEventManager, - SpreadArgs, -}; |