summaryrefslogtreecommitdiffstats
path: root/toolkit/components/webextensions/ExtensionUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/webextensions/ExtensionUtils.jsm')
-rw-r--r--toolkit/components/webextensions/ExtensionUtils.jsm1215
1 files changed, 0 insertions, 1215 deletions
diff --git a/toolkit/components/webextensions/ExtensionUtils.jsm b/toolkit/components/webextensions/ExtensionUtils.jsm
deleted file mode 100644
index e7f768c07..000000000
--- a/toolkit/components/webextensions/ExtensionUtils.jsm
+++ /dev/null
@@ -1,1215 +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) {
- Cu.reportError(`WebExtension API ${path} not found (it may be unimplemented by Firefox).`);
- }
- 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,
-};