summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/parent
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--toolkit/components/extensions/parent/.eslintrc.js31
-rw-r--r--toolkit/components/extensions/parent/ext-activityLog.js44
-rw-r--r--toolkit/components/extensions/parent/ext-alarms.js150
-rw-r--r--toolkit/components/extensions/parent/ext-backgroundPage.js231
-rw-r--r--toolkit/components/extensions/parent/ext-browserSettings.js463
-rw-r--r--toolkit/components/extensions/parent/ext-browsingData.js416
-rw-r--r--toolkit/components/extensions/parent/ext-captivePortal.js136
-rw-r--r--toolkit/components/extensions/parent/ext-clipboard.js89
-rw-r--r--toolkit/components/extensions/parent/ext-contentScripts.js202
-rw-r--r--toolkit/components/extensions/parent/ext-contextualIdentities.js338
-rw-r--r--toolkit/components/extensions/parent/ext-cookies.js613
-rw-r--r--toolkit/components/extensions/parent/ext-dns.js90
-rw-r--r--toolkit/components/extensions/parent/ext-downloads.js1264
-rw-r--r--toolkit/components/extensions/parent/ext-extension.js25
-rw-r--r--toolkit/components/extensions/parent/ext-geckoProfiler.js227
-rw-r--r--toolkit/components/extensions/parent/ext-i18n.js47
-rw-r--r--toolkit/components/extensions/parent/ext-identity.js158
-rw-r--r--toolkit/components/extensions/parent/ext-idle.js97
-rw-r--r--toolkit/components/extensions/parent/ext-management.js381
-rw-r--r--toolkit/components/extensions/parent/ext-networkStatus.js89
-rw-r--r--toolkit/components/extensions/parent/ext-notifications.js190
-rw-r--r--toolkit/components/extensions/parent/ext-permissions.js173
-rw-r--r--toolkit/components/extensions/parent/ext-privacy.js496
-rw-r--r--toolkit/components/extensions/parent/ext-protocolHandlers.js100
-rw-r--r--toolkit/components/extensions/parent/ext-proxy.js335
-rw-r--r--toolkit/components/extensions/parent/ext-runtime.js178
-rw-r--r--toolkit/components/extensions/parent/ext-storage.js228
-rw-r--r--toolkit/components/extensions/parent/ext-tabs-base.js2332
-rw-r--r--toolkit/components/extensions/parent/ext-telemetry.js211
-rw-r--r--toolkit/components/extensions/parent/ext-theme.js507
-rw-r--r--toolkit/components/extensions/parent/ext-toolkit.js100
-rw-r--r--toolkit/components/extensions/parent/ext-userScripts.js148
-rw-r--r--toolkit/components/extensions/parent/ext-webNavigation.js294
-rw-r--r--toolkit/components/extensions/parent/ext-webRequest.js162
34 files changed, 10545 insertions, 0 deletions
diff --git a/toolkit/components/extensions/parent/.eslintrc.js b/toolkit/components/extensions/parent/.eslintrc.js
new file mode 100644
index 0000000000..356dda7c70
--- /dev/null
+++ b/toolkit/components/extensions/parent/.eslintrc.js
@@ -0,0 +1,31 @@
+/* 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";
+
+module.exports = {
+ globals: {
+ CONTAINER_STORE: true,
+ DEFAULT_STORE: true,
+ EventEmitter: true,
+ EventManager: true,
+ InputEventManager: true,
+ PRIVATE_STORE: true,
+ TabBase: true,
+ TabManagerBase: true,
+ TabTrackerBase: true,
+ WindowBase: true,
+ WindowManagerBase: true,
+ WindowTrackerBase: true,
+ getUserContextIdForCookieStoreId: true,
+ getContainerForCookieStoreId: true,
+ getCookieStoreIdForContainer: true,
+ getCookieStoreIdForOriginAttributes: true,
+ getCookieStoreIdForTab: true,
+ isContainerCookieStoreId: true,
+ isDefaultCookieStoreId: true,
+ isPrivateCookieStoreId: true,
+ isValidCookieStoreId: true,
+ },
+};
diff --git a/toolkit/components/extensions/parent/ext-activityLog.js b/toolkit/components/extensions/parent/ext-activityLog.js
new file mode 100644
index 0000000000..e86113e1b0
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-activityLog.js
@@ -0,0 +1,44 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionCommon",
+ "resource://gre/modules/ExtensionCommon.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionActivityLog",
+ "resource://gre/modules/ExtensionActivityLog.jsm"
+);
+
+this.activityLog = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ activityLog: {
+ onExtensionActivity: new ExtensionCommon.EventManager({
+ context,
+ name: "activityLog.onExtensionActivity",
+ register: (fire, id) => {
+ // A logger cannot log itself.
+ if (id === context.extension.id) {
+ throw new ExtensionUtils.ExtensionError(
+ "Extension cannot monitor itself."
+ );
+ }
+ function handler(details) {
+ fire.async(details);
+ }
+
+ ExtensionActivityLog.addListener(id, handler);
+ return () => {
+ ExtensionActivityLog.removeListener(id, handler);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-alarms.js b/toolkit/components/extensions/parent/ext-alarms.js
new file mode 100644
index 0000000000..c5b1ea1c83
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-alarms.js
@@ -0,0 +1,150 @@
+/* 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";
+
+// The ext-* files are imported into the same scopes.
+/* import-globals-from ext-toolkit.js */
+
+// Manages an alarm created by the extension (alarms API).
+function Alarm(api, name, alarmInfo) {
+ this.api = api;
+ this.name = name;
+ this.when = alarmInfo.when;
+ this.delayInMinutes = alarmInfo.delayInMinutes;
+ this.periodInMinutes = alarmInfo.periodInMinutes;
+ this.canceled = false;
+
+ let delay, scheduledTime;
+ if (this.when) {
+ scheduledTime = this.when;
+ delay = this.when - Date.now();
+ } else {
+ if (!this.delayInMinutes) {
+ this.delayInMinutes = this.periodInMinutes;
+ }
+ delay = this.delayInMinutes * 60 * 1000;
+ scheduledTime = Date.now() + delay;
+ }
+
+ this.scheduledTime = scheduledTime;
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ delay = delay > 0 ? delay : 0;
+ timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ this.timer = timer;
+}
+
+Alarm.prototype = {
+ clear() {
+ this.timer.cancel();
+ this.api.alarms.delete(this.name);
+ this.canceled = true;
+ },
+
+ observe(subject, topic, data) {
+ if (this.canceled) {
+ return;
+ }
+
+ for (let callback of this.api.callbacks) {
+ callback(this);
+ }
+
+ if (!this.periodInMinutes) {
+ this.clear();
+ return;
+ }
+
+ let delay = this.periodInMinutes * 60 * 1000;
+ this.scheduledTime = Date.now() + delay;
+ this.timer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ get data() {
+ return {
+ name: this.name,
+ scheduledTime: this.scheduledTime,
+ periodInMinutes: this.periodInMinutes,
+ };
+ },
+};
+
+this.alarms = class extends ExtensionAPI {
+ constructor(extension) {
+ super(extension);
+
+ this.alarms = new Map();
+ this.callbacks = new Set();
+ }
+
+ onShutdown() {
+ for (let alarm of this.alarms.values()) {
+ alarm.clear();
+ }
+ }
+
+ getAPI(context) {
+ const self = this;
+
+ return {
+ alarms: {
+ create: function(name, alarmInfo) {
+ name = name || "";
+ if (self.alarms.has(name)) {
+ self.alarms.get(name).clear();
+ }
+ let alarm = new Alarm(self, name, alarmInfo);
+ self.alarms.set(alarm.name, alarm);
+ },
+
+ get: function(name) {
+ name = name || "";
+ if (self.alarms.has(name)) {
+ return Promise.resolve(self.alarms.get(name).data);
+ }
+ return Promise.resolve();
+ },
+
+ getAll: function() {
+ let result = Array.from(self.alarms.values(), alarm => alarm.data);
+ return Promise.resolve(result);
+ },
+
+ clear: function(name) {
+ name = name || "";
+ if (self.alarms.has(name)) {
+ self.alarms.get(name).clear();
+ return Promise.resolve(true);
+ }
+ return Promise.resolve(false);
+ },
+
+ clearAll: function() {
+ let cleared = false;
+ for (let alarm of self.alarms.values()) {
+ alarm.clear();
+ cleared = true;
+ }
+ return Promise.resolve(cleared);
+ },
+
+ onAlarm: new EventManager({
+ context,
+ name: "alarms.onAlarm",
+ register: fire => {
+ let callback = alarm => {
+ fire.sync(alarm.data);
+ };
+
+ self.callbacks.add(callback);
+ return () => {
+ self.callbacks.delete(callback);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-backgroundPage.js b/toolkit/components/extensions/parent/ext-backgroundPage.js
new file mode 100644
index 0000000000..9de2ad669c
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-backgroundPage.js
@@ -0,0 +1,231 @@
+/* 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";
+
+var { ExtensionParent } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionParent.jsm"
+);
+var { HiddenExtensionPage, promiseExtensionViewLoaded } = ExtensionParent;
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionTelemetry",
+ "resource://gre/modules/ExtensionTelemetry.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "DELAYED_STARTUP",
+ "extensions.webextensions.background-delayed-startup"
+);
+
+XPCOMUtils.defineLazyGetter(this, "serviceWorkerManager", () => {
+ return Cc["@mozilla.org/serviceworkers/manager;1"].getService(
+ Ci.nsIServiceWorkerManager
+ );
+});
+
+// Responsible for the background_page section of the manifest.
+class BackgroundPage extends HiddenExtensionPage {
+ constructor(extension, options) {
+ super(extension, "background");
+
+ this.page = options.page || null;
+ this.isGenerated = !!options.scripts;
+
+ if (this.page) {
+ this.url = this.extension.baseURI.resolve(this.page);
+ } else if (this.isGenerated) {
+ this.url = this.extension.baseURI.resolve(
+ "_generated_background_page.html"
+ );
+ }
+ }
+
+ async build() {
+ const { extension } = this;
+
+ ExtensionTelemetry.backgroundPageLoad.stopwatchStart(extension, this);
+
+ let context;
+ try {
+ await this.createBrowserElement();
+ if (!this.browser) {
+ throw new Error(
+ "Extension shut down before the background page was created"
+ );
+ }
+ extension._backgroundPageFrameLoader = this.browser.frameLoader;
+
+ extensions.emit("extension-browser-inserted", this.browser);
+
+ let contextPromise = promiseExtensionViewLoaded(this.browser);
+ this.browser.loadURI(this.url, {
+ triggeringPrincipal: extension.principal,
+ });
+
+ context = await contextPromise;
+ } catch (e) {
+ // Extension was down before the background page has loaded.
+ Cu.reportError(e);
+ ExtensionTelemetry.backgroundPageLoad.stopwatchCancel(extension, this);
+ if (extension.persistentListeners) {
+ EventManager.clearPrimedListeners(this.extension, false);
+ }
+ extension.emit("background-page-aborted");
+ return;
+ }
+
+ ExtensionTelemetry.backgroundPageLoad.stopwatchFinish(extension, this);
+
+ if (context) {
+ // Wait until all event listeners registered by the script so far
+ // to be handled.
+ await Promise.all(context.listenerPromises);
+ context.listenerPromises = null;
+ }
+
+ if (extension.persistentListeners) {
+ // |this.extension| may be null if the extension was shut down.
+ // In that case, we still want to clear the primed listeners,
+ // but not update the persistent listeners in the startupData.
+ EventManager.clearPrimedListeners(extension, !!this.extension);
+ }
+
+ extension.emit("background-page-started");
+ }
+
+ shutdown() {
+ this.extension._backgroundPageFrameLoader = null;
+ super.shutdown();
+ }
+}
+
+// Responsible for the background.service_worker section of the manifest.
+class BackgroundWorker {
+ constructor(extension, options) {
+ this.registrationInfo = null;
+ this.extension = extension;
+ this.workerScript = options.service_worker;
+
+ if (!this.workerScript) {
+ throw new Error("Missing mandatory background.service_worker property");
+ }
+ }
+
+ async build() {
+ const regInfo = await serviceWorkerManager.registerForAddonPrincipal(
+ this.extension.principal
+ );
+ this.registrationInfo = regInfo.QueryInterface(
+ Ci.nsIServiceWorkerRegistrationInfo
+ );
+ }
+
+ shutdown() {
+ if (this.registrationInfo) {
+ this.registrationInfo.forceShutdown();
+ this.registrationInfo = null;
+ }
+ }
+}
+
+this.backgroundPage = class extends ExtensionAPI {
+ async build() {
+ if (this.bgInstance) {
+ return;
+ }
+
+ let { extension } = this;
+ let { manifest } = extension;
+
+ let BackgroundClass = manifest.background.service_worker
+ ? BackgroundWorker
+ : BackgroundPage;
+
+ this.bgInstance = new BackgroundClass(extension, manifest.background);
+ return this.bgInstance.build();
+ }
+
+ onManifestEntry(entryName) {
+ let { extension } = this;
+
+ this.bgInstance = null;
+
+ // When in PPB background pages all run in a private context. This check
+ // simply avoids an extraneous error in the console since the BaseContext
+ // will throw.
+ if (
+ PrivateBrowsingUtils.permanentPrivateBrowsing &&
+ !extension.privateBrowsingAllowed
+ ) {
+ return;
+ }
+
+ // Used by runtime messaging to wait for background page listeners.
+ let bgStartupPromise = new Promise(resolve => {
+ let done = () => {
+ extension.off("background-page-started", done);
+ extension.off("background-page-aborted", done);
+ extension.off("shutdown", done);
+ resolve();
+ };
+ extension.on("background-page-started", done);
+ extension.on("background-page-aborted", done);
+ extension.on("shutdown", done);
+ });
+
+ extension.wakeupBackground = () => {
+ extension.emit("background-page-event");
+ extension.wakeupBackground = () => bgStartupPromise;
+ return bgStartupPromise;
+ };
+
+ if (extension.startupReason !== "APP_STARTUP" || !DELAYED_STARTUP) {
+ return this.build();
+ }
+
+ EventManager.primeListeners(extension);
+
+ extension.once("start-background-page", async () => {
+ if (!this.extension) {
+ // Extension was shut down. Don't build the background page.
+ // Primed listeners have been cleared in onShutdown.
+ return;
+ }
+ await this.build();
+ });
+
+ // There are two ways to start the background page:
+ // 1. If a primed event fires, then start the background page as
+ // soon as we have painted a browser window. Note that we have
+ // to touch browserPaintedPromise here to initialize the listener
+ // or else we can miss it if the event occurs after the first
+ // window is painted but before #2
+ // 2. After all windows have been restored.
+ extension.once("background-page-event", async () => {
+ await ExtensionParent.browserPaintedPromise;
+ extension.emit("start-background-page");
+ });
+
+ ExtensionParent.browserStartupPromise.then(() => {
+ extension.emit("start-background-page");
+ });
+ }
+
+ onShutdown() {
+ if (this.bgInstance) {
+ this.bgInstance.shutdown();
+ this.bgInstance = null;
+ } else {
+ EventManager.clearPrimedListeners(this.extension, false);
+ }
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-browserSettings.js b/toolkit/components/extensions/parent/ext-browserSettings.js
new file mode 100644
index 0000000000..946f08a93d
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-browserSettings.js
@@ -0,0 +1,463 @@
+/* -*- 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AboutNewTab",
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+var { ExtensionPreferencesManager } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionPreferencesManager.jsm"
+);
+
+var { ExtensionError } = ExtensionUtils;
+var { getSettingsAPI } = ExtensionPreferencesManager;
+
+const HOMEPAGE_OVERRIDE_SETTING = "homepage_override";
+const HOMEPAGE_URL_PREF = "browser.startup.homepage";
+const URL_STORE_TYPE = "url_overrides";
+const NEW_TAB_OVERRIDE_SETTING = "newTabURL";
+
+const PERM_DENY_ACTION = Services.perms.DENY_ACTION;
+
+// Add settings objects for supported APIs to the preferences manager.
+ExtensionPreferencesManager.addSetting("allowPopupsForUserEvents", {
+ permission: "browserSettings",
+ prefNames: ["dom.popup_allowed_events"],
+
+ setCallback(value) {
+ let returnObj = {};
+ // If the value is true, then reset the pref, otherwise set it to "".
+ returnObj[this.prefNames[0]] = value ? undefined : "";
+ return returnObj;
+ },
+});
+
+ExtensionPreferencesManager.addSetting("cacheEnabled", {
+ permission: "browserSettings",
+ prefNames: ["browser.cache.disk.enable", "browser.cache.memory.enable"],
+
+ setCallback(value) {
+ let returnObj = {};
+ for (let pref of this.prefNames) {
+ returnObj[pref] = value;
+ }
+ return returnObj;
+ },
+});
+
+ExtensionPreferencesManager.addSetting("closeTabsByDoubleClick", {
+ permission: "browserSettings",
+ prefNames: ["browser.tabs.closeTabByDblclick"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("contextMenuShowEvent", {
+ permission: "browserSettings",
+ prefNames: ["ui.context_menus.after_mouseup"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value === "mouseup" };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("ftpProtocolEnabled", {
+ permission: "browserSettings",
+ prefNames: ["network.ftp.enabled"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("imageAnimationBehavior", {
+ permission: "browserSettings",
+ prefNames: ["image.animation_mode"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("newTabPosition", {
+ permission: "browserSettings",
+ prefNames: [
+ "browser.tabs.insertRelatedAfterCurrent",
+ "browser.tabs.insertAfterCurrent",
+ ],
+
+ setCallback(value) {
+ return {
+ "browser.tabs.insertAfterCurrent": value === "afterCurrent",
+ "browser.tabs.insertRelatedAfterCurrent": value === "relatedAfterCurrent",
+ };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("openBookmarksInNewTabs", {
+ permission: "browserSettings",
+ prefNames: ["browser.tabs.loadBookmarksInTabs"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("openSearchResultsInNewTabs", {
+ permission: "browserSettings",
+ prefNames: ["browser.search.openintab"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("openUrlbarResultsInNewTabs", {
+ permission: "browserSettings",
+ prefNames: ["browser.urlbar.openintab"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("webNotificationsDisabled", {
+ permission: "browserSettings",
+ prefNames: ["permissions.default.desktop-notification"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value ? PERM_DENY_ACTION : undefined };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("overrideDocumentColors", {
+ permission: "browserSettings",
+ prefNames: ["browser.display.document_color_use"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("useDocumentFonts", {
+ permission: "browserSettings",
+ prefNames: ["browser.display.use_document_fonts"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("zoomFullPage", {
+ permission: "browserSettings",
+ prefNames: ["browser.zoom.full"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("zoomSiteSpecific", {
+ permission: "browserSettings",
+ prefNames: ["browser.zoom.siteSpecific"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+this.browserSettings = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+
+ return {
+ browserSettings: {
+ allowPopupsForUserEvents: getSettingsAPI({
+ context,
+ name: "allowPopupsForUserEvents",
+ callback() {
+ return Services.prefs.getCharPref("dom.popup_allowed_events") != "";
+ },
+ }),
+ cacheEnabled: getSettingsAPI({
+ context,
+ name: "cacheEnabled",
+ callback() {
+ return (
+ Services.prefs.getBoolPref("browser.cache.disk.enable") &&
+ Services.prefs.getBoolPref("browser.cache.memory.enable")
+ );
+ },
+ }),
+ closeTabsByDoubleClick: getSettingsAPI({
+ context,
+ name: "closeTabsByDoubleClick",
+ callback() {
+ return Services.prefs.getBoolPref(
+ "browser.tabs.closeTabByDblclick"
+ );
+ },
+ validate() {
+ if (AppConstants.platform == "android") {
+ throw new ExtensionError(
+ `android is not a supported platform for the closeTabsByDoubleClick setting.`
+ );
+ }
+ },
+ }),
+ contextMenuShowEvent: Object.assign(
+ getSettingsAPI({
+ context,
+ name: "contextMenuShowEvent",
+ callback() {
+ if (AppConstants.platform === "win") {
+ return "mouseup";
+ }
+ let prefValue = Services.prefs.getBoolPref(
+ "ui.context_menus.after_mouseup",
+ null
+ );
+ return prefValue ? "mouseup" : "mousedown";
+ },
+ }),
+ {
+ set: details => {
+ if (!["mouseup", "mousedown"].includes(details.value)) {
+ throw new ExtensionError(
+ `${details.value} is not a valid value for contextMenuShowEvent.`
+ );
+ }
+ if (
+ AppConstants.platform === "android" ||
+ (AppConstants.platform === "win" &&
+ details.value === "mousedown")
+ ) {
+ return false;
+ }
+ return ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "contextMenuShowEvent",
+ details.value
+ );
+ },
+ }
+ ),
+ ftpProtocolEnabled: getSettingsAPI({
+ context,
+ name: "ftpProtocolEnabled",
+ callback() {
+ return Services.prefs.getBoolPref("network.ftp.enabled");
+ },
+ }),
+ homepageOverride: getSettingsAPI({
+ context,
+ name: HOMEPAGE_OVERRIDE_SETTING,
+ callback() {
+ return Services.prefs.getStringPref(HOMEPAGE_URL_PREF);
+ },
+ readOnly: true,
+ onChange: new ExtensionCommon.EventManager({
+ context,
+ name: `${HOMEPAGE_URL_PREF}.onChange`,
+ register: fire => {
+ let listener = () => {
+ fire.async({
+ levelOfControl: "not_controllable",
+ value: Services.prefs.getStringPref(HOMEPAGE_URL_PREF),
+ });
+ };
+ Services.prefs.addObserver(HOMEPAGE_URL_PREF, listener);
+ return () => {
+ Services.prefs.removeObserver(HOMEPAGE_URL_PREF, listener);
+ };
+ },
+ }).api(),
+ }),
+ imageAnimationBehavior: getSettingsAPI({
+ context,
+ name: "imageAnimationBehavior",
+ callback() {
+ return Services.prefs.getCharPref("image.animation_mode");
+ },
+ }),
+ newTabPosition: getSettingsAPI({
+ context,
+ name: "newTabPosition",
+ callback() {
+ if (Services.prefs.getBoolPref("browser.tabs.insertAfterCurrent")) {
+ return "afterCurrent";
+ }
+ if (
+ Services.prefs.getBoolPref(
+ "browser.tabs.insertRelatedAfterCurrent"
+ )
+ ) {
+ return "relatedAfterCurrent";
+ }
+ return "atEnd";
+ },
+ }),
+ newTabPageOverride: getSettingsAPI({
+ context,
+ name: NEW_TAB_OVERRIDE_SETTING,
+ callback() {
+ return AboutNewTab.newTabURL;
+ },
+ storeType: URL_STORE_TYPE,
+ readOnly: true,
+ onChange: new ExtensionCommon.EventManager({
+ context,
+ name: `${NEW_TAB_OVERRIDE_SETTING}.onChange`,
+ register: fire => {
+ let listener = (text, id) => {
+ fire.async({
+ levelOfControl: "not_controllable",
+ value: AboutNewTab.newTabURL,
+ });
+ };
+ Services.obs.addObserver(listener, "newtab-url-changed");
+ return () => {
+ Services.obs.removeObserver(listener, "newtab-url-changed");
+ };
+ },
+ }).api(),
+ }),
+ openBookmarksInNewTabs: getSettingsAPI({
+ context,
+ name: "openBookmarksInNewTabs",
+ callback() {
+ return Services.prefs.getBoolPref(
+ "browser.tabs.loadBookmarksInTabs"
+ );
+ },
+ }),
+ openSearchResultsInNewTabs: getSettingsAPI({
+ context,
+ name: "openSearchResultsInNewTabs",
+ callback() {
+ return Services.prefs.getBoolPref("browser.search.openintab");
+ },
+ }),
+ openUrlbarResultsInNewTabs: getSettingsAPI({
+ context,
+ name: "openUrlbarResultsInNewTabs",
+ callback() {
+ return Services.prefs.getBoolPref("browser.urlbar.openintab");
+ },
+ }),
+ webNotificationsDisabled: getSettingsAPI({
+ context,
+ name: "webNotificationsDisabled",
+ callback() {
+ let prefValue = Services.prefs.getIntPref(
+ "permissions.default.desktop-notification",
+ null
+ );
+ return prefValue === PERM_DENY_ACTION;
+ },
+ }),
+ overrideDocumentColors: Object.assign(
+ getSettingsAPI({
+ context,
+ name: "overrideDocumentColors",
+ callback() {
+ let prefValue = Services.prefs.getIntPref(
+ "browser.display.document_color_use"
+ );
+ if (prefValue === 1) {
+ return "never";
+ } else if (prefValue === 2) {
+ return "always";
+ }
+ return "high-contrast-only";
+ },
+ }),
+ {
+ set: details => {
+ if (
+ !["never", "always", "high-contrast-only"].includes(
+ details.value
+ )
+ ) {
+ throw new ExtensionError(
+ `${details.value} is not a valid value for overrideDocumentColors.`
+ );
+ }
+ let prefValue = 0; // initialize to 0 - auto/high-contrast-only
+ if (details.value === "never") {
+ prefValue = 1;
+ } else if (details.value === "always") {
+ prefValue = 2;
+ }
+ return ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "overrideDocumentColors",
+ prefValue
+ );
+ },
+ }
+ ),
+ useDocumentFonts: Object.assign(
+ getSettingsAPI({
+ context,
+ name: "useDocumentFonts",
+ callback() {
+ return (
+ Services.prefs.getIntPref(
+ "browser.display.use_document_fonts"
+ ) !== 0
+ );
+ },
+ }),
+ {
+ set: details => {
+ if (typeof details.value !== "boolean") {
+ throw new ExtensionError(
+ `${details.value} is not a valid value for useDocumentFonts.`
+ );
+ }
+ return ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "useDocumentFonts",
+ Number(details.value)
+ );
+ },
+ }
+ ),
+ zoomFullPage: getSettingsAPI({
+ context,
+ name: "zoomFullPage",
+ callback() {
+ return Services.prefs.getBoolPref("browser.zoom.full");
+ },
+ }),
+ zoomSiteSpecific: getSettingsAPI({
+ context,
+ name: "zoomSiteSpecific",
+ callback() {
+ return Services.prefs.getBoolPref("browser.zoom.siteSpecific");
+ },
+ }),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-browsingData.js b/toolkit/components/extensions/parent/ext-browsingData.js
new file mode 100644
index 0000000000..9f19ed89be
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-browsingData.js
@@ -0,0 +1,416 @@
+/* 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";
+
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ LoginHelper: "resource://gre/modules/LoginHelper.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ setTimeout: "resource://gre/modules/Timer.jsm",
+ ServiceWorkerCleanUp: "resource://gre/modules/ServiceWorkerCleanUp.jsm",
+ // This helper contains the platform-specific bits of browsingData.
+ BrowsingDataDelegate: "resource:///modules/ExtensionBrowsingData.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "quotaManagerService",
+ "@mozilla.org/dom/quota-manager-service;1",
+ "nsIQuotaManagerService"
+);
+
+/**
+ * A number of iterations after which to yield time back
+ * to the system.
+ */
+const YIELD_PERIOD = 10;
+
+/**
+ * Convert a Date object to a PRTime (microseconds).
+ *
+ * @param {Date} date
+ * the Date object to convert.
+ * @returns {integer} microseconds from the epoch.
+ */
+const toPRTime = date => {
+ if (typeof date != "number" && date.constructor.name != "Date") {
+ throw new Error("Invalid value passed to toPRTime");
+ }
+ return date * 1000;
+};
+
+const makeRange = options => {
+ return options.since == null
+ ? null
+ : [toPRTime(options.since), toPRTime(Date.now())];
+};
+global.makeRange = makeRange;
+
+// General implementation for clearing data using Services.clearData.
+// Currently Sanitizer.items uses this under the hood.
+async function clearData(options, flags) {
+ if (options.hostnames) {
+ await Promise.all(
+ options.hostnames.map(
+ host =>
+ new Promise(resolve => {
+ // Set aIsUserRequest to true. This means when the ClearDataService
+ // "Cleaner" implementation doesn't support clearing by host
+ // it will delete all data instead.
+ // This is appropriate for cases like |cache|, which doesn't
+ // support clearing by a time range.
+ // In future when we use this for other data types, we have to
+ // evaluate if that behavior is still acceptable.
+ Services.clearData.deleteDataFromHost(host, true, flags, resolve);
+ })
+ )
+ );
+ return;
+ }
+
+ if (options.since) {
+ const range = makeRange(options);
+ await new Promise(resolve => {
+ Services.clearData.deleteDataInTimeRange(...range, true, flags, resolve);
+ });
+ return;
+ }
+
+ // Don't return the promise here and above to prevent leaking the resolved
+ // value.
+ await new Promise(resolve => Services.clearData.deleteData(flags, resolve));
+}
+
+const clearCache = options => {
+ return clearData(options, Ci.nsIClearDataService.CLEAR_ALL_CACHES);
+};
+
+const clearCookies = async function(options) {
+ let cookieMgr = Services.cookies;
+ // This code has been borrowed from Sanitizer.jsm.
+ let yieldCounter = 0;
+
+ if (options.since || options.hostnames || options.cookieStoreId) {
+ // Iterate through the cookies and delete any created after our cutoff.
+ let cookies = cookieMgr.cookies;
+ if (
+ !options.cookieStoreId ||
+ isPrivateCookieStoreId(options.cookieStoreId)
+ ) {
+ // By default nsICookieManager.cookies doesn't contain private cookies.
+ const privateCookies = cookieMgr.getCookiesWithOriginAttributes(
+ JSON.stringify({
+ privateBrowsingId: 1,
+ })
+ );
+ cookies = cookies.concat(privateCookies);
+ }
+ for (const cookie of cookies) {
+ if (
+ (!options.since || cookie.creationTime >= toPRTime(options.since)) &&
+ (!options.hostnames ||
+ options.hostnames.includes(cookie.host.replace(/^\./, ""))) &&
+ (!options.cookieStoreId ||
+ getCookieStoreIdForOriginAttributes(cookie.originAttributes) ===
+ options.cookieStoreId)
+ ) {
+ // This cookie was created after our cutoff, clear it.
+ cookieMgr.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ cookie.originAttributes
+ );
+
+ if (++yieldCounter % YIELD_PERIOD == 0) {
+ await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+ }
+ }
+ }
+ } else {
+ // Remove everything.
+ cookieMgr.removeAll();
+ }
+};
+
+// Ideally we could reuse the logic in Sanitizer.jsm or nsIClearDataService,
+// but this API exposes an ability to wipe data at a much finger granularity
+// than those APIs. (See also Bug 1531276)
+async function clearQuotaManager(options, dataType) {
+ // Can not clear localStorage/indexedDB in private browsing mode,
+ // just ignore.
+ if (options.cookieStoreId == PRIVATE_STORE) {
+ return;
+ }
+
+ let promises = [];
+ await new Promise((resolve, reject) => {
+ quotaManagerService.getUsage(request => {
+ if (request.resultCode != Cr.NS_OK) {
+ reject({ message: `Clear ${dataType} failed` });
+ return;
+ }
+
+ for (let item of request.result) {
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
+ item.origin
+ );
+
+ // Consistently to removeIndexedDB and the API documentation for
+ // removeLocalStorage, we should only clear the data stored by
+ // regular websites, on the contrary we shouldn't clear data stored
+ // by browser components (like about:newtab) or other extensions.
+ if (!["http", "https", "file"].includes(principal.scheme)) {
+ continue;
+ }
+
+ let host = principal.hostPort;
+ if (
+ (!options.hostnames || options.hostnames.includes(host)) &&
+ (!options.cookieStoreId ||
+ getCookieStoreIdForOriginAttributes(principal.originAttributes) ===
+ options.cookieStoreId)
+ ) {
+ promises.push(
+ new Promise((resolve, reject) => {
+ let clearRequest;
+ if (dataType === "indexedDB") {
+ clearRequest = quotaManagerService.clearStoragesForPrincipal(
+ principal,
+ null,
+ "idb"
+ );
+ } else {
+ clearRequest = quotaManagerService.clearStoragesForPrincipal(
+ principal,
+ "default",
+ "ls"
+ );
+ }
+
+ clearRequest.callback = () => {
+ if (clearRequest.resultCode == Cr.NS_OK) {
+ resolve();
+ } else {
+ reject({ message: `Clear ${dataType} failed` });
+ }
+ };
+ })
+ );
+ }
+ }
+
+ resolve();
+ });
+ });
+
+ return Promise.all(promises);
+}
+
+const clearIndexedDB = async function(options) {
+ return clearQuotaManager(options, "indexedDB");
+};
+
+const clearLocalStorage = async function(options) {
+ if (options.since) {
+ return Promise.reject({
+ message: "Firefox does not support clearing localStorage with 'since'.",
+ });
+ }
+
+ // The legacy LocalStorage implementation that will eventually be removed
+ // depends on this observer notification. Some other subsystems like
+ // Reporting headers depend on this too.
+ // When NextGenLocalStorage is enabled these notifications are ignored.
+ if (options.hostnames) {
+ for (let hostname of options.hostnames) {
+ Services.obs.notifyObservers(
+ null,
+ "extension:purge-localStorage",
+ hostname
+ );
+ }
+ } else {
+ Services.obs.notifyObservers(null, "extension:purge-localStorage");
+ }
+
+ if (Services.domStorageManager.nextGenLocalStorageEnabled) {
+ return clearQuotaManager(options, "localStorage");
+ }
+};
+
+const clearPasswords = async function(options) {
+ let yieldCounter = 0;
+
+ // Iterate through the logins and delete any updated after our cutoff.
+ for (let login of await LoginHelper.getAllUserFacingLogins()) {
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ if (!options.since || login.timePasswordChanged >= options.since) {
+ Services.logins.removeLogin(login);
+ if (++yieldCounter % YIELD_PERIOD == 0) {
+ await new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long.
+ }
+ }
+ }
+};
+
+const clearServiceWorkers = options => {
+ if (!options.hostnames) {
+ return ServiceWorkerCleanUp.removeAll();
+ }
+
+ return Promise.all(
+ options.hostnames.map(host => {
+ return ServiceWorkerCleanUp.removeFromHost(host);
+ })
+ );
+};
+
+class BrowsingDataImpl {
+ constructor(extension) {
+ this.extension = extension;
+ // Some APIs cannot implement in a platform-independent way and they are
+ // delegated to a platform-specific delegate.
+ this.platformDelegate = new BrowsingDataDelegate(extension);
+ }
+
+ handleRemoval(dataType, options) {
+ // First, let's see if the platform implements this
+ let result = this.platformDelegate.handleRemoval(dataType, options);
+ if (result !== undefined) {
+ return result;
+ }
+
+ // ... if not, run the default behavior.
+ switch (dataType) {
+ case "cache":
+ return clearCache(options);
+ case "cookies":
+ return clearCookies(options);
+ case "indexedDB":
+ return clearIndexedDB(options);
+ case "localStorage":
+ return clearLocalStorage(options);
+ case "passwords":
+ return clearPasswords(options);
+ case "pluginData":
+ this.extension?.logger.warn(
+ "pluginData has been deprecated (along with Flash plugin support)"
+ );
+ return Promise.resolve();
+ case "serviceWorkers":
+ return clearServiceWorkers(options);
+ default:
+ return undefined;
+ }
+ }
+
+ doRemoval(options, dataToRemove) {
+ if (
+ options.originTypes &&
+ (options.originTypes.protectedWeb || options.originTypes.extension)
+ ) {
+ return Promise.reject({
+ message:
+ "Firefox does not support protectedWeb or extension as originTypes.",
+ });
+ }
+
+ if (options.cookieStoreId) {
+ const SUPPORTED_TYPES = ["cookies", "indexedDB"];
+ if (Services.domStorageManager.nextGenLocalStorageEnabled) {
+ // Only the next-gen storage supports removal by cookieStoreId.
+ SUPPORTED_TYPES.push("localStorage");
+ }
+
+ for (let dataType in dataToRemove) {
+ if (dataToRemove[dataType] && !SUPPORTED_TYPES.includes(dataType)) {
+ return Promise.reject({
+ message: `Firefox does not support clearing ${dataType} with 'cookieStoreId'.`,
+ });
+ }
+ }
+
+ if (
+ !isPrivateCookieStoreId(options.cookieStoreId) &&
+ !isDefaultCookieStoreId(options.cookieStoreId) &&
+ !getContainerForCookieStoreId(options.cookieStoreId)
+ ) {
+ return Promise.reject({
+ message: `Invalid cookieStoreId: ${options.cookieStoreId}`,
+ });
+ }
+ }
+
+ let removalPromises = [];
+ let invalidDataTypes = [];
+ for (let dataType in dataToRemove) {
+ if (dataToRemove[dataType]) {
+ let result = this.handleRemoval(dataType, options);
+ if (result === undefined) {
+ invalidDataTypes.push(dataType);
+ } else {
+ removalPromises.push(result);
+ }
+ }
+ }
+ if (invalidDataTypes.length) {
+ this.extension.logger.warn(
+ `Firefox does not support dataTypes: ${invalidDataTypes.toString()}.`
+ );
+ }
+ return Promise.all(removalPromises);
+ }
+
+ settings() {
+ return this.platformDelegate.settings();
+ }
+}
+
+this.browsingData = class extends ExtensionAPI {
+ getAPI(context) {
+ const impl = new BrowsingDataImpl(context.extension);
+ return {
+ browsingData: {
+ settings() {
+ return impl.settings();
+ },
+ remove(options, dataToRemove) {
+ return impl.doRemoval(options, dataToRemove);
+ },
+ removeCache(options) {
+ return impl.doRemoval(options, { cache: true });
+ },
+ removeCookies(options) {
+ return impl.doRemoval(options, { cookies: true });
+ },
+ removeDownloads(options) {
+ return impl.doRemoval(options, { downloads: true });
+ },
+ removeFormData(options) {
+ return impl.doRemoval(options, { formData: true });
+ },
+ removeHistory(options) {
+ return impl.doRemoval(options, { history: true });
+ },
+ removeIndexedDB(options) {
+ return impl.doRemoval(options, { indexedDB: true });
+ },
+ removeLocalStorage(options) {
+ return impl.doRemoval(options, { localStorage: true });
+ },
+ removePasswords(options) {
+ return impl.doRemoval(options, { passwords: true });
+ },
+ removePluginData(options) {
+ return impl.doRemoval(options, { pluginData: true });
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-captivePortal.js b/toolkit/components/extensions/parent/ext-captivePortal.js
new file mode 100644
index 0000000000..baf0e04e39
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-captivePortal.js
@@ -0,0 +1,136 @@
+/* -*- 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";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gCPS",
+ "@mozilla.org/network/captive-portal-service;1",
+ "nsICaptivePortalService"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "gCaptivePortalEnabled",
+ "network.captive-portal-service.enabled",
+ false
+);
+
+var { ExtensionPreferencesManager } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionPreferencesManager.jsm"
+);
+
+var { getSettingsAPI } = ExtensionPreferencesManager;
+
+const CAPTIVE_URL_PREF = "captivedetect.canonicalURL";
+
+function nameForCPSState(state) {
+ switch (state) {
+ case gCPS.UNKNOWN:
+ return "unknown";
+ case gCPS.NOT_CAPTIVE:
+ return "not_captive";
+ case gCPS.UNLOCKED_PORTAL:
+ return "unlocked_portal";
+ case gCPS.LOCKED_PORTAL:
+ return "locked_portal";
+ default:
+ return "unknown";
+ }
+}
+
+var { ExtensionError } = ExtensionUtils;
+
+this.captivePortal = class extends ExtensionAPI {
+ getAPI(context) {
+ function checkEnabled() {
+ if (!gCaptivePortalEnabled) {
+ throw new ExtensionError("Captive Portal detection is not enabled");
+ }
+ }
+
+ return {
+ captivePortal: {
+ getState() {
+ checkEnabled();
+ return nameForCPSState(gCPS.state);
+ },
+ getLastChecked() {
+ checkEnabled();
+ return gCPS.lastChecked;
+ },
+ onStateChanged: new EventManager({
+ context,
+ name: "captivePortal.onStateChanged",
+ register: fire => {
+ checkEnabled();
+
+ let observer = (subject, topic) => {
+ fire.async({ state: nameForCPSState(gCPS.state) });
+ };
+
+ Services.obs.addObserver(
+ observer,
+ "ipc:network:captive-portal-set-state"
+ );
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ "ipc:network:captive-portal-set-state"
+ );
+ };
+ },
+ }).api(),
+ onConnectivityAvailable: new EventManager({
+ context,
+ name: "captivePortal.onConnectivityAvailable",
+ register: fire => {
+ checkEnabled();
+
+ let observer = (subject, topic, data) => {
+ fire.async({ status: data });
+ };
+
+ Services.obs.addObserver(
+ observer,
+ "network:captive-portal-connectivity"
+ );
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ "network:captive-portal-connectivity"
+ );
+ };
+ },
+ }).api(),
+ canonicalURL: getSettingsAPI({
+ context,
+ name: "captiveURL",
+ callback() {
+ return Services.prefs.getStringPref(CAPTIVE_URL_PREF);
+ },
+ readOnly: true,
+ onChange: new ExtensionCommon.EventManager({
+ context,
+ name: "captiveURL.onChange",
+ register: fire => {
+ let listener = (text, id) => {
+ fire.async({
+ levelOfControl: "not_controllable",
+ value: Services.prefs.getStringPref(CAPTIVE_URL_PREF),
+ });
+ };
+ Services.prefs.addObserver(CAPTIVE_URL_PREF, listener);
+ return () => {
+ Services.prefs.removeObserver(CAPTIVE_URL_PREF, listener);
+ };
+ },
+ }).api(),
+ }),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-clipboard.js b/toolkit/components/extensions/parent/ext-clipboard.js
new file mode 100644
index 0000000000..048b704b4f
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-clipboard.js
@@ -0,0 +1,89 @@
+/* -*- 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";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "imgTools",
+ "@mozilla.org/image/tools;1",
+ "imgITools"
+);
+
+const Transferable = Components.Constructor(
+ "@mozilla.org/widget/transferable;1",
+ "nsITransferable"
+);
+
+this.clipboard = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ clipboard: {
+ async setImageData(imageData, imageType) {
+ if (AppConstants.platform == "android") {
+ return Promise.reject({
+ message:
+ "Writing images to the clipboard is not supported on Android",
+ });
+ }
+ let img;
+ try {
+ img = imgTools.decodeImageFromArrayBuffer(
+ imageData,
+ `image/${imageType}`
+ );
+ } catch (e) {
+ return Promise.reject({
+ message: `Data is not a valid ${imageType} image`,
+ });
+ }
+
+ // Other applications can only access the copied image once the data
+ // is exported via the platform-specific clipboard APIs:
+ // nsClipboard::SelectionGetEvent (widget/gtk/nsClipboard.cpp)
+ // nsClipboard::PasteDictFromTransferable (widget/cocoa/nsClipboard.mm)
+ // nsDataObj::GetDib (widget/windows/nsDataObj.cpp)
+ //
+ // The common protocol for exporting a nsITransferable as an image is:
+ // - Use nsITransferable::GetTransferData to fetch the stored data.
+ // - QI imgIContainer on the pointer.
+ // - Convert the image to the native clipboard format.
+ //
+ // Below we create a nsITransferable in the above format.
+ let transferable = new Transferable();
+ transferable.init(null);
+ const kNativeImageMime = "application/x-moz-nativeimage";
+ transferable.addDataFlavor(kNativeImageMime);
+
+ // Internal consumers expect the image data to be stored as a
+ // nsIInputStream. On Linux and Windows, pasted data is directly
+ // retrieved from the system's native clipboard, and made available
+ // as a nsIInputStream.
+ //
+ // On macOS, nsClipboard::GetNativeClipboardData (nsClipboard.mm) uses
+ // a cached copy of nsITransferable if available, e.g. when the copy
+ // was initiated by the same browser instance. To make sure that a
+ // nsIInputStream is returned instead of the cached imgIContainer,
+ // the image is exported as as `kNativeImageMime`. Data associated
+ // with this type is converted to a platform-specific image format
+ // when written to the clipboard. The type is not used when images
+ // are read from the clipboard (on all platforms, not just macOS).
+ // This forces nsClipboard::GetNativeClipboardData to fall back to
+ // the native clipboard, and return the image as a nsITransferable.
+
+ // The length should not be zero. (Bug 1493292)
+ transferable.setTransferData(kNativeImageMime, img, 1);
+
+ Services.clipboard.setData(
+ transferable,
+ null,
+ Services.clipboard.kGlobalClipboard
+ );
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-contentScripts.js b/toolkit/components/extensions/parent/ext-contentScripts.js
new file mode 100644
index 0000000000..39da7ef366
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-contentScripts.js
@@ -0,0 +1,202 @@
+/* -*- 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";
+
+/* exported registerContentScript, unregisterContentScript */
+/* global registerContentScript, unregisterContentScript */
+
+var { ExtensionUtils } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionUtils.jsm"
+);
+
+var { ExtensionError, getUniqueId } = ExtensionUtils;
+
+/**
+ * Represents (in the main browser process) a content script registered
+ * programmatically (instead of being included in the addon manifest).
+ *
+ * @param {ProxyContextParent} context
+ * The parent proxy context related to the extension context which
+ * has registered the content script.
+ * @param {RegisteredContentScriptOptions} details
+ * The options object related to the registered content script
+ * (which has the properties described in the content_scripts.json
+ * JSON API schema file).
+ */
+class ContentScriptParent {
+ constructor({ context, details }) {
+ this.context = context;
+ this.scriptId = getUniqueId();
+ this.blobURLs = new Set();
+
+ this.options = this._convertOptions(details);
+
+ context.callOnClose(this);
+ }
+
+ close() {
+ this.destroy();
+ }
+
+ destroy() {
+ if (this.destroyed) {
+ throw new Error("Unable to destroy ContentScriptParent twice");
+ }
+
+ this.destroyed = true;
+
+ this.context.forgetOnClose(this);
+
+ for (const blobURL of this.blobURLs) {
+ this.context.cloneScope.URL.revokeObjectURL(blobURL);
+ }
+
+ this.blobURLs.clear();
+
+ this.context = null;
+ this.options = null;
+ }
+
+ _convertOptions(details) {
+ const { context } = this;
+
+ const options = {
+ matches: details.matches,
+ excludeMatches: details.excludeMatches,
+ includeGlobs: details.includeGlobs,
+ excludeGlobs: details.excludeGlobs,
+ allFrames: details.allFrames,
+ matchAboutBlank: details.matchAboutBlank,
+ runAt: details.runAt || "document_idle",
+ jsPaths: [],
+ cssPaths: [],
+ };
+
+ const convertCodeToURL = (data, mime) => {
+ const blob = new context.cloneScope.Blob(data, { type: mime });
+ const blobURL = context.cloneScope.URL.createObjectURL(blob);
+
+ this.blobURLs.add(blobURL);
+
+ return blobURL;
+ };
+
+ if (details.js && details.js.length) {
+ options.jsPaths = details.js.map(data => {
+ if (data.file) {
+ return data.file;
+ }
+
+ return convertCodeToURL([data.code], "text/javascript");
+ });
+ }
+
+ if (details.css && details.css.length) {
+ options.cssPaths = details.css.map(data => {
+ if (data.file) {
+ return data.file;
+ }
+
+ return convertCodeToURL([data.code], "text/css");
+ });
+ }
+
+ return options;
+ }
+
+ serialize() {
+ return this.options;
+ }
+}
+
+this.contentScripts = class extends ExtensionAPI {
+ getAPI(context) {
+ const { extension } = context;
+
+ // Map of the content script registered from the extension context.
+ //
+ // Map<scriptId -> ContentScriptParent>
+ const parentScriptsMap = new Map();
+
+ // Unregister all the scriptId related to a context when it is closed.
+ context.callOnClose({
+ close() {
+ if (parentScriptsMap.size === 0) {
+ return;
+ }
+
+ const scriptIds = Array.from(parentScriptsMap.keys());
+
+ for (let scriptId of scriptIds) {
+ extension.registeredContentScripts.delete(scriptId);
+ }
+ extension.updateContentScripts();
+
+ extension.broadcast("Extension:UnregisterContentScripts", {
+ id: extension.id,
+ scriptIds,
+ });
+ },
+ });
+
+ return {
+ contentScripts: {
+ async register(details) {
+ for (let origin of details.matches) {
+ if (!extension.allowedOrigins.subsumes(new MatchPattern(origin))) {
+ throw new ExtensionError(
+ `Permission denied to register a content script for ${origin}`
+ );
+ }
+ }
+
+ const contentScript = new ContentScriptParent({ context, details });
+ const { scriptId } = contentScript;
+
+ parentScriptsMap.set(scriptId, contentScript);
+
+ const scriptOptions = contentScript.serialize();
+
+ await extension.broadcast("Extension:RegisterContentScript", {
+ id: extension.id,
+ options: scriptOptions,
+ scriptId,
+ });
+
+ extension.registeredContentScripts.set(scriptId, scriptOptions);
+ extension.updateContentScripts();
+
+ return scriptId;
+ },
+
+ // This method is not available to the extension code, the extension code
+ // doesn't have access to the internally used scriptId, on the contrary
+ // the extension code will call script.unregister on the script API object
+ // that is resolved from the register API method returned promise.
+ async unregister(scriptId) {
+ const contentScript = parentScriptsMap.get(scriptId);
+ if (!contentScript) {
+ Cu.reportError(new Error(`No such content script ID: ${scriptId}`));
+
+ return;
+ }
+
+ parentScriptsMap.delete(scriptId);
+ extension.registeredContentScripts.delete(scriptId);
+ extension.updateContentScripts();
+
+ contentScript.destroy();
+
+ await extension.broadcast("Extension:UnregisterContentScripts", {
+ id: extension.id,
+ scriptIds: [scriptId],
+ });
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-contextualIdentities.js b/toolkit/components/extensions/parent/ext-contextualIdentities.js
new file mode 100644
index 0000000000..dfb4c6ea1b
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-contextualIdentities.js
@@ -0,0 +1,338 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ContextualIdentityService",
+ "resource://gre/modules/ContextualIdentityService.jsm"
+);
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "containersEnabled",
+ "privacy.userContext.enabled"
+);
+
+var { ExtensionPreferencesManager } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionPreferencesManager.jsm"
+);
+
+var { ExtensionError } = ExtensionUtils;
+
+const CONTAINER_PREF_INSTALL_DEFAULTS = {
+ "privacy.userContext.enabled": true,
+ "privacy.userContext.ui.enabled": true,
+ "privacy.usercontext.about_newtab_segregation.enabled": true,
+ "privacy.userContext.extension": undefined,
+};
+
+const CONTAINERS_ENABLED_SETTING_NAME = "privacy.containers";
+
+const CONTAINER_COLORS = new Map([
+ ["blue", "#37adff"],
+ ["turquoise", "#00c79a"],
+ ["green", "#51cd00"],
+ ["yellow", "#ffcb00"],
+ ["orange", "#ff9f00"],
+ ["red", "#ff613d"],
+ ["pink", "#ff4bda"],
+ ["purple", "#af51f5"],
+ ["toolbar", "#7c7c7d"],
+]);
+
+const CONTAINER_ICONS = new Set([
+ "briefcase",
+ "cart",
+ "circle",
+ "dollar",
+ "fence",
+ "fingerprint",
+ "gift",
+ "vacation",
+ "food",
+ "fruit",
+ "pet",
+ "tree",
+ "chill",
+]);
+
+function getContainerIcon(iconName) {
+ if (!CONTAINER_ICONS.has(iconName)) {
+ throw new ExtensionError(`Invalid icon ${iconName} for container`);
+ }
+ return `resource://usercontext-content/${iconName}.svg`;
+}
+
+function getContainerColor(colorName) {
+ if (!CONTAINER_COLORS.has(colorName)) {
+ throw new ExtensionError(`Invalid color name ${colorName} for container`);
+ }
+ return CONTAINER_COLORS.get(colorName);
+}
+
+const convertIdentity = identity => {
+ let result = {
+ name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
+ icon: identity.icon,
+ iconUrl: getContainerIcon(identity.icon),
+ color: identity.color,
+ colorCode: getContainerColor(identity.color),
+ cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
+ };
+
+ return result;
+};
+
+const checkAPIEnabled = () => {
+ if (!containersEnabled) {
+ throw new ExtensionError("Contextual identities are currently disabled");
+ }
+};
+
+const convertIdentityFromObserver = wrappedIdentity => {
+ let identity = wrappedIdentity.wrappedJSObject;
+ let iconUrl, colorCode;
+ try {
+ iconUrl = getContainerIcon(identity.icon);
+ colorCode = getContainerColor(identity.color);
+ } catch (e) {
+ return null;
+ }
+
+ let result = {
+ name: identity.name,
+ icon: identity.icon,
+ iconUrl,
+ color: identity.color,
+ colorCode,
+ cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
+ };
+
+ return result;
+};
+
+ExtensionPreferencesManager.addSetting(CONTAINERS_ENABLED_SETTING_NAME, {
+ prefNames: Object.keys(CONTAINER_PREF_INSTALL_DEFAULTS),
+
+ setCallback(value) {
+ if (value !== true) {
+ return {
+ ...CONTAINER_PREF_INSTALL_DEFAULTS,
+ "privacy.userContext.extension": value,
+ };
+ }
+ return {};
+ },
+});
+
+this.contextualIdentities = class extends ExtensionAPI {
+ onStartup() {
+ let { extension } = this;
+
+ if (extension.hasPermission("contextualIdentities")) {
+ ExtensionPreferencesManager.setSetting(
+ extension.id,
+ CONTAINERS_ENABLED_SETTING_NAME,
+ extension.id
+ );
+ }
+ }
+
+ getAPI(context) {
+ let self = {
+ contextualIdentities: {
+ async get(cookieStoreId) {
+ checkAPIEnabled();
+ let containerId = getContainerForCookieStoreId(cookieStoreId);
+ if (!containerId) {
+ throw new ExtensionError(
+ `Invalid contextual identity: ${cookieStoreId}`
+ );
+ }
+
+ let identity = ContextualIdentityService.getPublicIdentityFromId(
+ containerId
+ );
+ return convertIdentity(identity);
+ },
+
+ async query(details) {
+ checkAPIEnabled();
+ let identities = [];
+ ContextualIdentityService.getPublicIdentities().forEach(identity => {
+ if (
+ details.name &&
+ ContextualIdentityService.getUserContextLabel(
+ identity.userContextId
+ ) != details.name
+ ) {
+ return;
+ }
+
+ identities.push(convertIdentity(identity));
+ });
+
+ return identities;
+ },
+
+ async create(details) {
+ // Lets prevent making containers that are not valid
+ getContainerIcon(details.icon);
+ getContainerColor(details.color);
+
+ let identity = ContextualIdentityService.create(
+ details.name,
+ details.icon,
+ details.color
+ );
+ return convertIdentity(identity);
+ },
+
+ async update(cookieStoreId, details) {
+ checkAPIEnabled();
+ let containerId = getContainerForCookieStoreId(cookieStoreId);
+ if (!containerId) {
+ throw new ExtensionError(
+ `Invalid contextual identity: ${cookieStoreId}`
+ );
+ }
+
+ let identity = ContextualIdentityService.getPublicIdentityFromId(
+ containerId
+ );
+ if (!identity) {
+ throw new ExtensionError(
+ `Invalid contextual identity: ${cookieStoreId}`
+ );
+ }
+
+ if (details.name !== null) {
+ identity.name = details.name;
+ }
+
+ if (details.color !== null) {
+ getContainerColor(details.color);
+ identity.color = details.color;
+ }
+
+ if (details.icon !== null) {
+ getContainerIcon(details.icon);
+ identity.icon = details.icon;
+ }
+
+ if (
+ !ContextualIdentityService.update(
+ identity.userContextId,
+ identity.name,
+ identity.icon,
+ identity.color
+ )
+ ) {
+ throw new ExtensionError(
+ `Contextual identity failed to update: ${cookieStoreId}`
+ );
+ }
+
+ return convertIdentity(identity);
+ },
+
+ async remove(cookieStoreId) {
+ checkAPIEnabled();
+ let containerId = getContainerForCookieStoreId(cookieStoreId);
+ if (!containerId) {
+ throw new ExtensionError(
+ `Invalid contextual identity: ${cookieStoreId}`
+ );
+ }
+
+ let identity = ContextualIdentityService.getPublicIdentityFromId(
+ containerId
+ );
+ if (!identity) {
+ throw new ExtensionError(
+ `Invalid contextual identity: ${cookieStoreId}`
+ );
+ }
+
+ // We have to create the identity object before removing it.
+ let convertedIdentity = convertIdentity(identity);
+
+ if (!ContextualIdentityService.remove(identity.userContextId)) {
+ throw new ExtensionError(
+ `Contextual identity failed to remove: ${cookieStoreId}`
+ );
+ }
+
+ return convertedIdentity;
+ },
+
+ onCreated: new EventManager({
+ context,
+ name: "contextualIdentities.onCreated",
+ register: fire => {
+ let observer = (subject, topic) => {
+ let convertedIdentity = convertIdentityFromObserver(subject);
+ if (convertedIdentity) {
+ fire.async({ contextualIdentity: convertedIdentity });
+ }
+ };
+
+ Services.obs.addObserver(observer, "contextual-identity-created");
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ "contextual-identity-created"
+ );
+ };
+ },
+ }).api(),
+
+ onUpdated: new EventManager({
+ context,
+ name: "contextualIdentities.onUpdated",
+ register: fire => {
+ let observer = (subject, topic) => {
+ let convertedIdentity = convertIdentityFromObserver(subject);
+ if (convertedIdentity) {
+ fire.async({ contextualIdentity: convertedIdentity });
+ }
+ };
+
+ Services.obs.addObserver(observer, "contextual-identity-updated");
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ "contextual-identity-updated"
+ );
+ };
+ },
+ }).api(),
+
+ onRemoved: new EventManager({
+ context,
+ name: "contextualIdentities.onRemoved",
+ register: fire => {
+ let observer = (subject, topic) => {
+ let convertedIdentity = convertIdentityFromObserver(subject);
+ if (convertedIdentity) {
+ fire.async({ contextualIdentity: convertedIdentity });
+ }
+ };
+
+ Services.obs.addObserver(observer, "contextual-identity-deleted");
+ return () => {
+ Services.obs.removeObserver(
+ observer,
+ "contextual-identity-deleted"
+ );
+ };
+ },
+ }).api(),
+ },
+ };
+
+ return self;
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-cookies.js b/toolkit/components/extensions/parent/ext-cookies.js
new file mode 100644
index 0000000000..b8ade229b1
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-cookies.js
@@ -0,0 +1,613 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+
+/* globals DEFAULT_STORE, PRIVATE_STORE */
+
+var { ExtensionError } = ExtensionUtils;
+
+const SAME_SITE_STATUSES = [
+ "no_restriction", // Index 0 = Ci.nsICookie.SAMESITE_NONE
+ "lax", // Index 1 = Ci.nsICookie.SAMESITE_LAX
+ "strict", // Index 2 = Ci.nsICookie.SAMESITE_STRICT
+];
+
+const isIPv4 = host => {
+ let match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
+
+ if (match) {
+ return match[1] < 256 && match[2] < 256 && match[3] < 256 && match[4] < 256;
+ }
+ return false;
+};
+const isIPv6 = host => host.includes(":");
+const addBracketIfIPv6 = host =>
+ isIPv6(host) && !host.startsWith("[") ? `[${host}]` : host;
+const dropBracketIfIPv6 = host =>
+ isIPv6(host) && host.startsWith("[") && host.endsWith("]")
+ ? host.slice(1, -1)
+ : host;
+
+const convertCookie = ({ cookie, isPrivate }) => {
+ let result = {
+ name: cookie.name,
+ value: cookie.value,
+ domain: addBracketIfIPv6(cookie.host),
+ hostOnly: !cookie.isDomain,
+ path: cookie.path,
+ secure: cookie.isSecure,
+ httpOnly: cookie.isHttpOnly,
+ sameSite: SAME_SITE_STATUSES[cookie.sameSite],
+ session: cookie.isSession,
+ firstPartyDomain: cookie.originAttributes.firstPartyDomain || "",
+ };
+
+ if (!cookie.isSession) {
+ result.expirationDate = cookie.expiry;
+ }
+
+ if (cookie.originAttributes.userContextId) {
+ result.storeId = getCookieStoreIdForContainer(
+ cookie.originAttributes.userContextId
+ );
+ } else if (cookie.originAttributes.privateBrowsingId || isPrivate) {
+ result.storeId = PRIVATE_STORE;
+ } else {
+ result.storeId = DEFAULT_STORE;
+ }
+
+ return result;
+};
+
+const isSubdomain = (otherDomain, baseDomain) => {
+ return otherDomain == baseDomain || otherDomain.endsWith("." + baseDomain);
+};
+
+// Checks that the given extension has permission to set the given cookie for
+// the given URI.
+const checkSetCookiePermissions = (extension, uri, cookie) => {
+ // Permission checks:
+ //
+ // - If the extension does not have permissions for the specified
+ // URL, it cannot set cookies for it.
+ //
+ // - If the specified URL could not set the given cookie, neither can
+ // the extension.
+ //
+ // Ideally, we would just have the cookie service make the latter
+ // determination, but that turns out to be quite complicated. At the
+ // moment, it requires constructing a cookie string and creating a
+ // dummy channel, both of which can be problematic. It also triggers
+ // a whole set of additional permission and preference checks, which
+ // may or may not be desirable.
+ //
+ // So instead, we do a similar set of checks here. Exactly what
+ // cookies a given URL should be able to set is not well-documented,
+ // and is not standardized in any standard that anyone actually
+ // follows. So instead, we follow the rules used by the cookie
+ // service.
+ //
+ // See source/netwerk/cookie/CookieService.cpp, in particular
+ // CheckDomain() and SetCookieInternal().
+
+ if (uri.scheme != "http" && uri.scheme != "https") {
+ return false;
+ }
+
+ if (!extension.allowedOrigins.matches(uri)) {
+ return false;
+ }
+
+ if (!cookie.host) {
+ // If no explicit host is specified, this becomes a host-only cookie.
+ cookie.host = uri.host;
+ return true;
+ }
+
+ // A leading "." is not expected, but is tolerated if it's not the only
+ // character in the host. If there is one, start by stripping it off. We'll
+ // add a new one on success.
+ if (cookie.host.length > 1) {
+ cookie.host = cookie.host.replace(/^\./, "");
+ }
+ cookie.host = cookie.host.toLowerCase();
+ cookie.host = dropBracketIfIPv6(cookie.host);
+
+ if (cookie.host != uri.host) {
+ // Not an exact match, so check for a valid subdomain.
+ let baseDomain;
+ try {
+ baseDomain = Services.eTLD.getBaseDomain(uri);
+ } catch (e) {
+ if (
+ e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS ||
+ e.result == Cr.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS
+ ) {
+ // The cookie service uses these to determine whether the domain
+ // requires an exact match. We already know we don't have an exact
+ // match, so return false. In all other cases, re-raise the error.
+ return false;
+ }
+ throw e;
+ }
+
+ // The cookie domain must be a subdomain of the base domain. This prevents
+ // us from setting cookies for domains like ".co.uk".
+ // The domain of the requesting URL must likewise be a subdomain of the
+ // cookie domain. This prevents us from setting cookies for entirely
+ // unrelated domains.
+ if (
+ !isSubdomain(cookie.host, baseDomain) ||
+ !isSubdomain(uri.host, cookie.host)
+ ) {
+ return false;
+ }
+
+ // RFC2109 suggests that we may only add cookies for sub-domains 1-level
+ // below us, but enforcing that would break the web, so we don't.
+ }
+
+ // If the host is an IP address, avoid adding a leading ".".
+ // An IP address is not a domain name, and only supports host-only cookies.
+ if (isIPv6(cookie.host) || isIPv4(cookie.host)) {
+ return true;
+ }
+
+ // An explicit domain was passed, so add a leading "." to make this a
+ // domain cookie.
+ cookie.host = "." + cookie.host;
+
+ // We don't do any significant checking of path permissions. RFC2109
+ // suggests we only allow sites to add cookies for sub-paths, similar to
+ // same origin policy enforcement, but no-one implements this.
+
+ return true;
+};
+
+/**
+ * Query the cookie store for matching cookies.
+ * @param {Object} detailsIn
+ * @param {Array} props Properties the extension is interested in matching against.
+ * @param {BaseContext} context The context making the query.
+ */
+const query = function*(detailsIn, props, context) {
+ let details = {};
+ props.forEach(property => {
+ if (detailsIn[property] !== null) {
+ details[property] = detailsIn[property];
+ }
+ });
+
+ if ("domain" in details) {
+ details.domain = details.domain.toLowerCase().replace(/^\./, "");
+ details.domain = dropBracketIfIPv6(details.domain);
+ }
+
+ let userContextId = 0;
+ let isPrivate = context.incognito;
+ if (details.storeId) {
+ if (!isValidCookieStoreId(details.storeId)) {
+ return;
+ }
+
+ if (isDefaultCookieStoreId(details.storeId)) {
+ isPrivate = false;
+ } else if (isPrivateCookieStoreId(details.storeId)) {
+ isPrivate = true;
+ } else if (isContainerCookieStoreId(details.storeId)) {
+ isPrivate = false;
+ userContextId = getContainerForCookieStoreId(details.storeId);
+ if (!userContextId) {
+ return;
+ }
+ }
+ }
+
+ let storeId = DEFAULT_STORE;
+ if (isPrivate) {
+ storeId = PRIVATE_STORE;
+ } else if ("storeId" in details) {
+ storeId = details.storeId;
+ }
+ if (storeId == PRIVATE_STORE && !context.privateBrowsingAllowed) {
+ throw new ExtensionError(
+ "Extension disallowed access to the private cookies storeId."
+ );
+ }
+
+ // We can use getCookiesFromHost for faster searching.
+ let cookies;
+ let host;
+ let url;
+ let originAttributes = {
+ userContextId,
+ privateBrowsingId: isPrivate ? 1 : 0,
+ };
+ if ("firstPartyDomain" in details) {
+ originAttributes.firstPartyDomain = details.firstPartyDomain;
+ }
+ if ("url" in details) {
+ try {
+ url = new URL(details.url);
+ host = dropBracketIfIPv6(url.hostname);
+ } catch (ex) {
+ // This often happens for about: URLs
+ return;
+ }
+ } else if ("domain" in details) {
+ host = details.domain;
+ }
+
+ if (host && "firstPartyDomain" in originAttributes) {
+ // getCookiesFromHost is more efficient than getCookiesWithOriginAttributes
+ // if the host and all origin attributes are known.
+ cookies = Services.cookies.getCookiesFromHost(host, originAttributes);
+ } else {
+ cookies = Services.cookies.getCookiesWithOriginAttributes(
+ JSON.stringify(originAttributes),
+ host
+ );
+ }
+
+ // Based on CookieService::GetCookieStringFromHttp
+ function matches(cookie) {
+ function domainMatches(host) {
+ return (
+ cookie.rawHost == host ||
+ (cookie.isDomain && host.endsWith(cookie.host))
+ );
+ }
+
+ function pathMatches(path) {
+ let cookiePath = cookie.path.replace(/\/$/, "");
+
+ if (!path.startsWith(cookiePath)) {
+ return false;
+ }
+
+ // path == cookiePath, but without the redundant string compare.
+ if (path.length == cookiePath.length) {
+ return true;
+ }
+
+ // URL path is a substring of the cookie path, so it matches if, and
+ // only if, the next character is a path delimiter.
+ return path[cookiePath.length] === "/";
+ }
+
+ // "Restricts the retrieved cookies to those that would match the given URL."
+ if (url) {
+ if (!domainMatches(host)) {
+ return false;
+ }
+
+ if (cookie.isSecure && url.protocol != "https:") {
+ return false;
+ }
+
+ if (!pathMatches(url.pathname)) {
+ return false;
+ }
+ }
+
+ if ("name" in details && details.name != cookie.name) {
+ return false;
+ }
+
+ // "Restricts the retrieved cookies to those whose domains match or are subdomains of this one."
+ if ("domain" in details && !isSubdomain(cookie.rawHost, details.domain)) {
+ return false;
+ }
+
+ // "Restricts the retrieved cookies to those whose path exactly matches this string.""
+ if ("path" in details && details.path != cookie.path) {
+ return false;
+ }
+
+ if ("secure" in details && details.secure != cookie.isSecure) {
+ return false;
+ }
+
+ if ("session" in details && details.session != cookie.isSession) {
+ return false;
+ }
+
+ // Check that the extension has permissions for this host.
+ if (!context.extension.allowedOrigins.matchesCookie(cookie)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ for (const cookie of cookies) {
+ if (matches(cookie)) {
+ yield { cookie, isPrivate, storeId };
+ }
+ }
+};
+
+const normalizeFirstPartyDomain = details => {
+ if (details.firstPartyDomain != null) {
+ return;
+ }
+ if (Services.prefs.getBoolPref("privacy.firstparty.isolate")) {
+ throw new ExtensionError(
+ "First-Party Isolation is enabled, but the required 'firstPartyDomain' attribute was not set."
+ );
+ }
+
+ // When FPI is disabled, the "firstPartyDomain" attribute is optional
+ // and defaults to the empty string.
+ details.firstPartyDomain = "";
+};
+
+this.cookies = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ let self = {
+ cookies: {
+ get: function(details) {
+ normalizeFirstPartyDomain(details);
+
+ // FIXME: We don't sort by length of path and creation time.
+ let allowed = ["url", "name", "storeId", "firstPartyDomain"];
+ for (let cookie of query(details, allowed, context)) {
+ return Promise.resolve(convertCookie(cookie));
+ }
+
+ // Found no match.
+ return Promise.resolve(null);
+ },
+
+ getAll: function(details) {
+ if (!("firstPartyDomain" in details)) {
+ normalizeFirstPartyDomain(details);
+ }
+
+ let allowed = [
+ "url",
+ "name",
+ "domain",
+ "path",
+ "secure",
+ "session",
+ "storeId",
+ ];
+
+ // firstPartyDomain may be set to null or undefined to not filter by FPD.
+ if (details.firstPartyDomain != null) {
+ allowed.push("firstPartyDomain");
+ }
+
+ let result = Array.from(
+ query(details, allowed, context),
+ convertCookie
+ );
+
+ return Promise.resolve(result);
+ },
+
+ set: function(details) {
+ normalizeFirstPartyDomain(details);
+
+ let uri = Services.io.newURI(details.url);
+
+ let path;
+ if (details.path !== null) {
+ path = details.path;
+ } else {
+ // This interface essentially emulates the behavior of the
+ // Set-Cookie header. In the case of an omitted path, the cookie
+ // service uses the directory path of the requesting URL, ignoring
+ // any filename or query parameters.
+ path = uri.QueryInterface(Ci.nsIURL).directory;
+ }
+
+ let name = details.name !== null ? details.name : "";
+ let value = details.value !== null ? details.value : "";
+ let secure = details.secure !== null ? details.secure : false;
+ let httpOnly = details.httpOnly !== null ? details.httpOnly : false;
+ let isSession = details.expirationDate === null;
+ let expiry = isSession
+ ? Number.MAX_SAFE_INTEGER
+ : details.expirationDate;
+ let isPrivate = context.incognito;
+ let userContextId = 0;
+ if (isDefaultCookieStoreId(details.storeId)) {
+ isPrivate = false;
+ } else if (isPrivateCookieStoreId(details.storeId)) {
+ if (!context.privateBrowsingAllowed) {
+ return Promise.reject({
+ message:
+ "Extension disallowed access to the private cookies storeId.",
+ });
+ }
+ isPrivate = true;
+ } else if (isContainerCookieStoreId(details.storeId)) {
+ let containerId = getContainerForCookieStoreId(details.storeId);
+ if (containerId === null) {
+ return Promise.reject({
+ message: `Illegal storeId: ${details.storeId}`,
+ });
+ }
+ isPrivate = false;
+ userContextId = containerId;
+ } else if (details.storeId !== null) {
+ return Promise.reject({ message: "Unknown storeId" });
+ }
+
+ let cookieAttrs = {
+ host: details.domain,
+ path: path,
+ isSecure: secure,
+ };
+ if (!checkSetCookiePermissions(extension, uri, cookieAttrs)) {
+ return Promise.reject({
+ message: `Permission denied to set cookie ${JSON.stringify(
+ details
+ )}`,
+ });
+ }
+
+ let originAttributes = {
+ userContextId,
+ privateBrowsingId: isPrivate ? 1 : 0,
+ firstPartyDomain: details.firstPartyDomain,
+ };
+
+ let sameSite = SAME_SITE_STATUSES.indexOf(details.sameSite);
+
+ let schemeType = Ci.nsICookie.SCHEME_UNSET;
+ if (uri.scheme === "https") {
+ schemeType = Ci.nsICookie.SCHEME_HTTPS;
+ } else if (uri.scheme === "http") {
+ schemeType = Ci.nsICookie.SCHEME_HTTP;
+ } else if (uri.scheme === "file") {
+ schemeType = Ci.nsICookie.SCHEME_FILE;
+ }
+
+ // The permission check may have modified the domain, so use
+ // the new value instead.
+ Services.cookies.add(
+ cookieAttrs.host,
+ path,
+ name,
+ value,
+ secure,
+ httpOnly,
+ isSession,
+ expiry,
+ originAttributes,
+ sameSite,
+ schemeType
+ );
+
+ return self.cookies.get(details);
+ },
+
+ remove: function(details) {
+ normalizeFirstPartyDomain(details);
+
+ let allowed = ["url", "name", "storeId", "firstPartyDomain"];
+ for (let { cookie, storeId } of query(details, allowed, context)) {
+ if (
+ isPrivateCookieStoreId(details.storeId) &&
+ !context.privateBrowsingAllowed
+ ) {
+ return Promise.reject({ message: "Unknown storeId" });
+ }
+ Services.cookies.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ cookie.originAttributes
+ );
+
+ // TODO Bug 1387957: could there be multiple per subdomain?
+ return Promise.resolve({
+ url: details.url,
+ name: details.name,
+ storeId,
+ firstPartyDomain: details.firstPartyDomain,
+ });
+ }
+
+ return Promise.resolve(null);
+ },
+
+ getAllCookieStores: function() {
+ let data = {};
+ for (let tab of extension.tabManager.query()) {
+ if (!(tab.cookieStoreId in data)) {
+ data[tab.cookieStoreId] = [];
+ }
+ data[tab.cookieStoreId].push(tab.id);
+ }
+
+ let result = [];
+ for (let key in data) {
+ result.push({
+ id: key,
+ tabIds: data[key],
+ incognito: key == PRIVATE_STORE,
+ });
+ }
+ return Promise.resolve(result);
+ },
+
+ onChanged: new EventManager({
+ context,
+ name: "cookies.onChanged",
+ register: fire => {
+ let observer = (subject, topic, data) => {
+ let notify = (removed, cookie, cause) => {
+ cookie.QueryInterface(Ci.nsICookie);
+
+ if (extension.allowedOrigins.matchesCookie(cookie)) {
+ fire.async({
+ removed,
+ cookie: convertCookie({
+ cookie,
+ isPrivate: topic == "private-cookie-changed",
+ }),
+ cause,
+ });
+ }
+ };
+
+ // We do our best effort here to map the incompatible states.
+ switch (data) {
+ case "deleted":
+ notify(true, subject, "explicit");
+ break;
+ case "added":
+ notify(false, subject, "explicit");
+ break;
+ case "changed":
+ notify(true, subject, "overwrite");
+ notify(false, subject, "explicit");
+ break;
+ case "batch-deleted":
+ subject.QueryInterface(Ci.nsIArray);
+ for (let i = 0; i < subject.length; i++) {
+ let cookie = subject.queryElementAt(i, Ci.nsICookie);
+ if (
+ !cookie.isSession &&
+ cookie.expiry * 1000 <= Date.now()
+ ) {
+ notify(true, cookie, "expired");
+ } else {
+ notify(true, cookie, "evicted");
+ }
+ }
+ break;
+ }
+ };
+
+ Services.obs.addObserver(observer, "cookie-changed");
+ if (context.privateBrowsingAllowed) {
+ Services.obs.addObserver(observer, "private-cookie-changed");
+ }
+ return () => {
+ Services.obs.removeObserver(observer, "cookie-changed");
+ if (context.privateBrowsingAllowed) {
+ Services.obs.removeObserver(observer, "private-cookie-changed");
+ }
+ };
+ },
+ }).api(),
+ },
+ };
+
+ return self;
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-dns.js b/toolkit/components/extensions/parent/ext-dns.js
new file mode 100644
index 0000000000..6af637924f
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-dns.js
@@ -0,0 +1,90 @@
+/* -*- 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";
+
+const dnssFlags = {
+ allow_name_collisions: Ci.nsIDNSService.RESOLVE_ALLOW_NAME_COLLISION,
+ bypass_cache: Ci.nsIDNSService.RESOLVE_BYPASS_CACHE,
+ canonical_name: Ci.nsIDNSService.RESOLVE_CANONICAL_NAME,
+ disable_ipv4: Ci.nsIDNSService.RESOLVE_DISABLE_IPV4,
+ disable_ipv6: Ci.nsIDNSService.RESOLVE_DISABLE_IPV6,
+ disable_trr: Ci.nsIDNSService.RESOLVE_DISABLE_TRR,
+ offline: Ci.nsIDNSService.RESOLVE_OFFLINE,
+ priority_low: Ci.nsIDNSService.RESOLVE_PRIORITY_LOW,
+ priority_medium: Ci.nsIDNSService.RESOLVE_PRIORITY_MEDIUM,
+ speculate: Ci.nsIDNSService.RESOLVE_SPECULATE,
+};
+
+function getErrorString(nsresult) {
+ let e = new Components.Exception("", nsresult);
+ return e.name;
+}
+
+this.dns = class extends ExtensionAPI {
+ getAPI(context) {
+ const dnss = Cc["@mozilla.org/network/dns-service;1"].getService(
+ Ci.nsIDNSService
+ );
+ return {
+ dns: {
+ resolve: function(hostname, flags) {
+ let dnsFlags = flags.reduce(
+ (mask, flag) => mask | dnssFlags[flag],
+ 0
+ );
+
+ return new Promise((resolve, reject) => {
+ let request;
+ let response = {
+ addresses: [],
+ };
+ let listener = {
+ onLookupComplete: function(inRequest, inRecord, inStatus) {
+ if (inRequest === request) {
+ if (!Components.isSuccessCode(inStatus)) {
+ return reject({ message: getErrorString(inStatus) });
+ }
+ inRecord.QueryInterface(Ci.nsIDNSAddrRecord);
+ if (dnsFlags & Ci.nsIDNSService.RESOLVE_CANONICAL_NAME) {
+ try {
+ response.canonicalName = inRecord.canonicalName;
+ } catch (e) {
+ // no canonicalName
+ }
+ }
+ response.isTRR = inRecord.IsTRR();
+ while (inRecord.hasMore()) {
+ let addr = inRecord.getNextAddrAsString();
+ // Sometimes there are duplicate records with the same ip.
+ if (!response.addresses.includes(addr)) {
+ response.addresses.push(addr);
+ }
+ }
+ return resolve(response);
+ }
+ },
+ };
+ try {
+ request = dnss.asyncResolve(
+ hostname,
+ Ci.nsIDNSService.RESOLVE_TYPE_DEFAULT,
+ dnsFlags,
+ null, // resolverInfo
+ listener,
+ null,
+ {} /* defaultOriginAttributes */
+ );
+ } catch (e) {
+ // handle exceptions such as offline mode.
+ return reject({ message: e.name });
+ }
+ });
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-downloads.js b/toolkit/components/extensions/parent/ext-downloads.js
new file mode 100644
index 0000000000..e6094d91ff
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-downloads.js
@@ -0,0 +1,1264 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AppConstants",
+ "resource://gre/modules/AppConstants.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Downloads",
+ "resource://gre/modules/Downloads.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "DownloadPaths",
+ "resource://gre/modules/DownloadPaths.jsm"
+);
+ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "DownloadLastDir",
+ "resource://gre/modules/DownloadLastDir.jsm"
+);
+
+var { EventEmitter, ignoreEvent } = ExtensionCommon;
+
+const DOWNLOAD_ITEM_FIELDS = [
+ "id",
+ "url",
+ "referrer",
+ "filename",
+ "incognito",
+ "danger",
+ "mime",
+ "startTime",
+ "endTime",
+ "estimatedEndTime",
+ "state",
+ "paused",
+ "canResume",
+ "error",
+ "bytesReceived",
+ "totalBytes",
+ "fileSize",
+ "exists",
+ "byExtensionId",
+ "byExtensionName",
+];
+
+const DOWNLOAD_DATE_FIELDS = ["startTime", "endTime", "estimatedEndTime"];
+
+// Fields that we generate onChanged events for.
+const DOWNLOAD_ITEM_CHANGE_FIELDS = [
+ "endTime",
+ "state",
+ "paused",
+ "canResume",
+ "error",
+ "exists",
+];
+
+// From https://fetch.spec.whatwg.org/#forbidden-header-name
+// Since bug 1367626 we allow extensions to set REFERER.
+const FORBIDDEN_HEADERS = [
+ "ACCEPT-CHARSET",
+ "ACCEPT-ENCODING",
+ "ACCESS-CONTROL-REQUEST-HEADERS",
+ "ACCESS-CONTROL-REQUEST-METHOD",
+ "CONNECTION",
+ "CONTENT-LENGTH",
+ "COOKIE",
+ "COOKIE2",
+ "DATE",
+ "DNT",
+ "EXPECT",
+ "HOST",
+ "KEEP-ALIVE",
+ "ORIGIN",
+ "TE",
+ "TRAILER",
+ "TRANSFER-ENCODING",
+ "UPGRADE",
+ "VIA",
+];
+
+const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i;
+
+const PROMPTLESS_DOWNLOAD_PREF = "browser.download.useDownloadDir";
+
+// Lists of file extensions for each file picker filter taken from filepicker.properties
+const FILTER_HTML_EXTENSIONS = ["html", "htm", "shtml", "xhtml"];
+
+const FILTER_TEXT_EXTENSIONS = ["txt", "text"];
+
+const FILTER_IMAGES_EXTENSIONS = [
+ "jpe",
+ "jpg",
+ "jpeg",
+ "gif",
+ "png",
+ "bmp",
+ "ico",
+ "svg",
+ "svgz",
+ "tif",
+ "tiff",
+ "ai",
+ "drw",
+ "pct",
+ "psp",
+ "xcf",
+ "psd",
+ "raw",
+ "webp",
+];
+
+const FILTER_XML_EXTENSIONS = ["xml"];
+
+const FILTER_AUDIO_EXTENSIONS = [
+ "aac",
+ "aif",
+ "flac",
+ "iff",
+ "m4a",
+ "m4b",
+ "mid",
+ "midi",
+ "mp3",
+ "mpa",
+ "mpc",
+ "oga",
+ "ogg",
+ "ra",
+ "ram",
+ "snd",
+ "wav",
+ "wma",
+];
+
+const FILTER_VIDEO_EXTENSIONS = [
+ "avi",
+ "divx",
+ "flv",
+ "m4v",
+ "mkv",
+ "mov",
+ "mp4",
+ "mpeg",
+ "mpg",
+ "ogm",
+ "ogv",
+ "ogx",
+ "rm",
+ "rmvb",
+ "smil",
+ "webm",
+ "wmv",
+ "xvid",
+];
+
+class DownloadItem {
+ constructor(id, download, extension) {
+ this.id = id;
+ this.download = download;
+ this.extension = extension;
+ this.prechange = {};
+ this._error = null;
+ }
+
+ get url() {
+ return this.download.source.url;
+ }
+ get referrer() {
+ const uri = this.download.source.referrerInfo
+ ? this.download.source.referrerInfo.originalReferrer
+ : null;
+
+ return uri && uri.spec;
+ }
+ get filename() {
+ return this.download.target.path;
+ }
+ get incognito() {
+ return this.download.source.isPrivate;
+ }
+ get danger() {
+ return "safe";
+ } // TODO
+ get mime() {
+ return this.download.contentType;
+ }
+ get startTime() {
+ return this.download.startTime;
+ }
+ get endTime() {
+ return null;
+ } // TODO
+ get estimatedEndTime() {
+ // Based on the code in summarizeDownloads() in DownloadsCommon.jsm
+ if (this.download.hasProgress && this.download.speed > 0) {
+ let sizeLeft = this.download.totalBytes - this.download.currentBytes;
+ let timeLeftInSeconds = sizeLeft / this.download.speed;
+ return new Date(Date.now() + timeLeftInSeconds * 1000);
+ }
+ }
+ get state() {
+ if (this.download.succeeded) {
+ return "complete";
+ }
+ if (this.download.canceled || this.error) {
+ return "interrupted";
+ }
+ return "in_progress";
+ }
+ get paused() {
+ return (
+ this.download.canceled &&
+ this.download.hasPartialData &&
+ !this.download.error
+ );
+ }
+ get canResume() {
+ return (
+ (this.download.stopped || this.download.canceled) &&
+ this.download.hasPartialData &&
+ !this.download.error
+ );
+ }
+ get error() {
+ if (this._error) {
+ return this._error;
+ }
+ if (
+ !this.download.startTime ||
+ !this.download.stopped ||
+ this.download.succeeded
+ ) {
+ return null;
+ }
+ // TODO store this instead of calculating it
+
+ if (this.download.error) {
+ if (this.download.error.becauseSourceFailed) {
+ return "NETWORK_FAILED"; // TODO
+ }
+ if (this.download.error.becauseTargetFailed) {
+ return "FILE_FAILED"; // TODO
+ }
+ return "CRASH";
+ }
+ return "USER_CANCELED";
+ }
+ set error(value) {
+ this._error = value && value.toString();
+ }
+ get bytesReceived() {
+ return this.download.currentBytes;
+ }
+ get totalBytes() {
+ return this.download.hasProgress ? this.download.totalBytes : -1;
+ }
+ get fileSize() {
+ // todo: this is supposed to be post-compression
+ return this.download.succeeded ? this.download.target.size : -1;
+ }
+ get exists() {
+ return this.download.target.exists;
+ }
+ get byExtensionId() {
+ return this.extension ? this.extension.id : undefined;
+ }
+ get byExtensionName() {
+ return this.extension ? this.extension.name : undefined;
+ }
+
+ /**
+ * Create a cloneable version of this object by pulling all the
+ * fields into simple properties (instead of getters).
+ *
+ * @returns {object} A DownloadItem with flat properties,
+ * suitable for cloning.
+ */
+ serialize() {
+ let obj = {};
+ for (let field of DOWNLOAD_ITEM_FIELDS) {
+ obj[field] = this[field];
+ }
+ for (let field of DOWNLOAD_DATE_FIELDS) {
+ if (obj[field]) {
+ obj[field] = obj[field].toISOString();
+ }
+ }
+ return obj;
+ }
+
+ // When a change event fires, handlers can look at how an individual
+ // field changed by comparing item.fieldname with item.prechange.fieldname.
+ // After all handlers have been invoked, this gets called to store the
+ // current values of all fields ahead of the next event.
+ _storePrechange() {
+ for (let field of DOWNLOAD_ITEM_CHANGE_FIELDS) {
+ this.prechange[field] = this[field];
+ }
+ }
+}
+
+// DownloadMap maps back and forth between the numeric identifiers used in
+// the downloads WebExtension API and a Download object from the Downloads jsm.
+// TODO Bug 1247794: make id and extension info persistent
+const DownloadMap = new (class extends EventEmitter {
+ constructor() {
+ super();
+
+ this.currentId = 0;
+ this.loadPromise = null;
+
+ // Maps numeric id -> DownloadItem
+ this.byId = new Map();
+
+ // Maps Download object -> DownloadItem
+ this.byDownload = new WeakMap();
+ }
+
+ lazyInit() {
+ if (this.loadPromise == null) {
+ this.loadPromise = Downloads.getList(Downloads.ALL).then(list => {
+ let self = this;
+ return list
+ .addView({
+ onDownloadAdded(download) {
+ const item = self.newFromDownload(download, null);
+ self.emit("create", item);
+ item._storePrechange();
+ },
+
+ onDownloadRemoved(download) {
+ const item = self.byDownload.get(download);
+ if (item != null) {
+ self.emit("erase", item);
+ self.byDownload.delete(download);
+ self.byId.delete(item.id);
+ }
+ },
+
+ onDownloadChanged(download) {
+ const item = self.byDownload.get(download);
+ if (item == null) {
+ Cu.reportError(
+ "Got onDownloadChanged for unknown download object"
+ );
+ } else {
+ self.emit("change", item);
+ item._storePrechange();
+ }
+ },
+ })
+ .then(() => list.getAll())
+ .then(downloads => {
+ downloads.forEach(download => {
+ this.newFromDownload(download, null);
+ });
+ })
+ .then(() => list);
+ });
+ }
+ return this.loadPromise;
+ }
+
+ getDownloadList() {
+ return this.lazyInit();
+ }
+
+ getAll() {
+ return this.lazyInit().then(() => this.byId.values());
+ }
+
+ fromId(id, privateAllowed = true) {
+ const download = this.byId.get(id);
+ if (!download || (!privateAllowed && download.incognito)) {
+ throw new Error(`Invalid download id ${id}`);
+ }
+ return download;
+ }
+
+ newFromDownload(download, extension) {
+ if (this.byDownload.has(download)) {
+ return this.byDownload.get(download);
+ }
+
+ const id = ++this.currentId;
+ let item = new DownloadItem(id, download, extension);
+ this.byId.set(id, item);
+ this.byDownload.set(download, item);
+ return item;
+ }
+
+ erase(item) {
+ // TODO Bug 1255507: for now we only work with downloads in the DownloadList
+ // from getAll()
+ return this.getDownloadList().then(list => {
+ list.remove(item.download);
+ });
+ }
+})();
+
+// Create a callable function that filters a DownloadItem based on a
+// query object of the type passed to search() or erase().
+const downloadQuery = query => {
+ let queryTerms = [];
+ let queryNegativeTerms = [];
+ if (query.query != null) {
+ for (let term of query.query) {
+ if (term[0] == "-") {
+ queryNegativeTerms.push(term.slice(1).toLowerCase());
+ } else {
+ queryTerms.push(term.toLowerCase());
+ }
+ }
+ }
+
+ function normalizeDownloadTime(arg, before) {
+ if (arg == null) {
+ return before ? Number.MAX_VALUE : 0;
+ }
+ return ExtensionCommon.normalizeTime(arg).getTime();
+ }
+
+ const startedBefore = normalizeDownloadTime(query.startedBefore, true);
+ const startedAfter = normalizeDownloadTime(query.startedAfter, false);
+ // const endedBefore = normalizeDownloadTime(query.endedBefore, true);
+ // const endedAfter = normalizeDownloadTime(query.endedAfter, false);
+
+ const totalBytesGreater =
+ query.totalBytesGreater !== null ? query.totalBytesGreater : -1;
+ const totalBytesLess =
+ query.totalBytesLess !== null ? query.totalBytesLess : Number.MAX_VALUE;
+
+ // Handle options for which we can have a regular expression and/or
+ // an explicit value to match.
+ function makeMatch(regex, value, field) {
+ if (value == null && regex == null) {
+ return input => true;
+ }
+
+ let re;
+ try {
+ re = new RegExp(regex || "", "i");
+ } catch (err) {
+ throw new Error(`Invalid ${field}Regex: ${err.message}`);
+ }
+ if (value == null) {
+ return input => re.test(input);
+ }
+
+ value = value.toLowerCase();
+ if (re.test(value)) {
+ return input => value == input;
+ }
+ return input => false;
+ }
+
+ const matchFilename = makeMatch(
+ query.filenameRegex,
+ query.filename,
+ "filename"
+ );
+ const matchUrl = makeMatch(query.urlRegex, query.url, "url");
+
+ return function(item) {
+ const url = item.url.toLowerCase();
+ const filename = item.filename.toLowerCase();
+
+ if (
+ !queryTerms.every(term => url.includes(term) || filename.includes(term))
+ ) {
+ return false;
+ }
+
+ if (
+ queryNegativeTerms.some(
+ term => url.includes(term) || filename.includes(term)
+ )
+ ) {
+ return false;
+ }
+
+ if (!matchFilename(filename) || !matchUrl(url)) {
+ return false;
+ }
+
+ if (!item.startTime) {
+ if (query.startedBefore != null || query.startedAfter != null) {
+ return false;
+ }
+ } else if (
+ item.startTime > startedBefore ||
+ item.startTime < startedAfter
+ ) {
+ return false;
+ }
+
+ // todo endedBefore, endedAfter
+
+ if (item.totalBytes == -1) {
+ if (query.totalBytesGreater !== null || query.totalBytesLess !== null) {
+ return false;
+ }
+ } else if (
+ item.totalBytes <= totalBytesGreater ||
+ item.totalBytes >= totalBytesLess
+ ) {
+ return false;
+ }
+
+ // todo: include danger
+ const SIMPLE_ITEMS = [
+ "id",
+ "mime",
+ "startTime",
+ "endTime",
+ "state",
+ "paused",
+ "error",
+ "incognito",
+ "bytesReceived",
+ "totalBytes",
+ "fileSize",
+ "exists",
+ ];
+ for (let field of SIMPLE_ITEMS) {
+ if (query[field] != null && item[field] != query[field]) {
+ return false;
+ }
+ }
+
+ return true;
+ };
+};
+
+const queryHelper = query => {
+ let matchFn;
+ try {
+ matchFn = downloadQuery(query);
+ } catch (err) {
+ return Promise.reject({ message: err.message });
+ }
+
+ let compareFn;
+ if (query.orderBy != null) {
+ const fields = query.orderBy.map(field =>
+ field[0] == "-"
+ ? { reverse: true, name: field.slice(1) }
+ : { reverse: false, name: field }
+ );
+
+ for (let field of fields) {
+ if (!DOWNLOAD_ITEM_FIELDS.includes(field.name)) {
+ return Promise.reject({
+ message: `Invalid orderBy field ${field.name}`,
+ });
+ }
+ }
+
+ compareFn = (dl1, dl2) => {
+ for (let field of fields) {
+ const val1 = dl1[field.name];
+ const val2 = dl2[field.name];
+
+ if (val1 < val2) {
+ return field.reverse ? 1 : -1;
+ } else if (val1 > val2) {
+ return field.reverse ? -1 : 1;
+ }
+ }
+ return 0;
+ };
+ }
+
+ return DownloadMap.getAll().then(downloads => {
+ if (compareFn) {
+ downloads = Array.from(downloads);
+ downloads.sort(compareFn);
+ }
+ let results = [];
+ for (let download of downloads) {
+ if (query.limit && results.length >= query.limit) {
+ break;
+ }
+ if (matchFn(download)) {
+ results.push(download);
+ }
+ }
+ return results;
+ });
+};
+
+function downloadEventManagerAPI(context, name, event, listener) {
+ let register = fire => {
+ const handler = (what, item) => {
+ if (context.privateBrowsingAllowed || !item.incognito) {
+ listener(fire, what, item);
+ }
+ };
+ let registerPromise = DownloadMap.getDownloadList().then(() => {
+ DownloadMap.on(event, handler);
+ });
+ return () => {
+ registerPromise.then(() => {
+ DownloadMap.off(event, handler);
+ });
+ };
+ };
+
+ return new EventManager({ context, name, register }).api();
+}
+
+this.downloads = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ return {
+ downloads: {
+ download(options) {
+ let { filename } = options;
+ if (filename && AppConstants.platform === "win") {
+ // cross platform javascript code uses "/"
+ filename = filename.replace(/\//g, "\\");
+ }
+
+ if (filename != null) {
+ if (!filename.length) {
+ return Promise.reject({ message: "filename must not be empty" });
+ }
+
+ let path = OS.Path.split(filename);
+ if (path.absolute) {
+ return Promise.reject({
+ message: "filename must not be an absolute path",
+ });
+ }
+
+ if (path.components.some(component => component == "..")) {
+ return Promise.reject({
+ message: "filename must not contain back-references (..)",
+ });
+ }
+
+ if (
+ path.components.some(component => {
+ let sanitized = DownloadPaths.sanitize(component, {
+ compressWhitespaces: false,
+ });
+ return component != sanitized;
+ })
+ ) {
+ return Promise.reject({
+ message: "filename must not contain illegal characters",
+ });
+ }
+ }
+
+ if (options.incognito && !context.privateBrowsingAllowed) {
+ return Promise.reject({
+ message: "private browsing access not allowed",
+ });
+ }
+
+ if (options.conflictAction == "prompt") {
+ // TODO
+ return Promise.reject({
+ message: "conflictAction prompt not yet implemented",
+ });
+ }
+
+ if (options.headers) {
+ for (let { name } of options.headers) {
+ if (
+ FORBIDDEN_HEADERS.includes(name.toUpperCase()) ||
+ name.match(FORBIDDEN_PREFIXES)
+ ) {
+ return Promise.reject({
+ message: "Forbidden request header name",
+ });
+ }
+ }
+ }
+
+ // Handle method, headers and body options.
+ function adjustChannel(channel) {
+ if (channel instanceof Ci.nsIHttpChannel) {
+ const method = options.method || "GET";
+ channel.requestMethod = method;
+
+ if (options.headers) {
+ for (let { name, value } of options.headers) {
+ if (name.toLowerCase() == "referer") {
+ // The referer header and referrerInfo object should always
+ // match. So if we want to set the header from privileged
+ // context, we should set referrerInfo. The referrer header
+ // will get set internally.
+ channel.setNewReferrerInfo(
+ value,
+ Ci.nsIReferrerInfo.UNSAFE_URL,
+ true
+ );
+ } else {
+ channel.setRequestHeader(name, value, false);
+ }
+ }
+ }
+
+ if (options.body != null) {
+ const stream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ stream.setData(options.body, options.body.length);
+
+ channel.QueryInterface(Ci.nsIUploadChannel2);
+ channel.explicitSetUploadStream(
+ stream,
+ null,
+ -1,
+ method,
+ false
+ );
+ }
+ }
+ return Promise.resolve();
+ }
+
+ function allowHttpStatus(download, status) {
+ const item = DownloadMap.byDownload.get(download);
+ if (item === null) {
+ return true;
+ }
+
+ let error = null;
+ switch (status) {
+ case 204: // No Content
+ case 205: // Reset Content
+ case 404: // Not Found
+ error = "SERVER_BAD_CONTENT";
+ break;
+
+ case 403: // Forbidden
+ error = "SERVER_FORBIDDEN";
+ break;
+
+ case 402: // Unauthorized
+ case 407: // Proxy authentication required
+ error = "SERVER_UNAUTHORIZED";
+ break;
+
+ default:
+ if (status >= 400) {
+ error = "SERVER_FAILED";
+ }
+ break;
+ }
+
+ if (error) {
+ item.error = error;
+ return false;
+ }
+
+ // No error, ergo allow the request.
+ return true;
+ }
+
+ async function createTarget(downloadsDir) {
+ if (!filename) {
+ let uri = Services.io.newURI(options.url);
+ if (uri instanceof Ci.nsIURL) {
+ filename = DownloadPaths.sanitize(
+ Services.textToSubURI.unEscapeURIForUI(uri.fileName)
+ );
+ }
+ }
+
+ let target = OS.Path.join(downloadsDir, filename || "download");
+
+ let saveAs;
+ if (options.saveAs !== null) {
+ saveAs = options.saveAs;
+ } else {
+ // If options.saveAs was not specified, only show the file chooser
+ // if |browser.download.useDownloadDir == false|. That is to say,
+ // only show the file chooser if Firefox normally shows it when
+ // a file is downloaded.
+ saveAs = !Services.prefs.getBoolPref(
+ PROMPTLESS_DOWNLOAD_PREF,
+ true
+ );
+ }
+
+ // Create any needed subdirectories if required by filename.
+ const dir = OS.Path.dirname(target);
+ await OS.File.makeDir(dir, { from: downloadsDir });
+
+ if (await OS.File.exists(target)) {
+ // This has a race, something else could come along and create
+ // the file between this test and them time the download code
+ // creates the target file. But we can't easily fix it without
+ // modifying DownloadCore so we live with it for now.
+ switch (options.conflictAction) {
+ case "uniquify":
+ default:
+ target = DownloadPaths.createNiceUniqueFile(
+ new FileUtils.File(target)
+ ).path;
+ if (saveAs) {
+ // createNiceUniqueFile actually creates the file, which
+ // is premature if we need to show a SaveAs dialog.
+ await OS.File.remove(target);
+ }
+ break;
+
+ case "overwrite":
+ break;
+ }
+ }
+
+ if (!saveAs || AppConstants.platform === "android") {
+ return target;
+ }
+
+ if (!("windowTracker" in global)) {
+ return target;
+ }
+
+ // At this point we are committed to displaying the file picker.
+ const downloadLastDir = new DownloadLastDir(
+ null,
+ options.incognito
+ );
+
+ async function getLastDirectory() {
+ return new Promise(resolve => {
+ downloadLastDir.getFileAsync(extension.baseURI, file => {
+ resolve(file);
+ });
+ });
+ }
+
+ function appendFilterForFileExtension(picker, ext) {
+ if (FILTER_HTML_EXTENSIONS.includes(ext)) {
+ picker.appendFilters(Ci.nsIFilePicker.filterHTML);
+ } else if (FILTER_TEXT_EXTENSIONS.includes(ext)) {
+ picker.appendFilters(Ci.nsIFilePicker.filterText);
+ } else if (FILTER_IMAGES_EXTENSIONS.includes(ext)) {
+ picker.appendFilters(Ci.nsIFilePicker.filterImages);
+ } else if (FILTER_XML_EXTENSIONS.includes(ext)) {
+ picker.appendFilters(Ci.nsIFilePicker.filterXML);
+ } else if (FILTER_AUDIO_EXTENSIONS.includes(ext)) {
+ picker.appendFilters(Ci.nsIFilePicker.filterAudio);
+ } else if (FILTER_VIDEO_EXTENSIONS.includes(ext)) {
+ picker.appendFilters(Ci.nsIFilePicker.filterVideo);
+ }
+ }
+
+ function saveLastDirectory(lastDir) {
+ downloadLastDir.setFile(extension.baseURI, lastDir);
+ }
+
+ // Use windowTracker to find a window, rather than Services.wm,
+ // so that this doesn't break where navigator:browser isn't the
+ // main window (e.g. Thunderbird).
+ const window = global.windowTracker.getTopWindow().window;
+ const basename = OS.Path.basename(target);
+ const ext = basename.match(/\.([^.]+)$/)?.[1];
+
+ // If the filename passed in by the extension is a simple name
+ // and not a path, we open the file picker so it displays the
+ // last directory that was chosen by the user.
+ const pathSep = AppConstants.platform === "win" ? "\\" : "/";
+ const lastFilePickerDirectory =
+ !filename || !filename.includes(pathSep)
+ ? await getLastDirectory()
+ : undefined;
+
+ // Setup the file picker Save As dialog.
+ const picker = Cc["@mozilla.org/filepicker;1"].createInstance(
+ Ci.nsIFilePicker
+ );
+ picker.init(window, null, Ci.nsIFilePicker.modeSave);
+ if (lastFilePickerDirectory) {
+ picker.displayDirectory = lastFilePickerDirectory;
+ } else {
+ picker.displayDirectory = new FileUtils.File(dir);
+ }
+ picker.defaultString = basename;
+ if (ext) {
+ // Configure a default file extension, used as fallback on Windows.
+ picker.defaultExtension = ext;
+ appendFilterForFileExtension(picker, ext);
+ }
+ picker.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ // Open the dialog and resolve/reject with the result.
+ return new Promise((resolve, reject) => {
+ picker.open(result => {
+ if (result === Ci.nsIFilePicker.returnCancel) {
+ reject({ message: "Download canceled by the user" });
+ } else {
+ saveLastDirectory(picker.file.parent);
+ resolve(picker.file.path);
+ }
+ });
+ });
+ }
+
+ let download;
+ return Downloads.getPreferredDownloadsDirectory()
+ .then(downloadsDir => createTarget(downloadsDir))
+ .then(target => {
+ const source = {
+ url: options.url,
+ isPrivate: options.incognito,
+ // Use the extension's principal to allow extensions to observe
+ // their own downloads via the webRequest API.
+ loadingPrincipal: context.principal,
+ };
+
+ // blob:-URLs can only be loaded by the principal with which they
+ // are associated. This principal may have origin attributes.
+ // `context.principal` does sometimes not have these attributes
+ // due to bug 1653681. If `context.principal` were to be passed,
+ // the download request would be rejected because of mismatching
+ // principals (origin attributes).
+ // TODO bug 1653681: fix context.principal and remove this.
+ if (options.url.startsWith("blob:")) {
+ // To make sure that the blob:-URL can be loaded, fall back to
+ // the default (system) principal instead.
+ delete source.loadingPrincipal;
+ }
+
+ // Unless the API user explicitly wants errors ignored,
+ // set the allowHttpStatus callback, which will instruct
+ // DownloadCore to cancel downloads on HTTP errors.
+ if (!options.allowHttpErrors) {
+ source.allowHttpStatus = allowHttpStatus;
+ }
+
+ if (options.method || options.headers || options.body) {
+ source.adjustChannel = adjustChannel;
+ }
+
+ return Downloads.createDownload({
+ source,
+ target: {
+ path: target,
+ partFilePath: target + ".part",
+ },
+ });
+ })
+ .then(dl => {
+ download = dl;
+ return DownloadMap.getDownloadList();
+ })
+ .then(list => {
+ const item = DownloadMap.newFromDownload(download, extension);
+ list.add(download);
+
+ // This is necessary to make pause/resume work.
+ download.tryToKeepPartialData = true;
+
+ // Do not handle errors.
+ // Extensions will use listeners to be informed about errors.
+ // Just ignore any errors from |start()| to avoid spamming the
+ // error console.
+ download.start().catch(e => {
+ if (e.name !== "DownloadError") {
+ Cu.reportError(e);
+ }
+ });
+
+ return item.id;
+ });
+ },
+
+ removeFile(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
+ } catch (err) {
+ return Promise.reject({ message: `Invalid download id ${id}` });
+ }
+ if (item.state !== "complete") {
+ return Promise.reject({
+ message: `Cannot remove incomplete download id ${id}`,
+ });
+ }
+ return OS.File.remove(item.filename, { ignoreAbsent: false }).catch(
+ err => {
+ return Promise.reject({
+ message: `Could not remove download id ${item.id} because the file doesn't exist`,
+ });
+ }
+ );
+ });
+ },
+
+ search(query) {
+ if (!context.privateBrowsingAllowed) {
+ query.incognito = false;
+ }
+ return queryHelper(query).then(items =>
+ items.map(item => item.serialize())
+ );
+ },
+
+ pause(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
+ } catch (err) {
+ return Promise.reject({ message: `Invalid download id ${id}` });
+ }
+ if (item.state != "in_progress") {
+ return Promise.reject({
+ message: `Download ${id} cannot be paused since it is in state ${item.state}`,
+ });
+ }
+
+ return item.download.cancel();
+ });
+ },
+
+ resume(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
+ } catch (err) {
+ return Promise.reject({ message: `Invalid download id ${id}` });
+ }
+ if (!item.canResume) {
+ return Promise.reject({
+ message: `Download ${id} cannot be resumed`,
+ });
+ }
+
+ item.error = null;
+ return item.download.start();
+ });
+ },
+
+ cancel(id) {
+ return DownloadMap.lazyInit().then(() => {
+ let item;
+ try {
+ item = DownloadMap.fromId(id, context.privateBrowsingAllowed);
+ } catch (err) {
+ return Promise.reject({ message: `Invalid download id ${id}` });
+ }
+ if (item.download.succeeded) {
+ return Promise.reject({
+ message: `Download ${id} is already complete`,
+ });
+ }
+ return item.download.finalize(true);
+ });
+ },
+
+ showDefaultFolder() {
+ Downloads.getPreferredDownloadsDirectory()
+ .then(dir => {
+ let dirobj = new FileUtils.File(dir);
+ if (dirobj.isDirectory()) {
+ dirobj.launch();
+ } else {
+ throw new Error(
+ `Download directory ${dirobj.path} is not actually a directory`
+ );
+ }
+ })
+ .catch(Cu.reportError);
+ },
+
+ erase(query) {
+ if (!context.privateBrowsingAllowed) {
+ query.incognito = false;
+ }
+ return queryHelper(query).then(items => {
+ let results = [];
+ let promises = [];
+ for (let item of items) {
+ promises.push(DownloadMap.erase(item));
+ results.push(item.id);
+ }
+ return Promise.all(promises).then(() => results);
+ });
+ },
+
+ open(downloadId) {
+ return DownloadMap.lazyInit()
+ .then(() => {
+ let download = DownloadMap.fromId(
+ downloadId,
+ context.privateBrowsingAllowed
+ ).download;
+ if (download.succeeded) {
+ return download.launch();
+ }
+ return Promise.reject({ message: "Download has not completed." });
+ })
+ .catch(error => {
+ return Promise.reject({ message: error.message });
+ });
+ },
+
+ show(downloadId) {
+ return DownloadMap.lazyInit()
+ .then(() => {
+ let download = DownloadMap.fromId(
+ downloadId,
+ context.privateBrowsingAllowed
+ );
+ return download.download.showContainingDirectory();
+ })
+ .then(() => {
+ return true;
+ })
+ .catch(error => {
+ return Promise.reject({ message: error.message });
+ });
+ },
+
+ getFileIcon(downloadId, options) {
+ return DownloadMap.lazyInit()
+ .then(() => {
+ let size = options && options.size ? options.size : 32;
+ let download = DownloadMap.fromId(
+ downloadId,
+ context.privateBrowsingAllowed
+ ).download;
+ let pathPrefix = "";
+ let path;
+
+ if (download.succeeded) {
+ let file = FileUtils.File(download.target.path);
+ path = Services.io.newFileURI(file).spec;
+ } else {
+ path = OS.Path.basename(download.target.path);
+ pathPrefix = "//";
+ }
+
+ return new Promise((resolve, reject) => {
+ let chromeWebNav = Services.appShell.createWindowlessBrowser(
+ true
+ );
+ let system = Services.scriptSecurityManager.getSystemPrincipal();
+ chromeWebNav.docShell.createAboutBlankContentViewer(
+ system,
+ system
+ );
+
+ let img = chromeWebNav.document.createElement("img");
+ img.width = size;
+ img.height = size;
+
+ let handleLoad;
+ let handleError;
+ const cleanup = () => {
+ img.removeEventListener("load", handleLoad);
+ img.removeEventListener("error", handleError);
+ chromeWebNav.close();
+ chromeWebNav = null;
+ };
+
+ handleLoad = () => {
+ let canvas = chromeWebNav.document.createElement("canvas");
+ canvas.width = size;
+ canvas.height = size;
+ let context = canvas.getContext("2d");
+ context.drawImage(img, 0, 0, size, size);
+ let dataURL = canvas.toDataURL("image/png");
+ cleanup();
+ resolve(dataURL);
+ };
+
+ handleError = error => {
+ Cu.reportError(error);
+ cleanup();
+ reject(new Error("An unexpected error occurred"));
+ };
+
+ img.addEventListener("load", handleLoad);
+ img.addEventListener("error", handleError);
+ img.src = `moz-icon:${pathPrefix}${path}?size=${size}`;
+ });
+ })
+ .catch(error => {
+ return Promise.reject({ message: error.message });
+ });
+ },
+
+ // When we do setShelfEnabled(), check for additional "downloads.shelf" permission.
+ // i.e.:
+ // setShelfEnabled(enabled) {
+ // if (!extension.hasPermission("downloads.shelf")) {
+ // throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing.");
+ // }
+ // ...
+ // }
+
+ onChanged: downloadEventManagerAPI(
+ context,
+ "downloads.onChanged",
+ "change",
+ (fire, what, item) => {
+ let changes = {};
+ const noundef = val => (val === undefined ? null : val);
+ DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
+ if (item[fld] != item.prechange[fld]) {
+ changes[fld] = {
+ previous: noundef(item.prechange[fld]),
+ current: noundef(item[fld]),
+ };
+ }
+ });
+ if (Object.keys(changes).length) {
+ changes.id = item.id;
+ fire.async(changes);
+ }
+ }
+ ),
+
+ onCreated: downloadEventManagerAPI(
+ context,
+ "downloads.onCreated",
+ "create",
+ (fire, what, item) => {
+ fire.async(item.serialize());
+ }
+ ),
+
+ onErased: downloadEventManagerAPI(
+ context,
+ "downloads.onErased",
+ "erase",
+ (fire, what, item) => {
+ fire.async(item.id);
+ }
+ ),
+
+ onDeterminingFilename: ignoreEvent(
+ context,
+ "downloads.onDeterminingFilename"
+ ),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-extension.js b/toolkit/components/extensions/parent/ext-extension.js
new file mode 100644
index 0000000000..2f0a168dd4
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-extension.js
@@ -0,0 +1,25 @@
+/* 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.extension = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ extension: {
+ get lastError() {
+ return context.lastError;
+ },
+
+ isAllowedIncognitoAccess() {
+ return context.privateBrowsingAllowed;
+ },
+
+ isAllowedFileSchemeAccess() {
+ return false;
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-geckoProfiler.js b/toolkit/components/extensions/parent/ext-geckoProfiler.js
new file mode 100644
index 0000000000..7fef0b9ee8
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-geckoProfiler.js
@@ -0,0 +1,227 @@
+/* -*- 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";
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+ChromeUtils.defineModuleGetter(
+ this,
+ "ProfilerGetSymbols",
+ "resource://gre/modules/ProfilerGetSymbols.jsm"
+);
+
+const PREF_ASYNC_STACK = "javascript.options.asyncstack";
+
+const ASYNC_STACKS_ENABLED = Services.prefs.getBoolPref(
+ PREF_ASYNC_STACK,
+ false
+);
+
+var { ExtensionError } = ExtensionUtils;
+
+const symbolCache = new Map();
+
+const primeSymbolStore = libs => {
+ for (const { path, debugName, debugPath, breakpadId } of libs) {
+ symbolCache.set(`${debugName}/${breakpadId}`, { path, debugPath });
+ }
+};
+
+const isRunningObserver = {
+ _observers: new Set(),
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "profiler-started":
+ case "profiler-stopped":
+ // Call observer(false) or observer(true), but do it through a promise
+ // so that it's asynchronous.
+ // We don't want it to be synchronous because of the observer call in
+ // addObserver, which is asynchronous, and we want to get the ordering
+ // right.
+ const isRunningPromise = Promise.resolve(topic === "profiler-started");
+ for (let observer of this._observers) {
+ isRunningPromise.then(observer);
+ }
+ break;
+ }
+ },
+
+ _startListening() {
+ Services.obs.addObserver(this, "profiler-started");
+ Services.obs.addObserver(this, "profiler-stopped");
+ },
+
+ _stopListening() {
+ Services.obs.removeObserver(this, "profiler-started");
+ Services.obs.removeObserver(this, "profiler-stopped");
+ },
+
+ addObserver(observer) {
+ if (this._observers.size === 0) {
+ this._startListening();
+ }
+
+ this._observers.add(observer);
+ observer(Services.profiler.IsActive());
+ },
+
+ removeObserver(observer) {
+ if (this._observers.delete(observer) && this._observers.size === 0) {
+ this._stopListening();
+ }
+ },
+};
+
+this.geckoProfiler = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ geckoProfiler: {
+ async start(options) {
+ const {
+ bufferSize,
+ windowLength,
+ interval,
+ features,
+ threads,
+ } = options;
+
+ Services.prefs.setBoolPref(PREF_ASYNC_STACK, false);
+ if (threads) {
+ Services.profiler.StartProfiler(
+ bufferSize,
+ interval,
+ features,
+ threads,
+ 0,
+ windowLength
+ );
+ } else {
+ Services.profiler.StartProfiler(
+ bufferSize,
+ interval,
+ features,
+ [],
+ 0,
+ windowLength
+ );
+ }
+ },
+
+ async stop() {
+ if (ASYNC_STACKS_ENABLED !== null) {
+ Services.prefs.setBoolPref(PREF_ASYNC_STACK, ASYNC_STACKS_ENABLED);
+ }
+
+ Services.profiler.StopProfiler();
+ },
+
+ async pause() {
+ Services.profiler.Pause();
+ },
+
+ async resume() {
+ Services.profiler.Resume();
+ },
+
+ async dumpProfileToFile(fileName) {
+ if (!Services.profiler.IsActive()) {
+ throw new ExtensionError(
+ "The profiler is stopped. " +
+ "You need to start the profiler before you can capture a profile."
+ );
+ }
+
+ if (fileName.includes("\\") || fileName.includes("/")) {
+ throw new ExtensionError("Path cannot contain a subdirectory.");
+ }
+
+ let fragments = [OS.Constants.Path.profileDir, "profiler", fileName];
+ let filePath = OS.Path.join(...fragments);
+
+ try {
+ await Services.profiler.dumpProfileToFileAsync(filePath);
+ } catch (e) {
+ Cu.reportError(e);
+ throw new ExtensionError(`Dumping profile to ${filePath} failed.`);
+ }
+ },
+
+ async getProfile() {
+ if (!Services.profiler.IsActive()) {
+ throw new ExtensionError(
+ "The profiler is stopped. " +
+ "You need to start the profiler before you can capture a profile."
+ );
+ }
+
+ return Services.profiler.getProfileDataAsync();
+ },
+
+ async getProfileAsArrayBuffer() {
+ if (!Services.profiler.IsActive()) {
+ throw new ExtensionError(
+ "The profiler is stopped. " +
+ "You need to start the profiler before you can capture a profile."
+ );
+ }
+
+ return Services.profiler.getProfileDataAsArrayBuffer();
+ },
+
+ async getProfileAsGzippedArrayBuffer() {
+ if (!Services.profiler.IsActive()) {
+ throw new ExtensionError(
+ "The profiler is stopped. " +
+ "You need to start the profiler before you can capture a profile."
+ );
+ }
+
+ return Services.profiler.getProfileDataAsGzippedArrayBuffer();
+ },
+
+ async getSymbols(debugName, breakpadId) {
+ if (symbolCache.size === 0) {
+ primeSymbolStore(Services.profiler.sharedLibraries);
+ }
+
+ const cachedLibInfo = symbolCache.get(`${debugName}/${breakpadId}`);
+ if (!cachedLibInfo) {
+ throw new Error(
+ `The library ${debugName} ${breakpadId} is not in the Services.profiler.sharedLibraries list, ` +
+ "so the local path for it is not known and symbols for it can not be obtained. " +
+ "This usually happens if a content process uses a library that's not used in the parent " +
+ "process - Services.profiler.sharedLibraries only knows about libraries in the parent process."
+ );
+ }
+
+ const { path, debugPath } = cachedLibInfo;
+ if (!OS.Path.split(path).absolute) {
+ throw new Error(
+ `Services.profiler.sharedLibraries did not contain an absolute path for the library ${debugName} ${breakpadId}, ` +
+ "so symbols for this library can not be obtained."
+ );
+ }
+
+ return ProfilerGetSymbols.getSymbolTable(path, debugPath, breakpadId);
+ },
+
+ onRunning: new EventManager({
+ context,
+ name: "geckoProfiler.onRunning",
+ register: fire => {
+ isRunningObserver.addObserver(fire.async);
+ return () => {
+ isRunningObserver.removeObserver(fire.async);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-i18n.js b/toolkit/components/extensions/parent/ext-i18n.js
new file mode 100644
index 0000000000..72ffc5b869
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-i18n.js
@@ -0,0 +1,47 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "LanguageDetector",
+ "resource:///modules/translation/LanguageDetector.jsm"
+);
+
+this.i18n = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ return {
+ i18n: {
+ getMessage: function(messageName, substitutions) {
+ return extension.localizeMessage(messageName, substitutions, {
+ cloneScope: context.cloneScope,
+ });
+ },
+
+ getAcceptLanguages: function() {
+ let result = extension.localeData.acceptLanguages;
+ return Promise.resolve(result);
+ },
+
+ getUILanguage: function() {
+ return extension.localeData.uiLocale;
+ },
+
+ detectLanguage: function(text) {
+ return LanguageDetector.detectLanguage(text).then(result => ({
+ isReliable: result.confident,
+ languages: result.languages.map(lang => {
+ return {
+ language: lang.languageCode,
+ percentage: lang.percent,
+ };
+ }),
+ }));
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-identity.js b/toolkit/components/extensions/parent/ext-identity.js
new file mode 100644
index 0000000000..68dea736e6
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-identity.js
@@ -0,0 +1,158 @@
+/* -*- 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["XMLHttpRequest", "ChannelWrapper"]);
+
+var { promiseDocumentLoaded } = ExtensionUtils;
+
+const checkRedirected = (url, redirectURI) => {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", url);
+ // We expect this if the user has not authenticated.
+ xhr.onload = () => {
+ reject(0);
+ };
+ // An unexpected error happened, log for extension authors.
+ xhr.onerror = () => {
+ reject(xhr.status);
+ };
+ // Catch redirect to our redirect_uri before a new request is made.
+ xhr.channel.notificationCallbacks = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIChannelEventSync",
+ ]),
+
+ getInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]),
+
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ let responseURL = newChannel.URI.spec;
+ if (responseURL.startsWith(redirectURI)) {
+ resolve(responseURL);
+ // Cancel the redirect.
+ callback.onRedirectVerifyCallback(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ },
+ };
+ xhr.send();
+ });
+};
+
+const openOAuthWindow = (details, redirectURI) => {
+ let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ let supportsStringPrefURL = Cc[
+ "@mozilla.org/supports-string;1"
+ ].createInstance(Ci.nsISupportsString);
+ supportsStringPrefURL.data = details.url;
+ args.appendElement(supportsStringPrefURL);
+
+ let window = Services.ww.openWindow(
+ null,
+ AppConstants.BROWSER_CHROME_URL,
+ "launchWebAuthFlow_dialog",
+ "chrome,location=yes,centerscreen,dialog=no,resizable=yes,scrollbars=yes",
+ args
+ );
+
+ return new Promise((resolve, reject) => {
+ let httpActivityDistributor = Cc[
+ "@mozilla.org/network/http-activity-distributor;1"
+ ].getService(Ci.nsIHttpActivityDistributor);
+
+ let unloadListener;
+ let httpObserver;
+
+ const resolveIfRedirectURI = channel => {
+ const url = channel.URI && channel.URI.spec;
+ if (!url || !url.startsWith(redirectURI)) {
+ return;
+ }
+
+ // Early exit if channel isn't related to the oauth dialog.
+ let wrapper = ChannelWrapper.get(channel);
+ if (
+ !wrapper.browserElement &&
+ wrapper.browserElement !== window.gBrowser.selectedBrowser
+ ) {
+ return;
+ }
+
+ wrapper.cancel(Cr.NS_ERROR_ABORT, Ci.nsILoadInfo.BLOCKING_REASON_NONE);
+ window.gBrowser.webNavigation.stop(Ci.nsIWebNavigation.STOP_ALL);
+ window.removeEventListener("unload", unloadListener);
+ httpActivityDistributor.removeObserver(httpObserver);
+ window.close();
+ resolve(url);
+ };
+
+ httpObserver = {
+ observeActivity(channel, type, subtype, timestamp, sizeData, stringData) {
+ try {
+ channel.QueryInterface(Ci.nsIChannel);
+ } catch {
+ // Ignore activities for channels that doesn't implement nsIChannel
+ // (e.g. a NullHttpChannel).
+ return;
+ }
+
+ resolveIfRedirectURI(channel);
+ },
+ };
+
+ httpActivityDistributor.addObserver(httpObserver);
+
+ // If the user just closes the window we need to reject
+ unloadListener = () => {
+ window.removeEventListener("unload", unloadListener);
+ httpActivityDistributor.removeObserver(httpObserver);
+ reject({ message: "User cancelled or denied access." });
+ };
+
+ promiseDocumentLoaded(window.document).then(() => {
+ window.addEventListener("unload", unloadListener);
+ });
+ });
+};
+
+this.identity = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ identity: {
+ launchWebAuthFlowInParent: function(details, redirectURI) {
+ // If the request is automatically redirected the user has already
+ // authorized and we do not want to show the window.
+ return checkRedirected(details.url, redirectURI).catch(
+ requestError => {
+ // requestError is zero or xhr.status
+ if (requestError !== 0) {
+ Cu.reportError(
+ `browser.identity auth check failed with ${requestError}`
+ );
+ return Promise.reject({ message: "Invalid request" });
+ }
+ if (!details.interactive) {
+ return Promise.reject({ message: `Requires user interaction` });
+ }
+
+ return openOAuthWindow(details, redirectURI);
+ }
+ );
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-idle.js b/toolkit/components/extensions/parent/ext-idle.js
new file mode 100644
index 0000000000..ee0c0da374
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-idle.js
@@ -0,0 +1,97 @@
+/* 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";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "idleService",
+ "@mozilla.org/widget/useridleservice;1",
+ "nsIUserIdleService"
+);
+
+// WeakMap[Extension -> Object]
+let observersMap = new WeakMap();
+
+const getIdleObserverInfo = (extension, context) => {
+ let observerInfo = observersMap.get(extension);
+ if (!observerInfo) {
+ observerInfo = {
+ observer: null,
+ detectionInterval: 60,
+ };
+ observersMap.set(extension, observerInfo);
+ context.callOnClose({
+ close: () => {
+ let { observer, detectionInterval } = observersMap.get(extension);
+ if (observer) {
+ idleService.removeIdleObserver(observer, detectionInterval);
+ }
+ observersMap.delete(extension);
+ },
+ });
+ }
+ return observerInfo;
+};
+
+const getIdleObserver = (extension, context) => {
+ let observerInfo = getIdleObserverInfo(extension, context);
+ let { observer, detectionInterval } = observerInfo;
+ if (!observer) {
+ observer = new (class extends ExtensionCommon.EventEmitter {
+ observe(subject, topic, data) {
+ if (topic == "idle" || topic == "active") {
+ this.emit("stateChanged", topic);
+ }
+ }
+ })();
+ idleService.addIdleObserver(observer, detectionInterval);
+ observerInfo.observer = observer;
+ observerInfo.detectionInterval = detectionInterval;
+ }
+ return observer;
+};
+
+const setDetectionInterval = (extension, context, newInterval) => {
+ let observerInfo = getIdleObserverInfo(extension, context);
+ let { observer, detectionInterval } = observerInfo;
+ if (observer) {
+ idleService.removeIdleObserver(observer, detectionInterval);
+ idleService.addIdleObserver(observer, newInterval);
+ }
+ observerInfo.detectionInterval = newInterval;
+};
+
+this.idle = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ return {
+ idle: {
+ queryState: function(detectionIntervalInSeconds) {
+ if (idleService.idleTime < detectionIntervalInSeconds * 1000) {
+ return Promise.resolve("active");
+ }
+ return Promise.resolve("idle");
+ },
+ setDetectionInterval: function(detectionIntervalInSeconds) {
+ setDetectionInterval(extension, context, detectionIntervalInSeconds);
+ },
+ onStateChanged: new EventManager({
+ context,
+ name: "idle.onStateChanged",
+ register: fire => {
+ let listener = (event, data) => {
+ fire.sync(data);
+ };
+
+ getIdleObserver(extension, context).on("stateChanged", listener);
+ return () => {
+ getIdleObserver(extension, context).off("stateChanged", listener);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-management.js b/toolkit/components/extensions/parent/ext-management.js
new file mode 100644
index 0000000000..afc01b558b
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-management.js
@@ -0,0 +1,381 @@
+/* -*- 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";
+
+XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
+ return Services.strings.createBundle(
+ "chrome://global/locale/extensions.properties"
+ );
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManager",
+ "resource://gre/modules/AddonManager.jsm"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "promptService",
+ "@mozilla.org/embedcomp/prompt-service;1",
+ "nsIPromptService"
+);
+
+XPCOMUtils.defineLazyGetter(this, "GlobalManager", () => {
+ const { GlobalManager } = ChromeUtils.import(
+ "resource://gre/modules/Extension.jsm",
+ null
+ );
+ return GlobalManager;
+});
+
+var { ExtensionError } = ExtensionUtils;
+
+const _ = (key, ...args) => {
+ if (args.length) {
+ return strBundle.formatStringFromName(key, args);
+ }
+ return strBundle.GetStringFromName(key);
+};
+
+const installType = addon => {
+ if (addon.temporarilyInstalled) {
+ return "development";
+ } else if (addon.foreignInstall) {
+ return "sideload";
+ } else if (addon.isSystem) {
+ return "other";
+ }
+ return "normal";
+};
+
+const getExtensionInfoForAddon = (extension, addon) => {
+ let extInfo = {
+ id: addon.id,
+ name: addon.name,
+ description: addon.description || "",
+ version: addon.version,
+ mayDisable: !!(addon.permissions & AddonManager.PERM_CAN_DISABLE),
+ enabled: addon.isActive,
+ optionsUrl: addon.optionsURL || "",
+ installType: installType(addon),
+ type: addon.type,
+ };
+
+ if (extension) {
+ let m = extension.manifest;
+
+ let hostPerms = extension.allowedOrigins.patterns.map(
+ matcher => matcher.pattern
+ );
+
+ extInfo.permissions = Array.from(extension.permissions).filter(perm => {
+ return !hostPerms.includes(perm);
+ });
+ extInfo.hostPermissions = hostPerms;
+
+ extInfo.shortName = m.short_name || "";
+ if (m.icons) {
+ extInfo.icons = Object.keys(m.icons).map(key => {
+ return { size: Number(key), url: m.icons[key] };
+ });
+ }
+ }
+
+ if (!addon.isActive) {
+ extInfo.disabledReason = "unknown";
+ }
+ if (addon.homepageURL) {
+ extInfo.homepageUrl = addon.homepageURL;
+ }
+ if (addon.updateURL) {
+ extInfo.updateUrl = addon.updateURL;
+ }
+ return extInfo;
+};
+
+const listenerMap = new WeakMap();
+// Some management APIs are intentionally limited.
+const allowedTypes = ["theme", "extension"];
+
+function checkAllowedAddon(addon) {
+ if (addon.isSystem || addon.isAPIExtension) {
+ return false;
+ }
+ if (addon.type == "extension" && !addon.isWebExtension) {
+ return false;
+ }
+ return allowedTypes.includes(addon.type);
+}
+
+class AddonListener extends ExtensionCommon.EventEmitter {
+ constructor() {
+ super();
+ AddonManager.addAddonListener(this);
+ }
+
+ release() {
+ AddonManager.removeAddonListener(this);
+ }
+
+ getExtensionInfo(addon) {
+ let ext = addon.isWebExtension && GlobalManager.extensionMap.get(addon.id);
+ return getExtensionInfoForAddon(ext, addon);
+ }
+
+ onEnabled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onEnabled", this.getExtensionInfo(addon));
+ }
+
+ onDisabled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onDisabled", this.getExtensionInfo(addon));
+ }
+
+ onInstalled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onInstalled", this.getExtensionInfo(addon));
+ }
+
+ onUninstalled(addon) {
+ if (!checkAllowedAddon(addon)) {
+ return;
+ }
+ this.emit("onUninstalled", this.getExtensionInfo(addon));
+ }
+}
+
+let addonListener;
+
+const getManagementListener = (extension, context) => {
+ if (!listenerMap.has(extension)) {
+ if (!addonListener) {
+ addonListener = new AddonListener();
+ }
+ listenerMap.set(extension, {});
+ context.callOnClose({
+ close: () => {
+ listenerMap.delete(extension);
+ if (listenerMap.length === 0) {
+ addonListener.release();
+ addonListener = null;
+ }
+ },
+ });
+ }
+ return addonListener;
+};
+
+this.management = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ return {
+ management: {
+ async get(id) {
+ let addon = await AddonManager.getAddonByID(id);
+ if (!addon) {
+ throw new ExtensionError(`No such addon ${id}`);
+ }
+ if (!checkAllowedAddon(addon)) {
+ throw new ExtensionError("get not allowed for this addon");
+ }
+ // If the extension is enabled get it and use it for more data.
+ let ext = GlobalManager.extensionMap.get(addon.id);
+ return getExtensionInfoForAddon(ext, addon);
+ },
+
+ async getAll() {
+ let addons = await AddonManager.getAddonsByTypes(allowedTypes);
+ return addons.filter(checkAllowedAddon).map(addon => {
+ // If the extension is enabled get it and use it for more data.
+ let ext = GlobalManager.extensionMap.get(addon.id);
+ return getExtensionInfoForAddon(ext, addon);
+ });
+ },
+
+ async install({ url, hash }) {
+ let listener = {
+ onDownloadEnded(install) {
+ if (install.addon.appDisabled || install.addon.type !== "theme") {
+ install.cancel();
+ return false;
+ }
+ },
+ };
+
+ let telemetryInfo = {
+ source: "extension",
+ method: "management-webext-api",
+ };
+ let install = await AddonManager.getInstallForURL(url, {
+ hash,
+ telemetryInfo,
+ triggeringPrincipal: extension.principal,
+ });
+ install.addListener(listener);
+ try {
+ await install.install();
+ } catch (e) {
+ Cu.reportError(e);
+ throw new ExtensionError("Incompatible addon");
+ }
+ await install.addon.enable();
+ return { id: install.addon.id };
+ },
+
+ async getSelf() {
+ let addon = await AddonManager.getAddonByID(extension.id);
+ return getExtensionInfoForAddon(extension, addon);
+ },
+
+ async uninstallSelf(options) {
+ if (options && options.showConfirmDialog) {
+ let message = _("uninstall.confirmation.message", extension.name);
+ if (options.dialogMessage) {
+ message = `${options.dialogMessage}\n${message}`;
+ }
+ let title = _("uninstall.confirmation.title", extension.name);
+ let buttonFlags =
+ promptService.BUTTON_POS_0 *
+ promptService.BUTTON_TITLE_IS_STRING +
+ promptService.BUTTON_POS_1 * promptService.BUTTON_TITLE_IS_STRING;
+ let button0Title = _("uninstall.confirmation.button-0.label");
+ let button1Title = _("uninstall.confirmation.button-1.label");
+ let response = promptService.confirmEx(
+ null,
+ title,
+ message,
+ buttonFlags,
+ button0Title,
+ button1Title,
+ null,
+ null,
+ { value: 0 }
+ );
+ if (response == 1) {
+ throw new ExtensionError("User cancelled uninstall of extension");
+ }
+ }
+ let addon = await AddonManager.getAddonByID(extension.id);
+ let canUninstall = Boolean(
+ addon.permissions & AddonManager.PERM_CAN_UNINSTALL
+ );
+ if (!canUninstall) {
+ throw new ExtensionError("The add-on cannot be uninstalled");
+ }
+ addon.uninstall();
+ },
+
+ async setEnabled(id, enabled) {
+ let addon = await AddonManager.getAddonByID(id);
+ if (!addon) {
+ throw new ExtensionError(`No such addon ${id}`);
+ }
+ if (addon.type !== "theme") {
+ throw new ExtensionError("setEnabled applies only to theme addons");
+ }
+ if (addon.isSystem) {
+ throw new ExtensionError(
+ "setEnabled cannot be used with a system addon"
+ );
+ }
+ if (enabled) {
+ await addon.enable();
+ } else {
+ await addon.disable();
+ }
+ },
+
+ onDisabled: new EventManager({
+ context,
+ name: "management.onDisabled",
+ register: fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getManagementListener(extension, context).on(
+ "onDisabled",
+ listener
+ );
+ return () => {
+ getManagementListener(extension, context).off(
+ "onDisabled",
+ listener
+ );
+ };
+ },
+ }).api(),
+
+ onEnabled: new EventManager({
+ context,
+ name: "management.onEnabled",
+ register: fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getManagementListener(extension, context).on("onEnabled", listener);
+ return () => {
+ getManagementListener(extension, context).off(
+ "onEnabled",
+ listener
+ );
+ };
+ },
+ }).api(),
+
+ onInstalled: new EventManager({
+ context,
+ name: "management.onInstalled",
+ register: fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getManagementListener(extension, context).on(
+ "onInstalled",
+ listener
+ );
+ return () => {
+ getManagementListener(extension, context).off(
+ "onInstalled",
+ listener
+ );
+ };
+ },
+ }).api(),
+
+ onUninstalled: new EventManager({
+ context,
+ name: "management.onUninstalled",
+ register: fire => {
+ let listener = (event, data) => {
+ fire.async(data);
+ };
+
+ getManagementListener(extension, context).on(
+ "onUninstalled",
+ listener
+ );
+ return () => {
+ getManagementListener(extension, context).off(
+ "onUninstalled",
+ listener
+ );
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-networkStatus.js b/toolkit/components/extensions/parent/ext-networkStatus.js
new file mode 100644
index 0000000000..e0733f9819
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-networkStatus.js
@@ -0,0 +1,89 @@
+/* -*- 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";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gNetworkLinkService",
+ "@mozilla.org/network/network-link-service;1",
+ "nsINetworkLinkService"
+);
+
+function getLinkType() {
+ switch (gNetworkLinkService.linkType) {
+ case gNetworkLinkService.LINK_TYPE_UNKNOWN:
+ return "unknown";
+ case gNetworkLinkService.LINK_TYPE_ETHERNET:
+ return "ethernet";
+ case gNetworkLinkService.LINK_TYPE_USB:
+ return "usb";
+ case gNetworkLinkService.LINK_TYPE_WIFI:
+ return "wifi";
+ case gNetworkLinkService.LINK_TYPE_WIMAX:
+ return "wimax";
+ case gNetworkLinkService.LINK_TYPE_2G:
+ return "2g";
+ case gNetworkLinkService.LINK_TYPE_3G:
+ return "3g";
+ case gNetworkLinkService.LINK_TYPE_4G:
+ return "4g";
+ default:
+ return "unknown";
+ }
+}
+
+function getLinkStatus() {
+ if (!gNetworkLinkService.linkStatusKnown) {
+ return "unknown";
+ }
+ return gNetworkLinkService.isLinkUp ? "up" : "down";
+}
+
+function getLinkInfo() {
+ return {
+ id: gNetworkLinkService.networkID || undefined,
+ status: getLinkStatus(),
+ type: getLinkType(),
+ };
+}
+
+this.networkStatus = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ networkStatus: {
+ getLinkInfo,
+ onConnectionChanged: new EventManager({
+ context,
+ name: "networkStatus.onConnectionChanged",
+ register: fire => {
+ let observerStatus = (subject, topic, data) => {
+ fire.async(getLinkInfo());
+ };
+
+ Services.obs.addObserver(
+ observerStatus,
+ "network:link-status-changed"
+ );
+ Services.obs.addObserver(
+ observerStatus,
+ "network:link-type-changed"
+ );
+ return () => {
+ Services.obs.removeObserver(
+ observerStatus,
+ "network:link-status-changed"
+ );
+ Services.obs.removeObserver(
+ observerStatus,
+ "network:link-type-changed"
+ );
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-notifications.js b/toolkit/components/extensions/parent/ext-notifications.js
new file mode 100644
index 0000000000..68c50d2c43
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-notifications.js
@@ -0,0 +1,190 @@
+/* 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";
+
+const ToolkitModules = {};
+
+ChromeUtils.defineModuleGetter(
+ ToolkitModules,
+ "EventEmitter",
+ "resource://gre/modules/EventEmitter.jsm"
+);
+
+var { ignoreEvent } = ExtensionCommon;
+
+// Manages a notification popup (notifications API) created by the extension.
+function Notification(context, notificationsMap, id, options) {
+ this.notificationsMap = notificationsMap;
+ this.id = id;
+ this.options = options;
+
+ let imageURL;
+ if (options.iconUrl) {
+ imageURL = context.extension.baseURI.resolve(options.iconUrl);
+ }
+
+ // Set before calling into nsIAlertsService, because the notification may be
+ // closed during the call.
+ notificationsMap.set(id, this);
+
+ try {
+ let svc = Cc["@mozilla.org/alerts-service;1"].getService(
+ Ci.nsIAlertsService
+ );
+ svc.showAlertNotification(
+ imageURL,
+ options.title,
+ options.message,
+ true, // textClickable
+ this.id,
+ this,
+ this.id,
+ undefined,
+ undefined,
+ undefined,
+ // Principal is not set because doing so reveals buttons to control
+ // notification preferences, which are currently not implemented for
+ // notifications triggered via this extension API (bug 1589693).
+ undefined,
+ context.incognito
+ );
+ } catch (e) {
+ // This will fail if alerts aren't available on the system.
+
+ this.observe(null, "alertfinished", id);
+ }
+}
+
+Notification.prototype = {
+ clear() {
+ try {
+ let svc = Cc["@mozilla.org/alerts-service;1"].getService(
+ Ci.nsIAlertsService
+ );
+ svc.closeAlert(this.id);
+ } catch (e) {
+ // This will fail if the OS doesn't support this function.
+ }
+ this.notificationsMap.delete(this.id);
+ },
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "alertclickcallback":
+ this.notificationsMap.emit("clicked", data);
+ break;
+ case "alertfinished":
+ this.notificationsMap.emit("closed", data);
+ this.notificationsMap.delete(this.id);
+ break;
+ case "alertshow":
+ this.notificationsMap.emit("shown", data);
+ break;
+ }
+ },
+};
+
+this.notifications = class extends ExtensionAPI {
+ constructor(extension) {
+ super(extension);
+
+ this.nextId = 0;
+ this.notificationsMap = new Map();
+ ToolkitModules.EventEmitter.decorate(this.notificationsMap);
+ }
+
+ onShutdown() {
+ for (let notification of this.notificationsMap.values()) {
+ notification.clear();
+ }
+ }
+
+ getAPI(context) {
+ let notificationsMap = this.notificationsMap;
+
+ return {
+ notifications: {
+ create: (notificationId, options) => {
+ if (!notificationId) {
+ notificationId = String(this.nextId++);
+ }
+
+ if (notificationsMap.has(notificationId)) {
+ notificationsMap.get(notificationId).clear();
+ }
+
+ new Notification(context, notificationsMap, notificationId, options);
+
+ return Promise.resolve(notificationId);
+ },
+
+ clear: function(notificationId) {
+ if (notificationsMap.has(notificationId)) {
+ notificationsMap.get(notificationId).clear();
+ return Promise.resolve(true);
+ }
+ return Promise.resolve(false);
+ },
+
+ getAll: function() {
+ let result = {};
+ notificationsMap.forEach((value, key) => {
+ result[key] = value.options;
+ });
+ return Promise.resolve(result);
+ },
+
+ onClosed: new EventManager({
+ context,
+ name: "notifications.onClosed",
+ register: fire => {
+ let listener = (event, notificationId) => {
+ // TODO Bug 1413188, Support the byUser argument.
+ fire.async(notificationId, true);
+ };
+
+ notificationsMap.on("closed", listener);
+ return () => {
+ notificationsMap.off("closed", listener);
+ };
+ },
+ }).api(),
+
+ onClicked: new EventManager({
+ context,
+ name: "notifications.onClicked",
+ register: fire => {
+ let listener = (event, notificationId) => {
+ fire.async(notificationId);
+ };
+
+ notificationsMap.on("clicked", listener);
+ return () => {
+ notificationsMap.off("clicked", listener);
+ };
+ },
+ }).api(),
+
+ onShown: new EventManager({
+ context,
+ name: "notifications.onShown",
+ register: fire => {
+ let listener = (event, notificationId) => {
+ fire.async(notificationId);
+ };
+
+ notificationsMap.on("shown", listener);
+ return () => {
+ notificationsMap.off("shown", listener);
+ };
+ },
+ }).api(),
+
+ // TODO Bug 1190681, implement button support.
+ onButtonClicked: ignoreEvent(context, "notifications.onButtonClicked"),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-permissions.js b/toolkit/components/extensions/parent/ext-permissions.js
new file mode 100644
index 0000000000..cfb2276c1b
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-permissions.js
@@ -0,0 +1,173 @@
+/* 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";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+});
+
+var { ExtensionError } = ExtensionUtils;
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "promptsEnabled",
+ "extensions.webextOptionalPermissionPrompts"
+);
+
+function normalizePermissions(perms) {
+ perms = { ...perms };
+ perms.permissions = perms.permissions.filter(
+ perm => !perm.startsWith("internal:")
+ );
+ return perms;
+}
+
+this.permissions = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+
+ return {
+ permissions: {
+ async request(perms) {
+ let { permissions, origins } = perms;
+
+ let manifestPermissions =
+ context.extension.manifest.optional_permissions;
+ for (let perm of permissions) {
+ if (!manifestPermissions.includes(perm)) {
+ throw new ExtensionError(
+ `Cannot request permission ${perm} since it was not declared in optional_permissions`
+ );
+ }
+ }
+
+ let optionalOrigins = context.extension.optionalOrigins;
+ for (let origin of origins) {
+ if (!optionalOrigins.subsumes(new MatchPattern(origin))) {
+ throw new ExtensionError(
+ `Cannot request origin permission for ${origin} since it was not declared in optional_permissions`
+ );
+ }
+ }
+
+ if (promptsEnabled) {
+ permissions = permissions.filter(
+ perm => !context.extension.hasPermission(perm)
+ );
+ origins = origins.filter(
+ origin =>
+ !context.extension.allowedOrigins.subsumes(
+ new MatchPattern(origin)
+ )
+ );
+
+ if (!permissions.length && !origins.length) {
+ return true;
+ }
+
+ let browser = context.pendingEventBrowser || context.xulBrowser;
+ let allow = await new Promise(resolve => {
+ let subject = {
+ wrappedJSObject: {
+ browser,
+ name: context.extension.name,
+ icon: context.extension.iconURL,
+ permissions: { permissions, origins },
+ resolve,
+ },
+ };
+ Services.obs.notifyObservers(
+ subject,
+ "webextension-optional-permission-prompt"
+ );
+ });
+ if (!allow) {
+ return false;
+ }
+ }
+
+ await ExtensionPermissions.add(extension.id, perms, extension);
+ return true;
+ },
+
+ async getAll() {
+ let perms = normalizePermissions(context.extension.activePermissions);
+ delete perms.apis;
+ return perms;
+ },
+
+ async contains(permissions) {
+ for (let perm of permissions.permissions) {
+ if (!context.extension.hasPermission(perm)) {
+ return false;
+ }
+ }
+
+ for (let origin of permissions.origins) {
+ if (
+ !context.extension.allowedOrigins.subsumes(
+ new MatchPattern(origin)
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ async remove(permissions) {
+ await ExtensionPermissions.remove(
+ extension.id,
+ permissions,
+ extension
+ );
+ return true;
+ },
+
+ onAdded: new EventManager({
+ context,
+ name: "permissions.onAdded",
+ register: fire => {
+ let callback = (event, change) => {
+ if (change.extensionId == extension.id && change.added) {
+ let perms = normalizePermissions(change.added);
+ if (perms.permissions.length || perms.origins.length) {
+ fire.async(perms);
+ }
+ }
+ };
+
+ extensions.on("change-permissions", callback);
+ return () => {
+ extensions.off("change-permissions", callback);
+ };
+ },
+ }).api(),
+
+ onRemoved: new EventManager({
+ context,
+ name: "permissions.onRemoved",
+ register: fire => {
+ let callback = (event, change) => {
+ if (change.extensionId == extension.id && change.removed) {
+ let perms = normalizePermissions(change.removed);
+ if (perms.permissions.length || perms.origins.length) {
+ fire.async(perms);
+ }
+ }
+ };
+
+ extensions.on("change-permissions", callback);
+ return () => {
+ extensions.off("change-permissions", callback);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-privacy.js b/toolkit/components/extensions/parent/ext-privacy.js
new file mode 100644
index 0000000000..94f0763a1f
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-privacy.js
@@ -0,0 +1,496 @@
+/* -*- 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+
+var { ExtensionPreferencesManager } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionPreferencesManager.jsm"
+);
+
+var { ExtensionError } = ExtensionUtils;
+var { getSettingsAPI } = ExtensionPreferencesManager;
+
+const cookieSvc = Ci.nsICookieService;
+
+const getIntPref = p => Services.prefs.getIntPref(p, undefined);
+const getBoolPref = p => Services.prefs.getBoolPref(p, undefined);
+
+const TLS_MIN_PREF = "security.tls.version.min";
+const TLS_MAX_PREF = "security.tls.version.max";
+
+const cookieBehaviorValues = new Map([
+ ["allow_all", cookieSvc.BEHAVIOR_ACCEPT],
+ ["reject_third_party", cookieSvc.BEHAVIOR_REJECT_FOREIGN],
+ ["reject_all", cookieSvc.BEHAVIOR_REJECT],
+ ["allow_visited", cookieSvc.BEHAVIOR_LIMIT_FOREIGN],
+ ["reject_trackers", cookieSvc.BEHAVIOR_REJECT_TRACKER],
+ [
+ "reject_trackers_and_partition_foreign",
+ cookieSvc.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
+ ],
+]);
+
+function isTLSMinVersionLowerOrEQThan(version) {
+ return (
+ Services.prefs.getDefaultBranch("").getIntPref(TLS_MIN_PREF) <= version
+ );
+}
+
+const TLS_VERSIONS = [
+ { version: 1, name: "TLSv1", settable: isTLSMinVersionLowerOrEQThan(1) },
+ { version: 2, name: "TLSv1.1", settable: isTLSMinVersionLowerOrEQThan(2) },
+ { version: 3, name: "TLSv1.2", settable: true },
+ { version: 4, name: "TLSv1.3", settable: true },
+];
+
+// Add settings objects for supported APIs to the preferences manager.
+ExtensionPreferencesManager.addSetting("network.networkPredictionEnabled", {
+ permission: "privacy",
+ prefNames: [
+ "network.predictor.enabled",
+ "network.prefetch-next",
+ "network.http.speculative-parallel-limit",
+ "network.dns.disablePrefetch",
+ ],
+
+ setCallback(value) {
+ return {
+ "network.http.speculative-parallel-limit": value ? undefined : 0,
+ "network.dns.disablePrefetch": !value,
+ "network.predictor.enabled": value,
+ "network.prefetch-next": value,
+ };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("network.httpsOnlyMode", {
+ permission: "privacy",
+ prefNames: [
+ "dom.security.https_only_mode",
+ "dom.security.https_only_mode_pbm",
+ ],
+
+ setCallback(value) {
+ let prefs = {
+ "dom.security.https_only_mode": false,
+ "dom.security.https_only_mode_pbm": false,
+ };
+
+ switch (value) {
+ case "always":
+ prefs["dom.security.https_only_mode"] = true;
+ break;
+
+ case "private_browsing":
+ prefs["dom.security.https_only_mode_pbm"] = true;
+ break;
+
+ case "never":
+ break;
+ }
+
+ return prefs;
+ },
+});
+
+ExtensionPreferencesManager.addSetting("network.peerConnectionEnabled", {
+ permission: "privacy",
+ prefNames: ["media.peerconnection.enabled"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("network.webRTCIPHandlingPolicy", {
+ permission: "privacy",
+ prefNames: [
+ "media.peerconnection.ice.default_address_only",
+ "media.peerconnection.ice.no_host",
+ "media.peerconnection.ice.proxy_only_if_behind_proxy",
+ "media.peerconnection.ice.proxy_only",
+ ],
+
+ setCallback(value) {
+ let prefs = {};
+ switch (value) {
+ case "default":
+ // All prefs are already set to be reset.
+ break;
+
+ case "default_public_and_private_interfaces":
+ prefs["media.peerconnection.ice.default_address_only"] = true;
+ break;
+
+ case "default_public_interface_only":
+ prefs["media.peerconnection.ice.default_address_only"] = true;
+ prefs["media.peerconnection.ice.no_host"] = true;
+ break;
+
+ case "disable_non_proxied_udp":
+ prefs["media.peerconnection.ice.default_address_only"] = true;
+ prefs["media.peerconnection.ice.no_host"] = true;
+ prefs["media.peerconnection.ice.proxy_only_if_behind_proxy"] = true;
+ break;
+
+ case "proxy_only":
+ prefs["media.peerconnection.ice.proxy_only"] = true;
+ break;
+ }
+ return prefs;
+ },
+});
+
+ExtensionPreferencesManager.addSetting("services.passwordSavingEnabled", {
+ permission: "privacy",
+ prefNames: ["signon.rememberSignons"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("websites.cookieConfig", {
+ permission: "privacy",
+ prefNames: ["network.cookie.cookieBehavior", "network.cookie.lifetimePolicy"],
+
+ setCallback(value) {
+ const cookieBehavior = cookieBehaviorValues.get(value.behavior);
+
+ // Intentionally use Preferences.get("network.cookie.cookieBehavior") here
+ // to read the "real" preference value.
+ const needUpdate =
+ cookieBehavior !== getIntPref("network.cookie.cookieBehavior");
+ if (
+ needUpdate &&
+ getBoolPref("privacy.firstparty.isolate") &&
+ cookieBehavior === cookieSvc.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ ) {
+ throw new ExtensionError(
+ `Invalid cookieConfig '${value.behavior}' when firstPartyIsolate is enabled`
+ );
+ }
+ return {
+ "network.cookie.cookieBehavior": cookieBehavior,
+ "network.cookie.lifetimePolicy": value.nonPersistentCookies
+ ? cookieSvc.ACCEPT_SESSION
+ : cookieSvc.ACCEPT_NORMALLY,
+ };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("websites.firstPartyIsolate", {
+ permission: "privacy",
+ prefNames: ["privacy.firstparty.isolate"],
+
+ setCallback(value) {
+ // Intentionally use Preferences.get("network.cookie.cookieBehavior") here
+ // to read the "real" preference value.
+ const cookieBehavior = getIntPref("network.cookie.cookieBehavior");
+
+ const needUpdate = value !== getBoolPref("privacy.firstparty.isolate");
+ if (
+ needUpdate &&
+ value &&
+ cookieBehavior === cookieSvc.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN
+ ) {
+ const behavior = Array.from(cookieBehaviorValues.entries()).find(
+ entry => entry[1] === cookieBehavior
+ )[0];
+ throw new ExtensionError(
+ `Can't enable firstPartyIsolate when cookieBehavior is '${behavior}'`
+ );
+ }
+
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("websites.hyperlinkAuditingEnabled", {
+ permission: "privacy",
+ prefNames: ["browser.send_pings"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("websites.referrersEnabled", {
+ permission: "privacy",
+ prefNames: ["network.http.sendRefererHeader"],
+
+ // Values for network.http.sendRefererHeader:
+ // 0=don't send any, 1=send only on clicks, 2=send on image requests as well
+ // http://searchfox.org/mozilla-central/rev/61054508641ee76f9c49bcf7303ef3cfb6b410d2/modules/libpref/init/all.js#1585
+ setCallback(value) {
+ return { [this.prefNames[0]]: value ? 2 : 0 };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("websites.resistFingerprinting", {
+ permission: "privacy",
+ prefNames: ["privacy.resistFingerprinting"],
+
+ setCallback(value) {
+ return { [this.prefNames[0]]: value };
+ },
+});
+
+ExtensionPreferencesManager.addSetting("websites.trackingProtectionMode", {
+ permission: "privacy",
+ prefNames: [
+ "privacy.trackingprotection.enabled",
+ "privacy.trackingprotection.pbmode.enabled",
+ ],
+
+ setCallback(value) {
+ // Default to private browsing.
+ let prefs = {
+ "privacy.trackingprotection.enabled": false,
+ "privacy.trackingprotection.pbmode.enabled": true,
+ };
+
+ switch (value) {
+ case "private_browsing":
+ break;
+
+ case "always":
+ prefs["privacy.trackingprotection.enabled"] = true;
+ break;
+
+ case "never":
+ prefs["privacy.trackingprotection.pbmode.enabled"] = false;
+ break;
+ }
+
+ return prefs;
+ },
+});
+
+ExtensionPreferencesManager.addSetting("network.tlsVersionRestriction", {
+ permission: "privacy",
+ prefNames: [TLS_MIN_PREF, TLS_MAX_PREF],
+
+ setCallback(value) {
+ function tlsStringToVersion(string) {
+ const version = TLS_VERSIONS.find(a => a.name === string);
+ if (version && version.settable) {
+ return version.version;
+ }
+
+ throw new ExtensionError(
+ `Setting TLS version ${string} is not allowed for security reasons.`
+ );
+ }
+
+ const prefs = {};
+
+ if (value.minimum) {
+ prefs[TLS_MIN_PREF] = tlsStringToVersion(value.minimum);
+ }
+
+ if (value.maximum) {
+ prefs[TLS_MAX_PREF] = tlsStringToVersion(value.maximum);
+ }
+
+ // If minimum has passed and it's greater than the max value.
+ if (prefs[TLS_MIN_PREF]) {
+ const max = prefs[TLS_MAX_PREF] || getIntPref(TLS_MAX_PREF);
+ if (max < prefs[TLS_MIN_PREF]) {
+ throw new ExtensionError(
+ `Setting TLS min version grater than the max version is not allowed.`
+ );
+ }
+ }
+
+ // If maximum has passed and it's lower than the min value.
+ else if (prefs[TLS_MAX_PREF]) {
+ const min = getIntPref(TLS_MIN_PREF);
+ if (min > prefs[TLS_MAX_PREF]) {
+ throw new ExtensionError(
+ `Setting TLS max version lower than the min version is not allowed.`
+ );
+ }
+ }
+
+ return prefs;
+ },
+});
+
+this.privacy = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ privacy: {
+ network: {
+ networkPredictionEnabled: getSettingsAPI({
+ context,
+ name: "network.networkPredictionEnabled",
+ callback() {
+ return (
+ getBoolPref("network.predictor.enabled") &&
+ getBoolPref("network.prefetch-next") &&
+ getIntPref("network.http.speculative-parallel-limit") > 0 &&
+ !getBoolPref("network.dns.disablePrefetch")
+ );
+ },
+ }),
+ httpsOnlyMode: getSettingsAPI({
+ context,
+ name: "network.httpsOnlyMode",
+ callback() {
+ if (getBoolPref("dom.security.https_only_mode")) {
+ return "always";
+ }
+ if (getBoolPref("dom.security.https_only_mode_pbm")) {
+ return "private_browsing";
+ }
+ return "never";
+ },
+ readOnly: true,
+ }),
+ peerConnectionEnabled: getSettingsAPI({
+ context,
+ name: "network.peerConnectionEnabled",
+ callback() {
+ return getBoolPref("media.peerconnection.enabled");
+ },
+ }),
+ webRTCIPHandlingPolicy: getSettingsAPI({
+ context,
+ name: "network.webRTCIPHandlingPolicy",
+ callback() {
+ if (getBoolPref("media.peerconnection.ice.proxy_only")) {
+ return "proxy_only";
+ }
+
+ let default_address_only = getBoolPref(
+ "media.peerconnection.ice.default_address_only"
+ );
+ if (default_address_only) {
+ let no_host = getBoolPref("media.peerconnection.ice.no_host");
+ if (no_host) {
+ if (
+ getBoolPref(
+ "media.peerconnection.ice.proxy_only_if_behind_proxy"
+ )
+ ) {
+ return "disable_non_proxied_udp";
+ }
+ return "default_public_interface_only";
+ }
+ return "default_public_and_private_interfaces";
+ }
+
+ return "default";
+ },
+ }),
+ tlsVersionRestriction: getSettingsAPI({
+ context,
+ name: "network.tlsVersionRestriction",
+ callback() {
+ function tlsVersionToString(pref) {
+ const value = getIntPref(pref);
+ const version = TLS_VERSIONS.find(a => a.version === value);
+ if (version) {
+ return version.name;
+ }
+ return "unknown";
+ }
+
+ return {
+ minimum: tlsVersionToString(TLS_MIN_PREF),
+ maximum: tlsVersionToString(TLS_MAX_PREF),
+ };
+ },
+ validate() {
+ if (!context.extension.isPrivileged) {
+ throw new ExtensionError(
+ "tlsVersionRestriction can be set by privileged extensions only."
+ );
+ }
+ },
+ }),
+ },
+
+ services: {
+ passwordSavingEnabled: getSettingsAPI({
+ context,
+ name: "services.passwordSavingEnabled",
+ callback() {
+ return getBoolPref("signon.rememberSignons");
+ },
+ }),
+ },
+
+ websites: {
+ cookieConfig: getSettingsAPI({
+ context,
+ name: "websites.cookieConfig",
+ callback() {
+ let prefValue = getIntPref("network.cookie.cookieBehavior");
+ return {
+ behavior: Array.from(cookieBehaviorValues.entries()).find(
+ entry => entry[1] === prefValue
+ )[0],
+ nonPersistentCookies:
+ getIntPref("network.cookie.lifetimePolicy") ===
+ cookieSvc.ACCEPT_SESSION,
+ };
+ },
+ }),
+ firstPartyIsolate: getSettingsAPI({
+ context,
+ name: "websites.firstPartyIsolate",
+ callback() {
+ return getBoolPref("privacy.firstparty.isolate");
+ },
+ }),
+ hyperlinkAuditingEnabled: getSettingsAPI({
+ context,
+ name: "websites.hyperlinkAuditingEnabled",
+ callback() {
+ return getBoolPref("browser.send_pings");
+ },
+ }),
+ referrersEnabled: getSettingsAPI({
+ context,
+ name: "websites.referrersEnabled",
+ callback() {
+ return getIntPref("network.http.sendRefererHeader") !== 0;
+ },
+ }),
+ resistFingerprinting: getSettingsAPI({
+ context,
+ name: "websites.resistFingerprinting",
+ callback() {
+ return getBoolPref("privacy.resistFingerprinting");
+ },
+ }),
+ trackingProtectionMode: getSettingsAPI({
+ context,
+ name: "websites.trackingProtectionMode",
+ callback() {
+ if (getBoolPref("privacy.trackingprotection.enabled")) {
+ return "always";
+ } else if (
+ getBoolPref("privacy.trackingprotection.pbmode.enabled")
+ ) {
+ return "private_browsing";
+ }
+ return "never";
+ },
+ }),
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-protocolHandlers.js b/toolkit/components/extensions/parent/ext-protocolHandlers.js
new file mode 100644
index 0000000000..36cdf25d42
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-protocolHandlers.js
@@ -0,0 +1,100 @@
+/* -*- 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";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "handlerService",
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService"
+);
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "protocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService"
+);
+
+const hasHandlerApp = handlerConfig => {
+ let protoInfo = protocolService.getProtocolHandlerInfo(
+ handlerConfig.protocol
+ );
+ let appHandlers = protoInfo.possibleApplicationHandlers;
+ for (let i = 0; i < appHandlers.length; i++) {
+ let handler = appHandlers.queryElementAt(i, Ci.nsISupports);
+ if (
+ handler instanceof Ci.nsIWebHandlerApp &&
+ handler.uriTemplate === handlerConfig.uriTemplate
+ ) {
+ return true;
+ }
+ }
+ return false;
+};
+
+this.protocolHandlers = class extends ExtensionAPI {
+ onManifestEntry(entryName) {
+ let { extension } = this;
+ let { manifest } = extension;
+
+ for (let handlerConfig of manifest.protocol_handlers) {
+ if (hasHandlerApp(handlerConfig)) {
+ continue;
+ }
+
+ let handler = Cc[
+ "@mozilla.org/uriloader/web-handler-app;1"
+ ].createInstance(Ci.nsIWebHandlerApp);
+ handler.name = handlerConfig.name;
+ handler.uriTemplate = handlerConfig.uriTemplate;
+
+ let protoInfo = protocolService.getProtocolHandlerInfo(
+ handlerConfig.protocol
+ );
+ let handlers = protoInfo.possibleApplicationHandlers;
+ if (protoInfo.preferredApplicationHandler || handlers.length) {
+ protoInfo.alwaysAskBeforeHandling = true;
+ } else {
+ protoInfo.preferredApplicationHandler = handler;
+ protoInfo.alwaysAskBeforeHandling = false;
+ }
+ handlers.appendElement(handler);
+ handlerService.store(protoInfo);
+ }
+ }
+
+ onShutdown(isAppShutdown) {
+ let { extension } = this;
+ let { manifest } = extension;
+
+ if (isAppShutdown) {
+ return;
+ }
+
+ for (let handlerConfig of manifest.protocol_handlers) {
+ let protoInfo = protocolService.getProtocolHandlerInfo(
+ handlerConfig.protocol
+ );
+ let appHandlers = protoInfo.possibleApplicationHandlers;
+ for (let i = 0; i < appHandlers.length; i++) {
+ let handler = appHandlers.queryElementAt(i, Ci.nsISupports);
+ if (
+ handler instanceof Ci.nsIWebHandlerApp &&
+ handler.uriTemplate === handlerConfig.uriTemplate
+ ) {
+ appHandlers.removeElementAt(i);
+ if (protoInfo.preferredApplicationHandler === handler) {
+ protoInfo.preferredApplicationHandler = null;
+ protoInfo.alwaysAskBeforeHandling = true;
+ }
+ handlerService.store(protoInfo);
+ break;
+ }
+ }
+ }
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-proxy.js b/toolkit/components/extensions/parent/ext-proxy.js
new file mode 100644
index 0000000000..acb0ba3675
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-proxy.js
@@ -0,0 +1,335 @@
+/* -*- 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ProxyChannelFilter",
+ "resource://gre/modules/ProxyChannelFilter.jsm"
+);
+var { ExtensionPreferencesManager } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionPreferencesManager.jsm"
+);
+
+var { ExtensionError } = ExtensionUtils;
+var { getSettingsAPI } = ExtensionPreferencesManager;
+
+const proxySvc = Ci.nsIProtocolProxyService;
+
+const PROXY_TYPES_MAP = new Map([
+ ["none", proxySvc.PROXYCONFIG_DIRECT],
+ ["autoDetect", proxySvc.PROXYCONFIG_WPAD],
+ ["system", proxySvc.PROXYCONFIG_SYSTEM],
+ ["manual", proxySvc.PROXYCONFIG_MANUAL],
+ ["autoConfig", proxySvc.PROXYCONFIG_PAC],
+]);
+
+const DEFAULT_PORTS = new Map([
+ ["http", 80],
+ ["ssl", 443],
+ ["ftp", 21],
+ ["socks", 1080],
+]);
+
+ExtensionPreferencesManager.addSetting("proxy.settings", {
+ permission: "proxy",
+ prefNames: [
+ "network.proxy.type",
+ "network.proxy.http",
+ "network.proxy.http_port",
+ "network.proxy.share_proxy_settings",
+ "network.proxy.ftp",
+ "network.proxy.ftp_port",
+ "network.proxy.ssl",
+ "network.proxy.ssl_port",
+ "network.proxy.socks",
+ "network.proxy.socks_port",
+ "network.proxy.socks_version",
+ "network.proxy.socks_remote_dns",
+ "network.proxy.no_proxies_on",
+ "network.proxy.autoconfig_url",
+ "signon.autologin.proxy",
+ "network.http.proxy.respect-be-conservative",
+ ],
+
+ setCallback(value) {
+ let prefs = {
+ "network.proxy.type": PROXY_TYPES_MAP.get(value.proxyType),
+ "signon.autologin.proxy": value.autoLogin,
+ "network.proxy.socks_remote_dns": value.proxyDNS,
+ "network.proxy.autoconfig_url": value.autoConfigUrl,
+ "network.proxy.share_proxy_settings": value.httpProxyAll,
+ "network.proxy.socks_version": value.socksVersion,
+ "network.proxy.no_proxies_on": value.passthrough,
+ "network.http.proxy.respect-be-conservative": value.respectBeConservative,
+ };
+
+ for (let prop of ["http", "ftp", "ssl", "socks"]) {
+ if (value[prop]) {
+ let url = new URL(`http://${value[prop]}`);
+ prefs[`network.proxy.${prop}`] = url.hostname;
+ // Only fall back to defaults if no port provided.
+ let [, rawPort] = value[prop].split(":");
+ let port = parseInt(rawPort, 10) || DEFAULT_PORTS.get(prop);
+ prefs[`network.proxy.${prop}_port`] = port;
+ }
+ }
+
+ return prefs;
+ },
+});
+
+function registerProxyFilterEvent(
+ context,
+ extension,
+ fire,
+ filterProps,
+ extraInfoSpec = []
+) {
+ let listener = data => {
+ return fire.sync(data);
+ };
+
+ let filter = { ...filterProps };
+ if (filter.urls) {
+ let perms = new MatchPatternSet([
+ ...extension.allowedOrigins.patterns,
+ ...extension.optionalOrigins.patterns,
+ ]);
+ filter.urls = new MatchPatternSet(filter.urls);
+
+ if (!perms.overlapsAll(filter.urls)) {
+ Cu.reportError(
+ "The proxy.onRequest filter doesn't overlap with host permissions."
+ );
+ }
+ }
+
+ let proxyFilter = new ProxyChannelFilter(
+ context,
+ extension,
+ listener,
+ filter,
+ extraInfoSpec
+ );
+ return {
+ unregister: () => {
+ proxyFilter.destroy();
+ },
+ convert(_fire, _context) {
+ fire = _fire;
+ proxyFilter.context = _context;
+ },
+ };
+}
+
+this.proxy = class extends ExtensionAPI {
+ primeListener(extension, event, fire, params) {
+ if (event === "onRequest") {
+ return registerProxyFilterEvent(undefined, extension, fire, ...params);
+ }
+ }
+
+ getAPI(context) {
+ let { extension } = context;
+
+ return {
+ proxy: {
+ onRequest: new EventManager({
+ context,
+ name: `proxy.onRequest`,
+ persistent: {
+ module: "proxy",
+ event: "onRequest",
+ },
+ register: (fire, filter, info) => {
+ return registerProxyFilterEvent(
+ context,
+ context.extension,
+ fire,
+ filter,
+ info
+ ).unregister;
+ },
+ }).api(),
+
+ // Leaving as non-persistent. By itself it's not useful since proxy-error
+ // is emitted from the proxy filter.
+ onError: new EventManager({
+ context,
+ name: "proxy.onError",
+ register: fire => {
+ let listener = (name, error) => {
+ fire.async(error);
+ };
+ extension.on("proxy-error", listener);
+ return () => {
+ extension.off("proxy-error", listener);
+ };
+ },
+ }).api(),
+
+ settings: Object.assign(
+ getSettingsAPI({
+ context,
+ name: "proxy.settings",
+ callback() {
+ let prefValue = Services.prefs.getIntPref("network.proxy.type");
+ let proxyConfig = {
+ proxyType: Array.from(PROXY_TYPES_MAP.entries()).find(
+ entry => entry[1] === prefValue
+ )[0],
+ autoConfigUrl: Services.prefs.getCharPref(
+ "network.proxy.autoconfig_url"
+ ),
+ autoLogin: Services.prefs.getBoolPref("signon.autologin.proxy"),
+ proxyDNS: Services.prefs.getBoolPref(
+ "network.proxy.socks_remote_dns"
+ ),
+ httpProxyAll: Services.prefs.getBoolPref(
+ "network.proxy.share_proxy_settings"
+ ),
+ socksVersion: Services.prefs.getIntPref(
+ "network.proxy.socks_version"
+ ),
+ passthrough: Services.prefs.getCharPref(
+ "network.proxy.no_proxies_on"
+ ),
+ };
+
+ if (extension.isPrivileged) {
+ proxyConfig.respectBeConservative = Services.prefs.getBoolPref(
+ "network.http.proxy.respect-be-conservative"
+ );
+ }
+
+ for (let prop of ["http", "ftp", "ssl", "socks"]) {
+ let host = Services.prefs.getCharPref(`network.proxy.${prop}`);
+ let port = Services.prefs.getIntPref(
+ `network.proxy.${prop}_port`
+ );
+ proxyConfig[prop] = port ? `${host}:${port}` : host;
+ }
+
+ return proxyConfig;
+ },
+ // proxy.settings is unsupported on android.
+ validate() {
+ if (AppConstants.platform == "android") {
+ throw new ExtensionError(
+ `proxy.settings is not supported on android.`
+ );
+ }
+ },
+ }),
+ {
+ set: details => {
+ if (AppConstants.platform === "android") {
+ throw new ExtensionError(
+ "proxy.settings is not supported on android."
+ );
+ }
+
+ if (!extension.privateBrowsingAllowed) {
+ throw new ExtensionError(
+ "proxy.settings requires private browsing permission."
+ );
+ }
+
+ if (!Services.policies.isAllowed("changeProxySettings")) {
+ throw new ExtensionError(
+ "Proxy settings are being managed by the Policies manager."
+ );
+ }
+
+ let value = details.value;
+
+ // proxyType is optional and it should default to "system" when missing.
+ if (value.proxyType == null) {
+ value.proxyType = "system";
+ }
+
+ if (!PROXY_TYPES_MAP.has(value.proxyType)) {
+ throw new ExtensionError(
+ `${value.proxyType} is not a valid value for proxyType.`
+ );
+ }
+
+ if (value.httpProxyAll) {
+ // Match what about:preferences does with proxy settings
+ // since the proxy service does not check the value
+ // of share_proxy_settings.
+ for (let prop of ["ftp", "ssl"]) {
+ value[prop] = value.http;
+ }
+ }
+
+ for (let prop of ["http", "ftp", "ssl", "socks"]) {
+ let host = value[prop];
+ if (host) {
+ try {
+ // Fixup in case a full url is passed.
+ if (host.includes("://")) {
+ value[prop] = new URL(host).host;
+ } else {
+ // Validate the host value.
+ new URL(`http://${host}`);
+ }
+ } catch (e) {
+ throw new ExtensionError(
+ `${value[prop]} is not a valid value for ${prop}.`
+ );
+ }
+ }
+ }
+
+ if (value.proxyType === "autoConfig" || value.autoConfigUrl) {
+ try {
+ new URL(value.autoConfigUrl);
+ } catch (e) {
+ throw new ExtensionError(
+ `${value.autoConfigUrl} is not a valid value for autoConfigUrl.`
+ );
+ }
+ }
+
+ if (value.socksVersion !== undefined) {
+ if (
+ !Number.isInteger(value.socksVersion) ||
+ value.socksVersion < 4 ||
+ value.socksVersion > 5
+ ) {
+ throw new ExtensionError(
+ `${value.socksVersion} is not a valid value for socksVersion.`
+ );
+ }
+ }
+
+ if (
+ value.respectBeConservative !== undefined &&
+ !extension.isPrivileged &&
+ Services.prefs.getBoolPref(
+ "network.http.proxy.respect-be-conservative"
+ ) != value.respectBeConservative
+ ) {
+ throw new ExtensionError(
+ `respectBeConservative can be set by privileged extensions only.`
+ );
+ }
+
+ return ExtensionPreferencesManager.setSetting(
+ extension.id,
+ "proxy.settings",
+ value
+ );
+ },
+ }
+ ),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-runtime.js b/toolkit/components/extensions/parent/ext-runtime.js
new file mode 100644
index 0000000000..96892c1655
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-runtime.js
@@ -0,0 +1,178 @@
+/* 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";
+
+var { ExtensionParent } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionParent.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManager",
+ "resource://gre/modules/AddonManager.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "DevToolsShim",
+ "chrome://devtools-startup/content/DevToolsShim.jsm"
+);
+
+this.runtime = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ return {
+ runtime: {
+ onStartup: new EventManager({
+ context,
+ name: "runtime.onStartup",
+ register: fire => {
+ if (context.incognito || extension.startupReason != "APP_STARTUP") {
+ // This event should not fire if we are operating in a private profile.
+ return () => {};
+ }
+ let listener = () => fire.sync();
+ extension.on("background-page-started", listener);
+ return () => {
+ extension.off("background-page-started", listener);
+ };
+ },
+ }).api(),
+
+ onInstalled: new EventManager({
+ context,
+ name: "runtime.onInstalled",
+ register: fire => {
+ let temporary = !!extension.addonData.temporarilyInstalled;
+
+ let listener = () => {
+ switch (extension.startupReason) {
+ case "APP_STARTUP":
+ if (AddonManagerPrivate.browserUpdated) {
+ fire.sync({ reason: "browser_update", temporary });
+ }
+ break;
+ case "ADDON_INSTALL":
+ fire.sync({ reason: "install", temporary });
+ break;
+ case "ADDON_UPGRADE":
+ fire.sync({
+ reason: "update",
+ previousVersion: extension.addonData.oldVersion,
+ temporary,
+ });
+ break;
+ }
+ };
+ extension.on("background-page-started", listener);
+ return () => {
+ extension.off("background-page-started", listener);
+ };
+ },
+ }).api(),
+
+ onUpdateAvailable: new EventManager({
+ context,
+ name: "runtime.onUpdateAvailable",
+ register: fire => {
+ let instanceID = extension.addonData.instanceID;
+ AddonManager.addUpgradeListener(instanceID, upgrade => {
+ extension.upgrade = upgrade;
+ let details = {
+ version: upgrade.version,
+ };
+ fire.sync(details);
+ });
+ return () => {
+ AddonManager.removeUpgradeListener(instanceID);
+ };
+ },
+ }).api(),
+
+ reload: async () => {
+ if (extension.upgrade) {
+ // If there is a pending update, install it now.
+ extension.upgrade.install();
+ } else {
+ // Otherwise, reload the current extension.
+ let addon = await AddonManager.getAddonByID(extension.id);
+ addon.reload();
+ }
+ },
+
+ get lastError() {
+ // TODO(robwu): Figure out how to make sure that errors in the parent
+ // process are propagated to the child process.
+ // lastError should not be accessed from the parent.
+ return context.lastError;
+ },
+
+ getBrowserInfo: function() {
+ const { name, vendor, version, appBuildID } = Services.appinfo;
+ const info = { name, vendor, version, buildID: appBuildID };
+ return Promise.resolve(info);
+ },
+
+ getPlatformInfo: function() {
+ return Promise.resolve(ExtensionParent.PlatformInfo);
+ },
+
+ openOptionsPage: function() {
+ if (!extension.manifest.options_ui) {
+ return Promise.reject({ message: "No `options_ui` declared" });
+ }
+
+ // This expects openOptionsPage to be defined in the file using this,
+ // e.g. the browser/ version of ext-runtime.js
+ /* global openOptionsPage:false */
+ return openOptionsPage(extension).then(() => {});
+ },
+
+ setUninstallURL: function(url) {
+ if (url === null || url.length === 0) {
+ extension.uninstallURL = null;
+ return Promise.resolve();
+ }
+
+ let uri;
+ try {
+ uri = new URL(url);
+ } catch (e) {
+ return Promise.reject({
+ message: `Invalid URL: ${JSON.stringify(url)}`,
+ });
+ }
+
+ if (uri.protocol != "http:" && uri.protocol != "https:") {
+ return Promise.reject({
+ message: "url must have the scheme http or https",
+ });
+ }
+
+ extension.uninstallURL = url;
+ return Promise.resolve();
+ },
+
+ // This function is not exposed to the extension js code and it is only
+ // used by the alert function redefined into the background pages to be
+ // able to open the BrowserConsole from the main process.
+ openBrowserConsole() {
+ if (AppConstants.platform !== "android") {
+ DevToolsShim.openBrowserConsole();
+ }
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-storage.js b/toolkit/components/extensions/parent/ext-storage.js
new file mode 100644
index 0000000000..ac75e9d6d1
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-storage.js
@@ -0,0 +1,228 @@
+/* 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";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
+ ExtensionStorage: "resource://gre/modules/ExtensionStorage.jsm",
+ ExtensionStorageIDB: "resource://gre/modules/ExtensionStorageIDB.jsm",
+ NativeManifests: "resource://gre/modules/NativeManifests.jsm",
+});
+
+var { ExtensionError } = ExtensionUtils;
+
+XPCOMUtils.defineLazyGetter(this, "extensionStorageSync", () => {
+ let url = Services.prefs.getBoolPref("webextensions.storage.sync.kinto")
+ ? "resource://gre/modules/ExtensionStorageSyncKinto.jsm"
+ : "resource://gre/modules/ExtensionStorageSync.jsm";
+
+ const { extensionStorageSync } = ChromeUtils.import(url, {});
+ return extensionStorageSync;
+});
+
+const enforceNoTemporaryAddon = extensionId => {
+ const EXCEPTION_MESSAGE =
+ "The storage API will not work with a temporary addon ID. " +
+ "Please add an explicit addon ID to your manifest. " +
+ "For more information see https://mzl.la/3lPk1aE.";
+ if (AddonManagerPrivate.isTemporaryInstallID(extensionId)) {
+ throw new ExtensionError(EXCEPTION_MESSAGE);
+ }
+};
+
+// WeakMap[extension -> Promise<SerializableMap?>]
+const managedStorage = new WeakMap();
+
+const lookupManagedStorage = async (extensionId, context) => {
+ if (Services.policies) {
+ let extensionPolicy = Services.policies.getExtensionPolicy(extensionId);
+ if (extensionPolicy) {
+ return ExtensionStorage._serializableMap(extensionPolicy);
+ }
+ }
+ let info = await NativeManifests.lookupManifest(
+ "storage",
+ extensionId,
+ context
+ );
+ if (info) {
+ return ExtensionStorage._serializableMap(info.manifest.data);
+ }
+ return null;
+};
+
+this.storage = class extends ExtensionAPI {
+ constructor(extension) {
+ super(extension);
+
+ const messageName = `Extension:StorageLocalOnChanged:${extension.uuid}`;
+ Services.ppmm.addMessageListener(messageName, this);
+ this.clearStorageChangedListener = () => {
+ Services.ppmm.removeMessageListener(messageName, this);
+ };
+ }
+
+ onShutdown() {
+ const { clearStorageChangedListener } = this;
+ this.clearStorageChangedListener = null;
+
+ if (clearStorageChangedListener) {
+ clearStorageChangedListener();
+ }
+ }
+
+ receiveMessage({ name, data }) {
+ if (name !== `Extension:StorageLocalOnChanged:${this.extension.uuid}`) {
+ return;
+ }
+
+ ExtensionStorageIDB.notifyListeners(this.extension.id, data);
+ }
+
+ getAPI(context) {
+ let { extension } = context;
+
+ return {
+ storage: {
+ local: {
+ async callMethodInParentProcess(method, args) {
+ const res = await ExtensionStorageIDB.selectBackend({ extension });
+ if (!res.backendEnabled) {
+ return ExtensionStorage[method](extension.id, ...args);
+ }
+
+ const persisted = extension.hasPermission("unlimitedStorage");
+ const db = await ExtensionStorageIDB.open(
+ res.storagePrincipal.deserialize(this, true),
+ persisted
+ );
+ try {
+ const changes = await db[method](...args);
+ if (changes) {
+ ExtensionStorageIDB.notifyListeners(extension.id, changes);
+ }
+ return changes;
+ } catch (err) {
+ const normalizedError = ExtensionStorageIDB.normalizeStorageError(
+ {
+ error: err,
+ extensionId: extension.id,
+ storageMethod: method,
+ }
+ ).message;
+ return Promise.reject({
+ message: String(normalizedError),
+ });
+ }
+ },
+ // Private storage.local JSONFile backend methods (used internally by the child
+ // ext-storage.js module).
+ JSONFileBackend: {
+ get(spec) {
+ return ExtensionStorage.get(extension.id, spec);
+ },
+ set(items) {
+ return ExtensionStorage.set(extension.id, items);
+ },
+ remove(keys) {
+ return ExtensionStorage.remove(extension.id, keys);
+ },
+ clear() {
+ return ExtensionStorage.clear(extension.id);
+ },
+ },
+ // Private storage.local IDB backend methods (used internally by the child ext-storage.js
+ // module).
+ IDBBackend: {
+ selectBackend() {
+ return ExtensionStorageIDB.selectBackend(context);
+ },
+ },
+ },
+
+ sync: {
+ get(spec) {
+ enforceNoTemporaryAddon(extension.id);
+ return extensionStorageSync.get(extension, spec, context);
+ },
+ set(items) {
+ enforceNoTemporaryAddon(extension.id);
+ return extensionStorageSync.set(extension, items, context);
+ },
+ remove(keys) {
+ enforceNoTemporaryAddon(extension.id);
+ return extensionStorageSync.remove(extension, keys, context);
+ },
+ clear() {
+ enforceNoTemporaryAddon(extension.id);
+ return extensionStorageSync.clear(extension, context);
+ },
+ getBytesInUse(keys) {
+ enforceNoTemporaryAddon(extension.id);
+ return extensionStorageSync.getBytesInUse(extension, keys, context);
+ },
+ },
+
+ managed: {
+ async get(keys) {
+ enforceNoTemporaryAddon(extension.id);
+ let lookup = managedStorage.get(extension);
+
+ if (!lookup) {
+ lookup = lookupManagedStorage(extension.id, context);
+ managedStorage.set(extension, lookup);
+ }
+
+ let data = await lookup;
+ if (!data) {
+ return Promise.reject({
+ message: "Managed storage manifest not found",
+ });
+ }
+ return ExtensionStorage._filterProperties(data, keys);
+ },
+ },
+
+ onChanged: new EventManager({
+ context,
+ name: "storage.onChanged",
+ register: fire => {
+ let listenerLocal = changes => {
+ fire.raw(changes, "local");
+ };
+ let listenerSync = changes => {
+ fire.async(changes, "sync");
+ };
+
+ ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
+ ExtensionStorageIDB.addOnChangedListener(
+ extension.id,
+ listenerLocal
+ );
+ extensionStorageSync.addOnChangedListener(
+ extension,
+ listenerSync,
+ context
+ );
+ return () => {
+ ExtensionStorage.removeOnChangedListener(
+ extension.id,
+ listenerLocal
+ );
+ ExtensionStorageIDB.removeOnChangedListener(
+ extension.id,
+ listenerLocal
+ );
+ extensionStorageSync.removeOnChangedListener(
+ extension,
+ listenerSync
+ );
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-tabs-base.js b/toolkit/components/extensions/parent/ext-tabs-base.js
new file mode 100644
index 0000000000..20fc92601e
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-tabs-base.js
@@ -0,0 +1,2332 @@
+/* -*- 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";
+
+/* globals EventEmitter */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "containersEnabled",
+ "privacy.userContext.enabled"
+);
+
+var {
+ DefaultMap,
+ DefaultWeakMap,
+ ExtensionError,
+ parseMatchPatterns,
+} = ExtensionUtils;
+
+var { defineLazyGetter } = ExtensionCommon;
+
+/**
+ * The platform-specific type of native tab objects, which are wrapped by
+ * TabBase instances.
+ *
+ * @typedef {Object|XULElement} NativeTab
+ */
+
+/**
+ * @typedef {Object} MutedInfo
+ * @property {boolean} muted
+ * True if the tab is currently muted, false otherwise.
+ * @property {string} [reason]
+ * The reason the tab is muted. Either "user", if the tab was muted by a
+ * user, or "extension", if it was muted by an extension.
+ * @property {string} [extensionId]
+ * If the tab was muted by an extension, contains the internal ID of that
+ * extension.
+ */
+
+/**
+ * A platform-independent base class for extension-specific wrappers around
+ * native tab objects.
+ *
+ * @param {Extension} extension
+ * The extension object for which this wrapper is being created. Used to
+ * determine permissions for access to certain properties and
+ * functionality.
+ * @param {NativeTab} nativeTab
+ * The native tab object which is being wrapped. The type of this object
+ * varies by platform.
+ * @param {integer} id
+ * The numeric ID of this tab object. This ID should be the same for
+ * every extension, and for the lifetime of the tab.
+ */
+class TabBase {
+ constructor(extension, nativeTab, id) {
+ this.extension = extension;
+ this.tabManager = extension.tabManager;
+ this.id = id;
+ this.nativeTab = nativeTab;
+ this.activeTabWindowID = null;
+
+ if (!extension.privateBrowsingAllowed && this._incognito) {
+ throw new ExtensionError(`Invalid tab ID: ${id}`);
+ }
+ }
+
+ /**
+ * Sends a message, via the given context, to the ExtensionContent running in
+ * this tab. The tab's current innerWindowID is automatically added to the
+ * recipient filter for the message, and is used to ensure that the message is
+ * not processed if the content process navigates to a different content page
+ * before the message is received.
+ *
+ * @param {BaseContext} context
+ * The context through which to send the message.
+ * @param {string} messageName
+ * The name of the message to send.
+ * @param {object} [data = {}]
+ * Arbitrary, structured-clonable message data to send.
+ * @param {object} [options]
+ * An options object, as accepted by BaseContext.sendMessage.
+ *
+ * @returns {Promise}
+ */
+ sendMessage(context, messageName, data = {}, options = null) {
+ let { browser, innerWindowID } = this;
+
+ options = Object.assign({}, options);
+ options.recipient = Object.assign({ innerWindowID }, options.recipient);
+
+ return context.sendMessage(
+ browser.messageManager,
+ messageName,
+ data,
+ options
+ );
+ }
+
+ /**
+ * Capture the visible area of this tab, and return the result as a data: URI.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to perform the capture.
+ * @param {number} zoom
+ * The current zoom for the page.
+ * @param {Object} [options]
+ * The options with which to perform the capture.
+ * @param {string} [options.format = "png"]
+ * The image format in which to encode the captured data. May be one of
+ * "png" or "jpeg".
+ * @param {integer} [options.quality = 92]
+ * The quality at which to encode the captured image data, ranging from
+ * 0 to 100. Has no effect for the "png" format.
+ * @param {DOMRectInit} [options.rect]
+ * Area of the document to render, in CSS pixels, relative to the page.
+ * If null, the currently visible viewport is rendered.
+ * @param {number} [options.scale]
+ * The scale to render at, defaults to devicePixelRatio.
+ * @returns {Promise<string>}
+ */
+ async capture(context, zoom, options) {
+ let win = this.browser.ownerGlobal;
+ let scale = options?.scale || win.devicePixelRatio;
+ let rect = options?.rect && win.DOMRect.fromRect(options.rect);
+
+ let wgp = this.browsingContext.currentWindowGlobal;
+ let image = await wgp.drawSnapshot(rect, scale * zoom, "white");
+
+ let doc = Services.appShell.hiddenDOMWindow.document;
+ let canvas = doc.createElement("canvas");
+ canvas.width = image.width;
+ canvas.height = image.height;
+
+ let ctx = canvas.getContext("2d", { alpha: false });
+ ctx.drawImage(image, 0, 0);
+ image.close();
+
+ return canvas.toDataURL(`image/${options?.format}`, options?.quality / 100);
+ }
+
+ /**
+ * @property {integer | null} innerWindowID
+ * The last known innerWindowID loaded into this tab's docShell. This
+ * property must remain in sync with the last known values of
+ * properties such as `url` and `title`. Any operations on the content
+ * of an out-of-process tab will automatically fail if the
+ * innerWindowID of the tab when the message is received does not match
+ * the value of this property when the message was sent.
+ * @readonly
+ */
+ get innerWindowID() {
+ return this.browser.innerWindowID;
+ }
+
+ /**
+ * @property {boolean} hasTabPermission
+ * Returns true if the extension has permission to access restricted
+ * properties of this tab, such as `url`, `title`, and `favIconUrl`.
+ * @readonly
+ */
+ get hasTabPermission() {
+ return (
+ this.extension.hasPermission("tabs") ||
+ this.hasActiveTabPermission ||
+ this.matchesHostPermission
+ );
+ }
+
+ /**
+ * @property {boolean} hasActiveTabPermission
+ * Returns true if the extension has the "activeTab" permission, and
+ * has been granted access to this tab due to a user executing an
+ * extension action.
+ *
+ * If true, the extension may load scripts and CSS into this tab, and
+ * access restricted properties, such as its `url`.
+ * @readonly
+ */
+ get hasActiveTabPermission() {
+ return (
+ this.extension.hasPermission("activeTab") &&
+ this.activeTabWindowID != null &&
+ this.activeTabWindowID === this.innerWindowID
+ );
+ }
+
+ /**
+ * @property {boolean} matchesHostPermission
+ * Returns true if the extensions host permissions match the current tab url.
+ * @readonly
+ */
+ get matchesHostPermission() {
+ return this.extension.allowedOrigins.matches(this._uri);
+ }
+
+ /**
+ * @property {boolean} incognito
+ * Returns true if this is a private browsing tab, false otherwise.
+ * @readonly
+ */
+ get _incognito() {
+ return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
+ }
+
+ /**
+ * @property {string} _url
+ * Returns the current URL of this tab. Does not do any permission
+ * checks.
+ * @readonly
+ */
+ get _url() {
+ return this.browser.currentURI.spec;
+ }
+
+ /**
+ * @property {string | null} url
+ * Returns the current URL of this tab if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
+ get url() {
+ if (this.hasTabPermission) {
+ return this._url;
+ }
+ }
+
+ /**
+ * @property {nsIURI} _uri
+ * Returns the current URI of this tab.
+ * @readonly
+ */
+ get _uri() {
+ return this.browser.currentURI;
+ }
+
+ /**
+ * @property {string} _title
+ * Returns the current title of this tab. Does not do any permission
+ * checks.
+ * @readonly
+ */
+ get _title() {
+ return this.browser.contentTitle || this.nativeTab.label;
+ }
+
+ /**
+ * @property {nsIURI | null} title
+ * Returns the current title of this tab if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
+ get title() {
+ if (this.hasTabPermission) {
+ return this._title;
+ }
+ }
+
+ /**
+ * @property {string} _favIconUrl
+ * Returns the current favicon URL of this tab. Does not do any permission
+ * checks.
+ * @readonly
+ * @abstract
+ */
+ get _favIconUrl() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {nsIURI | null} faviconUrl
+ * Returns the current faviron URL of this tab if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
+ get favIconUrl() {
+ if (this.hasTabPermission) {
+ return this._favIconUrl;
+ }
+ }
+
+ /**
+ * @property {integer} lastAccessed
+ * Returns the last time the tab was accessed as the number of
+ * milliseconds since epoch.
+ * @readonly
+ * @abstract
+ */
+ get lastAccessed() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} audible
+ * Returns true if the tab is currently playing audio, false otherwise.
+ * @readonly
+ * @abstract
+ */
+ get audible() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {XULElement} browser
+ * Returns the XUL browser for the given tab.
+ * @readonly
+ * @abstract
+ */
+ get browser() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {BrowsingContext} browsingContext
+ * Returns the BrowsingContext for the given tab.
+ * @readonly
+ */
+ get browsingContext() {
+ return this.browser?.browsingContext;
+ }
+
+ /**
+ * @property {FrameLoader} frameLoader
+ * Returns the frameloader for the given tab.
+ * @readonly
+ */
+ get frameLoader() {
+ return this.browser && this.browser.frameLoader;
+ }
+
+ /**
+ * @property {string} cookieStoreId
+ * Returns the cookie store identifier for the given tab.
+ * @readonly
+ * @abstract
+ */
+ get cookieStoreId() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} openerTabId
+ * Returns the ID of the tab which opened this one.
+ * @readonly
+ */
+ get openerTabId() {
+ return null;
+ }
+
+ /**
+ * @property {integer} discarded
+ * Returns true if the tab is discarded.
+ * @readonly
+ * @abstract
+ */
+ get discarded() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} height
+ * Returns the pixel height of the visible area of the tab.
+ * @readonly
+ * @abstract
+ */
+ get height() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} hidden
+ * Returns true if the tab is hidden.
+ * @readonly
+ * @abstract
+ */
+ get hidden() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} index
+ * Returns the index of the tab in its window's tab list.
+ * @readonly
+ * @abstract
+ */
+ get index() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {MutedInfo} mutedInfo
+ * Returns information about the tab's current audio muting status.
+ * @readonly
+ * @abstract
+ */
+ get mutedInfo() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {SharingState} sharingState
+ * Returns object with tab sharingState.
+ * @readonly
+ * @abstract
+ */
+ get sharingState() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} pinned
+ * Returns true if the tab is pinned, false otherwise.
+ * @readonly
+ * @abstract
+ */
+ get pinned() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} active
+ * Returns true if the tab is the currently-selected tab, false
+ * otherwise.
+ * @readonly
+ * @abstract
+ */
+ get active() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} highlighted
+ * Returns true if the tab is highlighted.
+ * @readonly
+ * @abstract
+ */
+ get highlighted() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} selected
+ * An alias for `active`.
+ * @readonly
+ * @abstract
+ */
+ get selected() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {string} status
+ * Returns the current loading status of the tab. May be either
+ * "loading" or "complete".
+ * @readonly
+ * @abstract
+ */
+ get status() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} height
+ * Returns the pixel height of the visible area of the tab.
+ * @readonly
+ * @abstract
+ */
+ get width() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {DOMWindow} window
+ * Returns the browser window to which the tab belongs.
+ * @readonly
+ * @abstract
+ */
+ get window() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} window
+ * Returns the numeric ID of the browser window to which the tab belongs.
+ * @readonly
+ * @abstract
+ */
+ get windowId() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} attention
+ * Returns true if the tab is drawing attention.
+ * @readonly
+ * @abstract
+ */
+ get attention() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} isArticle
+ * Returns true if the document in the tab can be rendered in reader
+ * mode.
+ * @readonly
+ * @abstract
+ */
+ get isArticle() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} isInReaderMode
+ * Returns true if the document in the tab is being rendered in reader
+ * mode.
+ * @readonly
+ * @abstract
+ */
+ get isInReaderMode() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} successorTabId
+ * @readonly
+ * @abstract
+ */
+ get successorTabId() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns true if this tab matches the the given query info object. Omitted
+ * or null have no effect on the match.
+ *
+ * @param {object} queryInfo
+ * The query info against which to match.
+ * @param {boolean} [queryInfo.active]
+ * Matches against the exact value of the tab's `active` attribute.
+ * @param {boolean} [queryInfo.audible]
+ * Matches against the exact value of the tab's `audible` attribute.
+ * @param {string} [queryInfo.cookieStoreId]
+ * Matches against the exact value of the tab's `cookieStoreId` attribute.
+ * @param {boolean} [queryInfo.discarded]
+ * Matches against the exact value of the tab's `discarded` attribute.
+ * @param {boolean} [queryInfo.hidden]
+ * Matches against the exact value of the tab's `hidden` attribute.
+ * @param {boolean} [queryInfo.highlighted]
+ * Matches against the exact value of the tab's `highlighted` attribute.
+ * @param {integer} [queryInfo.index]
+ * Matches against the exact value of the tab's `index` attribute.
+ * @param {boolean} [queryInfo.muted]
+ * Matches against the exact value of the tab's `mutedInfo.muted` attribute.
+ * @param {boolean} [queryInfo.pinned]
+ * Matches against the exact value of the tab's `pinned` attribute.
+ * @param {string} [queryInfo.status]
+ * Matches against the exact value of the tab's `status` attribute.
+ * @param {string} [queryInfo.title]
+ * Matches against the exact value of the tab's `title` attribute.
+ * @param {string|boolean } [queryInfo.screen]
+ * Matches against the exact value of the tab's `sharingState.screen` attribute, or use true to match any screen sharing tab.
+ * @param {boolean} [queryInfo.camera]
+ * Matches against the exact value of the tab's `sharingState.camera` attribute.
+ * @param {boolean} [queryInfo.microphone]
+ * Matches against the exact value of the tab's `sharingState.microphone` attribute.
+ *
+ * Note: Per specification, this should perform a pattern match, rather
+ * than an exact value match, and will do so in the future.
+ * @param {MatchPattern} [queryInfo.url]
+ * Requires the tab's URL to match the given MatchPattern object.
+ *
+ * @returns {boolean}
+ * True if the tab matches the query.
+ */
+ matches(queryInfo) {
+ const PROPS = [
+ "active",
+ "audible",
+ "cookieStoreId",
+ "discarded",
+ "hidden",
+ "highlighted",
+ "index",
+ "openerTabId",
+ "pinned",
+ "status",
+ ];
+
+ function checkProperty(prop, obj) {
+ return queryInfo[prop] != null && queryInfo[prop] !== obj[prop];
+ }
+
+ if (PROPS.some(prop => checkProperty(prop, this))) {
+ return false;
+ }
+
+ if (checkProperty("muted", this.mutedInfo)) {
+ return false;
+ }
+
+ let state = this.sharingState;
+ if (["camera", "microphone"].some(prop => checkProperty(prop, state))) {
+ return false;
+ }
+ // query for screen can be boolean (ie. any) or string (ie. specific).
+ if (queryInfo.screen !== null) {
+ let match =
+ typeof queryInfo.screen == "boolean"
+ ? queryInfo.screen === !!state.screen
+ : queryInfo.screen === state.screen;
+ if (!match) {
+ return false;
+ }
+ }
+ if (queryInfo.url || queryInfo.title) {
+ if (!this.hasTabPermission) {
+ return false;
+ }
+ // Using _uri and _title instead of url/title to avoid repeated permission checks.
+ if (queryInfo.url && !queryInfo.url.matches(this._uri)) {
+ return false;
+ }
+ if (queryInfo.title && !queryInfo.title.matches(this._title)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Converts this tab object to a JSON-compatible object containing the values
+ * of its properties which the extension is permitted to access, in the format
+ * required to be returned by WebExtension APIs.
+ *
+ * @param {Object} [fallbackTabSize]
+ * A geometry data if the lazy geometry data for this tab hasn't been
+ * initialized yet.
+ * @returns {object}
+ */
+ convert(fallbackTabSize = null) {
+ let result = {
+ id: this.id,
+ index: this.index,
+ windowId: this.windowId,
+ highlighted: this.highlighted,
+ active: this.active,
+ attention: this.attention,
+ pinned: this.pinned,
+ status: this.status,
+ hidden: this.hidden,
+ discarded: this.discarded,
+ incognito: this.incognito,
+ width: this.width,
+ height: this.height,
+ lastAccessed: this.lastAccessed,
+ audible: this.audible,
+ mutedInfo: this.mutedInfo,
+ isArticle: this.isArticle,
+ isInReaderMode: this.isInReaderMode,
+ sharingState: this.sharingState,
+ successorTabId: this.successorTabId,
+ cookieStoreId: this.cookieStoreId,
+ };
+
+ // If the tab has not been fully layed-out yet, fallback to the geometry
+ // from a different tab (usually the currently active tab).
+ if (fallbackTabSize && (!result.width || !result.height)) {
+ result.width = fallbackTabSize.width;
+ result.height = fallbackTabSize.height;
+ }
+
+ let opener = this.openerTabId;
+ if (opener) {
+ result.openerTabId = opener;
+ }
+
+ if (this.hasTabPermission) {
+ for (let prop of ["url", "title", "favIconUrl"]) {
+ // We use the underscored variants here to avoid the redundant
+ // permissions checks imposed on the public properties.
+ let val = this[`_${prop}`];
+ if (val) {
+ result[prop] = val;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Query each content process hosting subframes of the tab, return results.
+ * @param {string} message
+ * @param {object} options
+ * @param {number} options.frameID
+ * @param {boolean} options.allFrames
+ * @returns {Promise[]}
+ */
+ async queryContent(message, options) {
+ let { allFrames, frameID } = options;
+
+ /** @type {Map<nsIDOMProcessParent, innerWindowId[]>} */
+ let byProcess = new DefaultMap(() => []);
+
+ // Recursively walk the tab's BC tree, find all frames, group by process.
+ function visit(bc) {
+ let win = bc.currentWindowGlobal;
+ if (win?.domProcess && (!frameID || frameID === bc.id)) {
+ byProcess.get(win.domProcess).push(win.innerWindowId);
+ }
+ if (allFrames || (frameID && !byProcess.size)) {
+ bc.children.forEach(visit);
+ }
+ }
+ visit(this.browsingContext);
+
+ let promises = Array.from(byProcess.entries(), ([proc, windows]) =>
+ proc.getActor("ExtensionContent").sendQuery(message, { windows, options })
+ );
+
+ let results = await Promise.all(promises).catch(err => {
+ if (err.name === "DataCloneError") {
+ let fileName = options.jsPaths.slice(-1)[0] || "<anonymous code>";
+ let message = `Script '${fileName}' result is non-structured-clonable data`;
+ return Promise.reject({ message, fileName });
+ }
+ throw err;
+ });
+ results = results.flat();
+
+ if (!results.length) {
+ if (frameID) {
+ throw new ExtensionError("Frame not found, or missing host permission");
+ }
+
+ let frames = allFrames ? ", and any iframes" : "";
+ throw new ExtensionError(`Missing host permission for the tab${frames}`);
+ }
+
+ if (!allFrames && results.length > 1) {
+ throw new ExtensionError("Internal error: multiple windows matched");
+ }
+
+ return results;
+ }
+
+ /**
+ * Inserts a script or stylesheet in the given tab, and returns a promise
+ * which resolves when the operation has completed.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to perform the injection.
+ * @param {InjectDetails} details
+ * The InjectDetails object, specifying what to inject, where, and
+ * when.
+ * @param {string} kind
+ * The kind of data being injected. Either "script" or "css".
+ * @param {string} method
+ * The name of the method which was called to trigger the injection.
+ * Used to generate appropriate error messages on failure.
+ *
+ * @returns {Promise}
+ * Resolves to the result of the execution, once it has completed.
+ * @private
+ */
+ _execute(context, details, kind, method) {
+ let options = {
+ jsPaths: [],
+ cssPaths: [],
+ removeCSS: method == "removeCSS",
+ extensionId: context.extension.id,
+ };
+
+ // We require a `code` or a `file` property, but we can't accept both.
+ if ((details.code === null) == (details.file === null)) {
+ return Promise.reject({
+ message: `${method} requires either a 'code' or a 'file' property, but not both`,
+ });
+ }
+
+ if (details.frameId !== null && details.allFrames) {
+ return Promise.reject({
+ message: `'frameId' and 'allFrames' are mutually exclusive`,
+ });
+ }
+
+ options.hasActiveTabPermission = this.hasActiveTabPermission;
+ options.matches = this.extension.allowedOrigins.patterns.map(
+ host => host.pattern
+ );
+
+ if (details.code !== null) {
+ options[`${kind}Code`] = details.code;
+ }
+ if (details.file !== null) {
+ let url = context.uri.resolve(details.file);
+ if (!this.extension.isExtensionURL(url)) {
+ return Promise.reject({
+ message: "Files to be injected must be within the extension",
+ });
+ }
+ options[`${kind}Paths`].push(url);
+ }
+ if (details.allFrames) {
+ options.allFrames = details.allFrames;
+ }
+ if (details.frameId !== null) {
+ options.frameID = details.frameId;
+ }
+ if (details.matchAboutBlank) {
+ options.matchAboutBlank = details.matchAboutBlank;
+ }
+ if (details.runAt !== null) {
+ options.runAt = details.runAt;
+ } else {
+ options.runAt = "document_idle";
+ }
+ if (details.cssOrigin !== null) {
+ options.cssOrigin = details.cssOrigin;
+ } else {
+ options.cssOrigin = "author";
+ }
+
+ options.wantReturnValue = true;
+ return this.queryContent("Execute", options);
+ }
+
+ /**
+ * Executes a script in the tab's content window, and returns a Promise which
+ * resolves to the result of the evaluation, or rejects to the value of any
+ * error the injection generates.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to inject the script.
+ * @param {InjectDetails} details
+ * The InjectDetails object, specifying what to inject, where, and
+ * when.
+ *
+ * @returns {Promise}
+ * Resolves to the result of the evaluation of the given script, once
+ * it has completed, or rejects with any error the evaluation
+ * generates.
+ */
+ executeScript(context, details) {
+ return this._execute(context, details, "js", "executeScript");
+ }
+
+ /**
+ * Injects CSS into the tab's content window, and returns a Promise which
+ * resolves when the injection is complete.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to inject the script.
+ * @param {InjectDetails} details
+ * The InjectDetails object, specifying what to inject, and where.
+ *
+ * @returns {Promise}
+ * Resolves when the injection has completed.
+ */
+ insertCSS(context, details) {
+ return this._execute(context, details, "css", "insertCSS").then(() => {});
+ }
+
+ /**
+ * Removes CSS which was previously into the tab's content window via
+ * `insertCSS`, and returns a Promise which resolves when the operation is
+ * complete.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to remove the CSS.
+ * @param {InjectDetails} details
+ * The InjectDetails object, specifying what to remove, and from where.
+ *
+ * @returns {Promise}
+ * Resolves when the operation has completed.
+ */
+ removeCSS(context, details) {
+ return this._execute(context, details, "css", "removeCSS").then(() => {});
+ }
+}
+
+defineLazyGetter(TabBase.prototype, "incognito", function() {
+ return this._incognito;
+});
+
+// Note: These must match the values in windows.json.
+const WINDOW_ID_NONE = -1;
+const WINDOW_ID_CURRENT = -2;
+
+/**
+ * A platform-independent base class for extension-specific wrappers around
+ * native browser windows
+ *
+ * @param {Extension} extension
+ * The extension object for which this wrapper is being created.
+ * @param {DOMWindow} window
+ * The browser DOM window which is being wrapped.
+ * @param {integer} id
+ * The numeric ID of this DOM window object. This ID should be the same for
+ * every extension, and for the lifetime of the window.
+ */
+class WindowBase {
+ constructor(extension, window, id) {
+ if (!extension.canAccessWindow(window)) {
+ throw new ExtensionError("extension cannot access window");
+ }
+ this.extension = extension;
+ this.window = window;
+ this.id = id;
+ }
+
+ /**
+ * @property {nsIAppWindow} appWindow
+ * The nsIAppWindow object for this browser window.
+ * @readonly
+ */
+ get appWindow() {
+ return this.window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow);
+ }
+
+ /**
+ * Returns true if this window is the current window for the given extension
+ * context, false otherwise.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to perform the check.
+ *
+ * @returns {boolean}
+ */
+ isCurrentFor(context) {
+ if (context && context.currentWindow) {
+ return this.window === context.currentWindow;
+ }
+ return this.isLastFocused;
+ }
+
+ /**
+ * @property {string} type
+ * The type of the window, as defined by the WebExtension API. May be
+ * either "normal" or "popup".
+ * @readonly
+ */
+ get type() {
+ let { chromeFlags } = this.appWindow;
+
+ if (chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG) {
+ return "popup";
+ }
+
+ return "normal";
+ }
+
+ /**
+ * Converts this window object to a JSON-compatible object which may be
+ * returned to an extension, in the format required to be returned by
+ * WebExtension APIs.
+ *
+ * @param {object} [getInfo]
+ * An optional object, the properties of which determine what data is
+ * available on the result object.
+ * @param {boolean} [getInfo.populate]
+ * Of true, the result object will contain a `tabs` property,
+ * containing an array of converted Tab objects, one for each tab in
+ * the window.
+ *
+ * @returns {object}
+ */
+ convert(getInfo) {
+ let result = {
+ id: this.id,
+ focused: this.focused,
+ top: this.top,
+ left: this.left,
+ width: this.width,
+ height: this.height,
+ incognito: this.incognito,
+ type: this.type,
+ state: this.state,
+ alwaysOnTop: this.alwaysOnTop,
+ title: this.title,
+ };
+
+ if (getInfo && getInfo.populate) {
+ result.tabs = Array.from(this.getTabs(), tab => tab.convert());
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns true if this window matches the the given query info object. Omitted
+ * or null have no effect on the match.
+ *
+ * @param {object} queryInfo
+ * The query info against which to match.
+ * @param {boolean} [queryInfo.currentWindow]
+ * Matches against against the return value of `isCurrentFor()` for the
+ * given context.
+ * @param {boolean} [queryInfo.lastFocusedWindow]
+ * Matches against the exact value of the window's `isLastFocused` attribute.
+ * @param {boolean} [queryInfo.windowId]
+ * Matches against the exact value of the window's ID, taking into
+ * account the special WINDOW_ID_CURRENT value.
+ * @param {string} [queryInfo.windowType]
+ * Matches against the exact value of the window's `type` attribute.
+ * @param {BaseContext} context
+ * The extension context for which the matching is being performed.
+ * Used to determine the current window for relevant properties.
+ *
+ * @returns {boolean}
+ * True if the window matches the query.
+ */
+ matches(queryInfo, context) {
+ if (
+ queryInfo.lastFocusedWindow !== null &&
+ queryInfo.lastFocusedWindow !== this.isLastFocused
+ ) {
+ return false;
+ }
+
+ if (queryInfo.windowType !== null && queryInfo.windowType !== this.type) {
+ return false;
+ }
+
+ if (queryInfo.windowId !== null) {
+ if (queryInfo.windowId === WINDOW_ID_CURRENT) {
+ if (!this.isCurrentFor(context)) {
+ return false;
+ }
+ } else if (queryInfo.windowId !== this.id) {
+ return false;
+ }
+ }
+
+ if (
+ queryInfo.currentWindow !== null &&
+ queryInfo.currentWindow !== this.isCurrentFor(context)
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @property {boolean} focused
+ * Returns true if the browser window is currently focused.
+ * @readonly
+ * @abstract
+ */
+ get focused() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} top
+ * Returns the pixel offset of the top of the window from the top of
+ * the screen.
+ * @readonly
+ * @abstract
+ */
+ get top() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} left
+ * Returns the pixel offset of the left of the window from the left of
+ * the screen.
+ * @readonly
+ * @abstract
+ */
+ get left() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} width
+ * Returns the pixel width of the window.
+ * @readonly
+ * @abstract
+ */
+ get width() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {integer} height
+ * Returns the pixel height of the window.
+ * @readonly
+ * @abstract
+ */
+ get height() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} incognito
+ * Returns true if this is a private browsing window, false otherwise.
+ * @readonly
+ * @abstract
+ */
+ get incognito() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} alwaysOnTop
+ * Returns true if this window is constrained to always remain above
+ * other windows.
+ * @readonly
+ * @abstract
+ */
+ get alwaysOnTop() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {boolean} isLastFocused
+ * Returns true if this is the browser window which most recently had
+ * focus.
+ * @readonly
+ * @abstract
+ */
+ get isLastFocused() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {string} state
+ * Returns or sets the current state of this window, as determined by
+ * `getState()`.
+ * @abstract
+ */
+ get state() {
+ throw new Error("Not implemented");
+ }
+
+ set state(state) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {nsIURI | null} title
+ * Returns the current title of this window if the extension has permission
+ * to read it, or null otherwise.
+ * @readonly
+ */
+ get title() {
+ // activeTab may be null when a new window is adopting an existing tab as its first tab
+ // (See Bug 1458918 for rationale).
+ if (this.activeTab && this.activeTab.hasTabPermission) {
+ return this._title;
+ }
+ }
+
+ // The JSDoc validator does not support @returns tags in abstract functions or
+ // star functions without return statements.
+ /* eslint-disable valid-jsdoc */
+ /**
+ * Returns the window state of the given window.
+ *
+ * @param {DOMWindow} window
+ * The window for which to return a state.
+ *
+ * @returns {string}
+ * The window's state. One of "normal", "minimized", "maximized",
+ * "fullscreen", or "docked".
+ * @static
+ * @abstract
+ */
+ static getState(window) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns an iterator of TabBase objects for each tab in this window.
+ *
+ * @returns {Iterator<TabBase>}
+ */
+ getTabs() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns an iterator of TabBase objects for each highlighted tab in this window.
+ *
+ * @returns {Iterator<TabBase>}
+ */
+ getHighlightedTabs() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {TabBase} The window's currently active tab.
+ */
+ get activeTab() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns the window's tab at the specified index.
+ *
+ * @param {integer} index
+ * The index of the desired tab.
+ *
+ * @returns {TabBase|undefined}
+ */
+ getTabAtIndex(index) {
+ throw new Error("Not implemented");
+ }
+ /* eslint-enable valid-jsdoc */
+}
+
+Object.assign(WindowBase, { WINDOW_ID_NONE, WINDOW_ID_CURRENT });
+
+/**
+ * The parameter type of "tab-attached" events, which are emitted when a
+ * pre-existing tab is attached to a new window.
+ *
+ * @typedef {Object} TabAttachedEvent
+ * @property {NativeTab} tab
+ * The native tab object in the window to which the tab is being
+ * attached. This may be a different object than was used to represent
+ * the tab in the old window.
+ * @property {integer} tabId
+ * The ID of the tab being attached.
+ * @property {integer} newWindowId
+ * The ID of the window to which the tab is being attached.
+ * @property {integer} newPosition
+ * The position of the tab in the tab list of the new window.
+ */
+
+/**
+ * The parameter type of "tab-detached" events, which are emitted when a
+ * pre-existing tab is detached from a window, in order to be attached to a new
+ * window.
+ *
+ * @typedef {Object} TabDetachedEvent
+ * @property {NativeTab} tab
+ * The native tab object in the window from which the tab is being
+ * detached. This may be a different object than will be used to
+ * represent the tab in the new window.
+ * @property {NativeTab} adoptedBy
+ * The native tab object in the window to which the tab will be attached,
+ * and is adopting the contents of this tab. This may be a different
+ * object than the tab in the previous window.
+ * @property {integer} tabId
+ * The ID of the tab being detached.
+ * @property {integer} oldWindowId
+ * The ID of the window from which the tab is being detached.
+ * @property {integer} oldPosition
+ * The position of the tab in the tab list of the window from which it is
+ * being detached.
+ */
+
+/**
+ * The parameter type of "tab-created" events, which are emitted when a
+ * new tab is created.
+ *
+ * @typedef {Object} TabCreatedEvent
+ * @property {NativeTab} tab
+ * The native tab object for the tab which is being created.
+ */
+
+/**
+ * The parameter type of "tab-removed" events, which are emitted when a
+ * tab is removed and destroyed.
+ *
+ * @typedef {Object} TabRemovedEvent
+ * @property {NativeTab} tab
+ * The native tab object for the tab which is being removed.
+ * @property {integer} tabId
+ * The ID of the tab being removed.
+ * @property {integer} windowId
+ * The ID of the window from which the tab is being removed.
+ * @property {boolean} isWindowClosing
+ * True if the tab is being removed because the window is closing.
+ */
+
+/**
+ * An object containing basic, extension-independent information about the window
+ * and tab that a XUL <browser> belongs to.
+ *
+ * @typedef {Object} BrowserData
+ * @property {integer} tabId
+ * The numeric ID of the tab that a <browser> belongs to, or -1 if it
+ * does not belong to a tab.
+ * @property {integer} windowId
+ * The numeric ID of the browser window that a <browser> belongs to, or -1
+ * if it does not belong to a browser window.
+ */
+
+/**
+ * A platform-independent base class for the platform-specific TabTracker
+ * classes, which track the opening and closing of tabs, and manage the mapping
+ * of them between numeric IDs and native tab objects.
+ *
+ * Instances of this class are EventEmitters which emit the following events,
+ * each with an argument of the given type:
+ *
+ * - "tab-attached" {@link TabAttacheEvent}
+ * - "tab-detached" {@link TabDetachedEvent}
+ * - "tab-created" {@link TabCreatedEvent}
+ * - "tab-removed" {@link TabRemovedEvent}
+ */
+class TabTrackerBase extends EventEmitter {
+ on(...args) {
+ if (!this.initialized) {
+ this.init();
+ }
+
+ return super.on(...args); // eslint-disable-line mozilla/balanced-listeners
+ }
+
+ /**
+ * Called to initialize the tab tracking listeners the first time that an
+ * event listener is added.
+ *
+ * @protected
+ * @abstract
+ */
+ init() {
+ throw new Error("Not implemented");
+ }
+
+ // The JSDoc validator does not support @returns tags in abstract functions or
+ // star functions without return statements.
+ /* eslint-disable valid-jsdoc */
+ /**
+ * Returns the numeric ID for the given native tab.
+ *
+ * @param {NativeTab} nativeTab
+ * The native tab for which to return an ID.
+ *
+ * @returns {integer}
+ * The tab's numeric ID.
+ * @abstract
+ */
+ getId(nativeTab) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns the native tab with the given numeric ID.
+ *
+ * @param {integer} tabId
+ * The numeric ID of the tab to return.
+ * @param {*} default_
+ * The value to return if no tab exists with the given ID.
+ *
+ * @returns {NativeTab}
+ * @throws {ExtensionError}
+ * If no tab exists with the given ID and a default return value is not
+ * provided.
+ * @abstract
+ */
+ getTab(tabId, default_ = undefined) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns basic information about the tab and window that the given browser
+ * belongs to.
+ *
+ * @param {XULElement} browser
+ * The XUL browser element for which to return data.
+ *
+ * @returns {BrowserData}
+ * @abstract
+ */
+ /* eslint-enable valid-jsdoc */
+ getBrowserData(browser) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * @property {NativeTab} activeTab
+ * Returns the native tab object for the active tab in the
+ * most-recently focused window, or null if no live tabs currently
+ * exist.
+ * @abstract
+ */
+ get activeTab() {
+ throw new Error("Not implemented");
+ }
+}
+
+/**
+ * A browser progress listener instance which calls a given listener function
+ * whenever the status of the given browser changes.
+ *
+ * @param {function(Object)} listener
+ * A function to be called whenever the status of a tab's top-level
+ * browser. It is passed an object with a `browser` property pointing to
+ * the XUL browser, and a `status` property with a string description of
+ * the browser's status.
+ * @private
+ */
+class StatusListener {
+ constructor(listener) {
+ this.listener = listener;
+ }
+
+ onStateChange(browser, webProgress, request, stateFlags, statusCode) {
+ if (!webProgress.isTopLevel) {
+ return;
+ }
+
+ let status;
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ status = "loading";
+ } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ status = "complete";
+ }
+ } else if (
+ stateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ statusCode == Cr.NS_BINDING_ABORTED
+ ) {
+ status = "complete";
+ }
+
+ if (status) {
+ this.listener({ browser, status });
+ }
+ }
+
+ onLocationChange(browser, webProgress, request, locationURI, flags) {
+ if (webProgress.isTopLevel) {
+ let status = webProgress.isLoadingDocument ? "loading" : "complete";
+ this.listener({ browser, status, url: locationURI.spec });
+ }
+ }
+}
+
+/**
+ * A platform-independent base class for the platform-specific WindowTracker
+ * classes, which track the opening and closing of windows, and manage the
+ * mapping of them between numeric IDs and native tab objects.
+ */
+class WindowTrackerBase extends EventEmitter {
+ constructor() {
+ super();
+
+ this._handleWindowOpened = this._handleWindowOpened.bind(this);
+
+ this._openListeners = new Set();
+ this._closeListeners = new Set();
+
+ this._listeners = new DefaultMap(() => new Set());
+
+ this._statusListeners = new DefaultWeakMap(listener => {
+ return new StatusListener(listener);
+ });
+
+ this._windowIds = new DefaultWeakMap(window => {
+ return window.docShell.outerWindowID;
+ });
+ }
+
+ isBrowserWindow(window) {
+ let { documentElement } = window.document;
+
+ return documentElement.getAttribute("windowtype") === "navigator:browser";
+ }
+
+ // The JSDoc validator does not support @returns tags in abstract functions or
+ // star functions without return statements.
+ /* eslint-disable valid-jsdoc */
+ /**
+ * Returns an iterator for all currently active browser windows.
+ *
+ * @param {boolean} [includeInomplete = false]
+ * If true, include browser windows which are not yet fully loaded.
+ * Otherwise, only include windows which are.
+ *
+ * @returns {Iterator<DOMWindow>}
+ */
+ /* eslint-enable valid-jsdoc */
+ *browserWindows(includeIncomplete = false) {
+ // The window type parameter is only available once the window's document
+ // element has been created. This means that, when looking for incomplete
+ // browser windows, we need to ignore the type entirely for windows which
+ // haven't finished loading, since we would otherwise skip browser windows
+ // in their early loading stages.
+ // This is particularly important given that the "domwindowcreated" event
+ // fires for browser windows when they're in that in-between state, and just
+ // before we register our own "domwindowcreated" listener.
+
+ for (let window of Services.wm.getEnumerator("")) {
+ let ok = includeIncomplete;
+ if (window.document.readyState === "complete") {
+ ok = this.isBrowserWindow(window);
+ }
+
+ if (ok) {
+ yield window;
+ }
+ }
+ }
+
+ /**
+ * @property {DOMWindow|null} topWindow
+ * The currently active, or topmost, browser window, or null if no
+ * browser window is currently open.
+ * @readonly
+ */
+ get topWindow() {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+ }
+
+ /**
+ * @property {DOMWindow|null} topWindow
+ * The currently active, or topmost, browser window that is not
+ * private browsing, or null if no browser window is currently open.
+ * @readonly
+ */
+ get topNonPBWindow() {
+ return Services.wm.getMostRecentNonPBWindow("navigator:browser");
+ }
+
+ /**
+ * Returns the top window accessible by the extension.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to return the current window.
+ *
+ * @returns {DOMWindow|null}
+ */
+ getTopWindow(context) {
+ if (context && !context.privateBrowsingAllowed) {
+ return this.topNonPBWindow;
+ }
+ return this.topWindow;
+ }
+
+ /**
+ * Returns the numeric ID for the given browser window.
+ *
+ * @param {DOMWindow} window
+ * The DOM window for which to return an ID.
+ *
+ * @returns {integer}
+ * The window's numeric ID.
+ */
+ getId(window) {
+ return this._windowIds.get(window);
+ }
+
+ /**
+ * Returns the browser window to which the given context belongs, or the top
+ * browser window if the context does not belong to a browser window.
+ *
+ * @param {BaseContext} context
+ * The extension context for which to return the current window.
+ *
+ * @returns {DOMWindow|null}
+ */
+ getCurrentWindow(context) {
+ return (context && context.currentWindow) || this.getTopWindow(context);
+ }
+
+ /**
+ * Returns the browser window with the given ID.
+ *
+ * @param {integer} id
+ * The ID of the window to return.
+ * @param {BaseContext} context
+ * The extension context for which the matching is being performed.
+ * Used to determine the current window for relevant properties.
+ * @param {boolean} [strict = true]
+ * If false, undefined will be returned instead of throwing an error
+ * in case no window exists with the given ID.
+ *
+ * @returns {DOMWindow|undefined}
+ * @throws {ExtensionError}
+ * If no window exists with the given ID and `strict` is true.
+ */
+ getWindow(id, context, strict = true) {
+ if (id === WINDOW_ID_CURRENT) {
+ return this.getCurrentWindow(context);
+ }
+
+ let window = Services.wm.getOuterWindowWithId(id);
+ if (
+ window &&
+ !window.closed &&
+ (window.document.readyState !== "complete" ||
+ this.isBrowserWindow(window))
+ ) {
+ if (!context || context.canAccessWindow(window)) {
+ // Tolerate incomplete windows because isBrowserWindow is only reliable
+ // once the window is fully loaded.
+ return window;
+ }
+ }
+
+ if (strict) {
+ throw new ExtensionError(`Invalid window ID: ${id}`);
+ }
+ }
+
+ /**
+ * @property {boolean} _haveListeners
+ * Returns true if any window open or close listeners are currently
+ * registered.
+ * @private
+ */
+ get _haveListeners() {
+ return this._openListeners.size > 0 || this._closeListeners.size > 0;
+ }
+
+ /**
+ * Register the given listener function to be called whenever a new browser
+ * window is opened.
+ *
+ * @param {function(DOMWindow)} listener
+ * The listener function to register.
+ */
+ addOpenListener(listener) {
+ if (!this._haveListeners) {
+ Services.ww.registerNotification(this);
+ }
+
+ this._openListeners.add(listener);
+
+ for (let window of this.browserWindows(true)) {
+ if (window.document.readyState !== "complete") {
+ window.addEventListener("load", this);
+ }
+ }
+ }
+
+ /**
+ * Unregister a listener function registered in a previous addOpenListener
+ * call.
+ *
+ * @param {function(DOMWindow)} listener
+ * The listener function to unregister.
+ */
+ removeOpenListener(listener) {
+ this._openListeners.delete(listener);
+
+ if (!this._haveListeners) {
+ Services.ww.unregisterNotification(this);
+ }
+ }
+
+ /**
+ * Register the given listener function to be called whenever a browser
+ * window is closed.
+ *
+ * @param {function(DOMWindow)} listener
+ * The listener function to register.
+ */
+ addCloseListener(listener) {
+ if (!this._haveListeners) {
+ Services.ww.registerNotification(this);
+ }
+
+ this._closeListeners.add(listener);
+ }
+
+ /**
+ * Unregister a listener function registered in a previous addCloseListener
+ * call.
+ *
+ * @param {function(DOMWindow)} listener
+ * The listener function to unregister.
+ */
+ removeCloseListener(listener) {
+ this._closeListeners.delete(listener);
+
+ if (!this._haveListeners) {
+ Services.ww.unregisterNotification(this);
+ }
+ }
+
+ /**
+ * Handles load events for recently-opened windows, and adds additional
+ * listeners which may only be safely added when the window is fully loaded.
+ *
+ * @param {Event} event
+ * A DOM event to handle.
+ * @private
+ */
+ handleEvent(event) {
+ if (event.type === "load") {
+ event.currentTarget.removeEventListener(event.type, this);
+
+ let window = event.target.defaultView;
+ if (!this.isBrowserWindow(window)) {
+ return;
+ }
+
+ for (let listener of this._openListeners) {
+ try {
+ listener(window);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Observes "domwindowopened" and "domwindowclosed" events, notifies the
+ * appropriate listeners, and adds necessary additional listeners to the new
+ * windows.
+ *
+ * @param {DOMWindow} window
+ * A DOM window.
+ * @param {string} topic
+ * The topic being observed.
+ * @private
+ */
+ observe(window, topic) {
+ if (topic === "domwindowclosed") {
+ if (!this.isBrowserWindow(window)) {
+ return;
+ }
+
+ window.removeEventListener("load", this);
+ for (let listener of this._closeListeners) {
+ try {
+ listener(window);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ } else if (topic === "domwindowopened") {
+ window.addEventListener("load", this);
+ }
+ }
+
+ /**
+ * Add an event listener to be called whenever the given DOM event is received
+ * at the top level of any browser window.
+ *
+ * @param {string} type
+ * The type of event to listen for. May be any valid DOM event name, or
+ * one of the following special cases:
+ *
+ * - "progress": Adds a tab progress listener to every browser window.
+ * - "status": Adds a StatusListener to every tab of every browser
+ * window.
+ * - "domwindowopened": Acts as an alias for addOpenListener.
+ * - "domwindowclosed": Acts as an alias for addCloseListener.
+ * @param {function|object} listener
+ * The listener to invoke in response to the given events.
+ *
+ * @returns {undefined}
+ */
+ addListener(type, listener) {
+ if (type === "domwindowopened") {
+ return this.addOpenListener(listener);
+ } else if (type === "domwindowclosed") {
+ return this.addCloseListener(listener);
+ }
+
+ if (this._listeners.size === 0) {
+ this.addOpenListener(this._handleWindowOpened);
+ }
+
+ if (type === "status") {
+ listener = this._statusListeners.get(listener);
+ type = "progress";
+ }
+
+ this._listeners.get(type).add(listener);
+
+ // Register listener on all existing windows.
+ for (let window of this.browserWindows()) {
+ this._addWindowListener(window, type, listener);
+ }
+ }
+
+ /**
+ * Removes an event listener previously registered via an addListener call.
+ *
+ * @param {string} type
+ * The type of event to stop listening for.
+ * @param {function|object} listener
+ * The listener to remove.
+ *
+ * @returns {undefined}
+ */
+ removeListener(type, listener) {
+ if (type === "domwindowopened") {
+ return this.removeOpenListener(listener);
+ } else if (type === "domwindowclosed") {
+ return this.removeCloseListener(listener);
+ }
+
+ if (type === "status") {
+ listener = this._statusListeners.get(listener);
+ type = "progress";
+ }
+
+ let listeners = this._listeners.get(type);
+ listeners.delete(listener);
+
+ if (listeners.size === 0) {
+ this._listeners.delete(type);
+ if (this._listeners.size === 0) {
+ this.removeOpenListener(this._handleWindowOpened);
+ }
+ }
+
+ // Unregister listener from all existing windows.
+ let useCapture = type === "focus" || type === "blur";
+ for (let window of this.browserWindows()) {
+ if (type === "progress") {
+ this.removeProgressListener(window, listener);
+ } else {
+ window.removeEventListener(type, listener, useCapture);
+ }
+ }
+ }
+
+ /**
+ * Adds a listener for the given event to the given window.
+ *
+ * @param {DOMWindow} window
+ * The browser window to which to add the listener.
+ * @param {string} eventType
+ * The type of DOM event to listen for, or "progress" to add a tab
+ * progress listener.
+ * @param {function|object} listener
+ * The listener to add.
+ * @private
+ */
+ _addWindowListener(window, eventType, listener) {
+ let useCapture = eventType === "focus" || eventType === "blur";
+
+ if (eventType === "progress") {
+ this.addProgressListener(window, listener);
+ } else {
+ window.addEventListener(eventType, listener, useCapture);
+ }
+ }
+
+ /**
+ * A private method which is called whenever a new browser window is opened,
+ * and adds the necessary listeners to it.
+ *
+ * @param {DOMWindow} window
+ * The window being opened.
+ * @private
+ */
+ _handleWindowOpened(window) {
+ for (let [eventType, listeners] of this._listeners) {
+ for (let listener of listeners) {
+ this._addWindowListener(window, eventType, listener);
+ }
+ }
+ }
+
+ /**
+ * Adds a tab progress listener to the given browser window.
+ *
+ * @param {DOMWindow} window
+ * The browser window to which to add the listener.
+ * @param {object} listener
+ * The tab progress listener to add.
+ * @abstract
+ */
+ addProgressListener(window, listener) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Removes a tab progress listener from the given browser window.
+ *
+ * @param {DOMWindow} window
+ * The browser window from which to remove the listener.
+ * @param {object} listener
+ * The tab progress listener to remove.
+ * @abstract
+ */
+ removeProgressListener(window, listener) {
+ throw new Error("Not implemented");
+ }
+}
+
+/**
+ * Manages native tabs, their wrappers, and their dynamic permissions for a
+ * particular extension.
+ *
+ * @param {Extension} extension
+ * The extension for which to manage tabs.
+ */
+class TabManagerBase {
+ constructor(extension) {
+ this.extension = extension;
+
+ this._tabs = new DefaultWeakMap(tab => this.wrapTab(tab));
+ }
+
+ /**
+ * If the extension has requested activeTab permission, grant it those
+ * permissions for the current inner window in the given native tab.
+ *
+ * @param {NativeTab} nativeTab
+ * The native tab for which to grant permissions.
+ */
+ addActiveTabPermission(nativeTab) {
+ if (this.extension.hasPermission("activeTab")) {
+ // Note that, unlike Chrome, we don't currently clear this permission with
+ // the tab navigates. If the inner window is revived from BFCache before
+ // we've granted this permission to a new inner window, the extension
+ // maintains its permissions for it.
+ let tab = this.getWrapper(nativeTab);
+ tab.activeTabWindowID = tab.innerWindowID;
+ }
+ }
+
+ /**
+ * Revoke the extension's activeTab permissions for the current inner window
+ * of the given native tab.
+ *
+ * @param {NativeTab} nativeTab
+ * The native tab for which to revoke permissions.
+ */
+ revokeActiveTabPermission(nativeTab) {
+ this.getWrapper(nativeTab).activeTabWindowID = null;
+ }
+
+ /**
+ * Returns true if the extension has requested activeTab permission, and has
+ * been granted permissions for the current inner window if this tab.
+ *
+ * @param {NativeTab} nativeTab
+ * The native tab for which to check permissions.
+ * @returns {boolean}
+ * True if the extension has activeTab permissions for this tab.
+ */
+ hasActiveTabPermission(nativeTab) {
+ return this.getWrapper(nativeTab).hasActiveTabPermission;
+ }
+
+ /**
+ * Returns true if the extension has permissions to access restricted
+ * properties of the given native tab. In practice, this means that it has
+ * either requested the "tabs" permission or has activeTab permissions for the
+ * given tab.
+ *
+ * NOTE: Never use this method on an object that is not a native tab
+ * for the current platform: this method implicitly generates a wrapper
+ * for the passed nativeTab parameter and the platform-specific tabTracker
+ * instance is likely to store it in a map which is cleared only when the
+ * tab is closed (and so, if nativeTab is not a real native tab, it will
+ * never be cleared from the platform-specific tabTracker instance),
+ * See Bug 1458918 for a rationale.
+ *
+ * @param {NativeTab} nativeTab
+ * The native tab for which to check permissions.
+ * @returns {boolean}
+ * True if the extension has permissions for this tab.
+ */
+ hasTabPermission(nativeTab) {
+ return this.getWrapper(nativeTab).hasTabPermission;
+ }
+
+ /**
+ * Returns this extension's TabBase wrapper for the given native tab. This
+ * method will always return the same wrapper object for any given native tab.
+ *
+ * @param {NativeTab} nativeTab
+ * The tab for which to return a wrapper.
+ *
+ * @returns {TabBase|undefined}
+ * The wrapper for this tab.
+ */
+ getWrapper(nativeTab) {
+ if (this.canAccessTab(nativeTab)) {
+ return this._tabs.get(nativeTab);
+ }
+ }
+
+ /**
+ * Determines access using extension context.
+ *
+ * @param {NativeTab} nativeTab
+ * The tab to check access on.
+ * @returns {boolean}
+ * True if the extension has permissions for this tab.
+ * @protected
+ * @abstract
+ */
+ canAccessTab(nativeTab) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Converts the given native tab to a JSON-compatible object, in the format
+ * required to be returned by WebExtension APIs, which may be safely passed to
+ * extension code.
+ *
+ * @param {NativeTab} nativeTab
+ * The native tab to convert.
+ * @param {Object} [fallbackTabSize]
+ * A geometry data if the lazy geometry data for this tab hasn't been
+ * initialized yet.
+ *
+ * @returns {Object}
+ */
+ convert(nativeTab, fallbackTabSize = null) {
+ return this.getWrapper(nativeTab).convert(fallbackTabSize);
+ }
+
+ // The JSDoc validator does not support @returns tags in abstract functions or
+ // star functions without return statements.
+ /* eslint-disable valid-jsdoc */
+ /**
+ * Returns an iterator of TabBase objects which match the given query info.
+ *
+ * @param {Object|null} [queryInfo = null]
+ * An object containing properties on which to filter. May contain any
+ * properties which are recognized by {@link TabBase#matches} or
+ * {@link WindowBase#matches}. Unknown properties will be ignored.
+ * @param {BaseContext|null} [context = null]
+ * The extension context for which the matching is being performed.
+ * Used to determine the current window for relevant properties.
+ *
+ * @returns {Iterator<TabBase>}
+ */
+ *query(queryInfo = null, context = null) {
+ if (queryInfo) {
+ if (queryInfo.url !== null) {
+ queryInfo.url = parseMatchPatterns([].concat(queryInfo.url), {
+ restrictSchemes: false,
+ });
+ }
+
+ if (queryInfo.title !== null) {
+ try {
+ queryInfo.title = new MatchGlob(queryInfo.title);
+ } catch (e) {
+ throw new ExtensionError(`Invalid title: ${queryInfo.title}`);
+ }
+ }
+ }
+ function* candidates(windowWrapper) {
+ if (queryInfo) {
+ let { active, highlighted, index } = queryInfo;
+ if (active === true) {
+ yield windowWrapper.activeTab;
+ return;
+ }
+ if (index != null) {
+ let tabWrapper = windowWrapper.getTabAtIndex(index);
+ if (tabWrapper) {
+ yield tabWrapper;
+ }
+ return;
+ }
+ if (highlighted === true) {
+ yield* windowWrapper.getHighlightedTabs();
+ return;
+ }
+ }
+ yield* windowWrapper.getTabs();
+ }
+ let windowWrappers = this.extension.windowManager.query(queryInfo, context);
+ for (let windowWrapper of windowWrappers) {
+ for (let tabWrapper of candidates(windowWrapper)) {
+ if (!queryInfo || tabWrapper.matches(queryInfo)) {
+ yield tabWrapper;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns a TabBase wrapper for the tab with the given ID.
+ *
+ * @param {integer} id
+ * The ID of the tab for which to return a wrapper.
+ *
+ * @returns {TabBase}
+ * @throws {ExtensionError}
+ * If no tab exists with the given ID.
+ * @abstract
+ */
+ get(tabId) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns a new TabBase instance wrapping the given native tab.
+ *
+ * @param {NativeTab} nativeTab
+ * The native tab for which to return a wrapper.
+ *
+ * @returns {TabBase}
+ * @protected
+ * @abstract
+ */
+ /* eslint-enable valid-jsdoc */
+ wrapTab(nativeTab) {
+ throw new Error("Not implemented");
+ }
+}
+
+/**
+ * Manages native browser windows and their wrappers for a particular extension.
+ *
+ * @param {Extension} extension
+ * The extension for which to manage windows.
+ */
+class WindowManagerBase {
+ constructor(extension) {
+ this.extension = extension;
+
+ this._windows = new DefaultWeakMap(window => this.wrapWindow(window));
+ }
+
+ /**
+ * Converts the given browser window to a JSON-compatible object, in the
+ * format required to be returned by WebExtension APIs, which may be safely
+ * passed to extension code.
+ *
+ * @param {DOMWindow} window
+ * The browser window to convert.
+ * @param {*} args
+ * Additional arguments to be passed to {@link WindowBase#convert}.
+ *
+ * @returns {Object}
+ */
+ convert(window, ...args) {
+ return this.getWrapper(window).convert(...args);
+ }
+
+ /**
+ * Returns this extension's WindowBase wrapper for the given browser window.
+ * This method will always return the same wrapper object for any given
+ * browser window.
+ *
+ * @param {DOMWindow} window
+ * The browser window for which to return a wrapper.
+ *
+ * @returns {WindowBase|undefined}
+ * The wrapper for this tab.
+ */
+ getWrapper(window) {
+ if (this.extension.canAccessWindow(window)) {
+ return this._windows.get(window);
+ }
+ }
+
+ /**
+ * Returns whether this window can be accessed by the extension in the given
+ * context.
+ *
+ * @param {DOMWindow} window
+ * The browser window that is being tested
+ * @param {BaseContext|null} context
+ * The extension context for which this test is being performed.
+ * @returns {boolean}
+ */
+ canAccessWindow(window, context) {
+ return (
+ (context && context.canAccessWindow(window)) ||
+ this.extension.canAccessWindow(window)
+ );
+ }
+
+ // The JSDoc validator does not support @returns tags in abstract functions or
+ // star functions without return statements.
+ /* eslint-disable valid-jsdoc */
+ /**
+ * Returns an iterator of WindowBase objects which match the given query info.
+ *
+ * @param {Object|null} [queryInfo = null]
+ * An object containing properties on which to filter. May contain any
+ * properties which are recognized by {@link WindowBase#matches}.
+ * Unknown properties will be ignored.
+ * @param {BaseContext|null} [context = null]
+ * The extension context for which the matching is being performed.
+ * Used to determine the current window for relevant properties.
+ *
+ * @returns {Iterator<WindowBase>}
+ */
+ *query(queryInfo = null, context = null) {
+ function* candidates(windowManager) {
+ if (queryInfo) {
+ let { currentWindow, windowId, lastFocusedWindow } = queryInfo;
+ if (currentWindow === true && windowId == null) {
+ windowId = WINDOW_ID_CURRENT;
+ }
+ if (windowId != null) {
+ let window = global.windowTracker.getWindow(windowId, context, false);
+ if (window) {
+ yield windowManager.getWrapper(window);
+ }
+ return;
+ }
+ if (lastFocusedWindow === true) {
+ let window = global.windowTracker.getTopWindow(context);
+ if (window) {
+ yield windowManager.getWrapper(window);
+ }
+ return;
+ }
+ }
+ yield* windowManager.getAll(context);
+ }
+ for (let windowWrapper of candidates(this)) {
+ if (!queryInfo || windowWrapper.matches(queryInfo, context)) {
+ yield windowWrapper;
+ }
+ }
+ }
+
+ /**
+ * Returns a WindowBase wrapper for the browser window with the given ID.
+ *
+ * @param {integer} id
+ * The ID of the browser window for which to return a wrapper.
+ * @param {BaseContext} context
+ * The extension context for which the matching is being performed.
+ * Used to determine the current window for relevant properties.
+ *
+ * @returns{WindowBase}
+ * @throws {ExtensionError}
+ * If no window exists with the given ID.
+ * @abstract
+ */
+ get(windowId, context) {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns an iterator of WindowBase wrappers for each currently existing
+ * browser window.
+ *
+ * @returns {Iterator<WindowBase>}
+ * @abstract
+ */
+ getAll() {
+ throw new Error("Not implemented");
+ }
+
+ /**
+ * Returns a new WindowBase instance wrapping the given browser window.
+ *
+ * @param {DOMWindow} window
+ * The browser window for which to return a wrapper.
+ *
+ * @returns {WindowBase}
+ * @protected
+ * @abstract
+ */
+ wrapWindow(window) {
+ throw new Error("Not implemented");
+ }
+ /* eslint-enable valid-jsdoc */
+}
+
+function getUserContextIdForCookieStoreId(
+ extension,
+ cookieStoreId,
+ isPrivateBrowsing
+) {
+ if (!extension.hasPermission("cookies")) {
+ throw new ExtensionError(
+ `No permission for cookieStoreId: ${cookieStoreId}`
+ );
+ }
+
+ if (!isValidCookieStoreId(cookieStoreId)) {
+ throw new ExtensionError(`Illegal cookieStoreId: ${cookieStoreId}`);
+ }
+
+ if (isPrivateBrowsing && !isPrivateCookieStoreId(cookieStoreId)) {
+ throw new ExtensionError(
+ `Illegal to set non-private cookieStoreId in a private window`
+ );
+ }
+
+ if (!isPrivateBrowsing && isPrivateCookieStoreId(cookieStoreId)) {
+ throw new ExtensionError(
+ `Illegal to set private cookieStoreId in a non-private window`
+ );
+ }
+
+ if (isContainerCookieStoreId(cookieStoreId)) {
+ if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
+ // Container tabs are not supported in perma-private browsing mode - bug 1320757
+ throw new ExtensionError(
+ `Contextual identities are unavailable in permanent private browsing mode`
+ );
+ }
+ if (!containersEnabled) {
+ throw new ExtensionError(`Contextual identities are currently disabled`);
+ }
+ let userContextId = getContainerForCookieStoreId(cookieStoreId);
+ if (!userContextId) {
+ throw new ExtensionError(
+ `No cookie store exists with ID ${cookieStoreId}`
+ );
+ }
+ return userContextId;
+ }
+
+ return Services.scriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
+}
+
+Object.assign(global, {
+ TabTrackerBase,
+ TabManagerBase,
+ TabBase,
+ WindowTrackerBase,
+ WindowManagerBase,
+ WindowBase,
+ getUserContextIdForCookieStoreId,
+});
diff --git a/toolkit/components/extensions/parent/ext-telemetry.js b/toolkit/components/extensions/parent/ext-telemetry.js
new file mode 100644
index 0000000000..6c87e79ed8
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-telemetry.js
@@ -0,0 +1,211 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "TelemetryController",
+ "resource://gre/modules/TelemetryController.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "TelemetryUtils",
+ "resource://gre/modules/TelemetryUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Services",
+ "resource://gre/modules/Services.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionUtils",
+ "resource://gre/modules/ExtensionUtils.jsm"
+);
+
+const SCALAR_TYPES = {
+ count: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ string: Ci.nsITelemetry.SCALAR_TYPE_STRING,
+ boolean: Ci.nsITelemetry.SCALAR_TYPE_BOOLEAN,
+};
+
+// Currently unsupported on Android: blocked on 1220177.
+// See 1280234 c67 for discussion.
+function desktopCheck() {
+ if (AppConstants.MOZ_BUILD_APP !== "browser") {
+ throw new ExtensionUtils.ExtensionError(
+ "This API is only supported on desktop"
+ );
+ }
+}
+
+this.telemetry = class extends ExtensionAPI {
+ getAPI(context) {
+ let { extension } = context;
+ return {
+ telemetry: {
+ submitPing(type, payload, options) {
+ desktopCheck();
+ const manifest = extension.manifest;
+ if (manifest.telemetry) {
+ throw new ExtensionUtils.ExtensionError(
+ "Encryption settings are defined, use submitEncryptedPing instead."
+ );
+ }
+
+ try {
+ TelemetryController.submitExternalPing(type, payload, options);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ submitEncryptedPing(payload, options) {
+ desktopCheck();
+
+ const manifest = extension.manifest;
+ if (!manifest.telemetry) {
+ throw new ExtensionUtils.ExtensionError(
+ "Encrypted telemetry pings require ping_type and public_key to be set in manifest."
+ );
+ }
+
+ if (!(options.schemaName && options.schemaVersion)) {
+ throw new ExtensionUtils.ExtensionError(
+ "Encrypted telemetry pings require schema name and version to be set in options object."
+ );
+ }
+
+ try {
+ const type = manifest.telemetry.ping_type;
+
+ // Optional manifest entries.
+ if (manifest.telemetry.study_name) {
+ options.studyName = manifest.telemetry.study_name;
+ }
+ options.addPioneerId = manifest.telemetry.pioneer_id === true;
+
+ // Required manifest entries.
+ options.useEncryption = true;
+ options.publicKey = manifest.telemetry.public_key.key;
+ options.encryptionKeyId = manifest.telemetry.public_key.id;
+ options.schemaNamespace = manifest.telemetry.schemaNamespace;
+
+ TelemetryController.submitExternalPing(type, payload, options);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ canUpload() {
+ desktopCheck();
+ // Note: remove the ternary and direct pref check when
+ // TelemetryController.canUpload() is implemented (bug 1440089).
+ try {
+ const result =
+ "canUpload" in TelemetryController
+ ? TelemetryController.canUpload()
+ : Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ return result;
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ scalarAdd(name, value) {
+ desktopCheck();
+ try {
+ Services.telemetry.scalarAdd(name, value);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ scalarSet(name, value) {
+ desktopCheck();
+ try {
+ Services.telemetry.scalarSet(name, value);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ scalarSetMaximum(name, value) {
+ desktopCheck();
+ try {
+ Services.telemetry.scalarSetMaximum(name, value);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ keyedScalarAdd(name, key, value) {
+ desktopCheck();
+ try {
+ Services.telemetry.keyedScalarAdd(name, key, value);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ keyedScalarSet(name, key, value) {
+ desktopCheck();
+ try {
+ Services.telemetry.keyedScalarSet(name, key, value);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ keyedScalarSetMaximum(name, key, value) {
+ desktopCheck();
+ try {
+ Services.telemetry.keyedScalarSetMaximum(name, key, value);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ recordEvent(category, method, object, value, extra) {
+ desktopCheck();
+ try {
+ Services.telemetry.recordEvent(
+ category,
+ method,
+ object,
+ value,
+ extra
+ );
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ registerScalars(category, data) {
+ desktopCheck();
+ try {
+ // For each scalar in `data`, replace scalar.kind with
+ // the appropriate nsITelemetry constant.
+ Object.keys(data).forEach(scalar => {
+ data[scalar].kind = SCALAR_TYPES[data[scalar].kind];
+ });
+ Services.telemetry.registerScalars(category, data);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ setEventRecordingEnabled(category, enabled) {
+ desktopCheck();
+ try {
+ Services.telemetry.setEventRecordingEnabled(category, enabled);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ registerEvents(category, data) {
+ desktopCheck();
+ try {
+ Services.telemetry.registerEvents(category, data);
+ } catch (ex) {
+ throw new ExtensionUtils.ExtensionError(ex);
+ }
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-theme.js b/toolkit/components/extensions/parent/ext-theme.js
new file mode 100644
index 0000000000..84433910da
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-theme.js
@@ -0,0 +1,507 @@
+/* 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";
+
+/* global windowTracker, EventManager, EventEmitter */
+
+/* eslint-disable complexity */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "LightweightThemeManager",
+ "resource://gre/modules/LightweightThemeManager.jsm"
+);
+
+const onUpdatedEmitter = new EventEmitter();
+
+// Represents an empty theme for convenience of use
+const emptyTheme = {
+ details: { colors: null, images: null, properties: null },
+};
+
+let defaultTheme = emptyTheme;
+// Map[windowId -> Theme instance]
+let windowOverrides = new Map();
+
+/**
+ * Class representing either a global theme affecting all windows or an override on a specific window.
+ * Any extension updating the theme with a new global theme will replace the singleton defaultTheme.
+ */
+class Theme {
+ /**
+ * Creates a theme instance.
+ *
+ * @param {string} extension Extension that created the theme.
+ * @param {Integer} windowId The windowId where the theme is applied.
+ */
+ constructor({
+ extension,
+ details,
+ darkDetails,
+ windowId,
+ experiment,
+ startupData,
+ }) {
+ this.extension = extension;
+ this.details = details;
+ this.darkDetails = darkDetails;
+ this.windowId = windowId;
+
+ if (startupData && startupData.lwtData) {
+ Object.assign(this, startupData);
+ } else {
+ // TODO(ntim): clean this in bug 1550090
+ this.lwtStyles = {};
+ this.lwtDarkStyles = null;
+ if (darkDetails) {
+ this.lwtDarkStyles = {};
+ }
+
+ if (experiment) {
+ if (extension.experimentsAllowed) {
+ this.lwtStyles.experimental = {
+ colors: {},
+ images: {},
+ properties: {},
+ };
+ const { baseURI } = this.extension;
+ if (experiment.stylesheet) {
+ experiment.stylesheet = baseURI.resolve(experiment.stylesheet);
+ }
+ this.experiment = experiment;
+ } else {
+ const { logger } = this.extension;
+ logger.warn("This extension is not allowed to run theme experiments");
+ return;
+ }
+ }
+ }
+ this.load();
+ }
+
+ /**
+ * Loads a theme by reading the properties from the extension's manifest.
+ * This method will override any currently applied theme.
+ *
+ * @param {Object} details Theme part of the manifest. Supported
+ * properties can be found in the schema under ThemeType.
+ */
+ load() {
+ if (!this.lwtData) {
+ this.loadDetails(this.details, this.lwtStyles);
+ if (this.darkDetails) {
+ this.loadDetails(this.darkDetails, this.lwtDarkStyles);
+ }
+
+ this.lwtData = {
+ theme: this.lwtStyles,
+ darkTheme: this.lwtDarkStyles,
+ };
+
+ if (this.experiment) {
+ this.lwtData.experiment = this.experiment;
+ }
+
+ this.extension.startupData = {
+ lwtData: this.lwtData,
+ lwtStyles: this.lwtStyles,
+ lwtDarkStyles: this.lwtDarkStyles,
+ experiment: this.experiment,
+ };
+ this.extension.saveStartupData();
+ }
+
+ if (this.windowId) {
+ this.lwtData.window = windowTracker.getWindow(
+ this.windowId
+ ).docShell.outerWindowID;
+ windowOverrides.set(this.windowId, this);
+ } else {
+ windowOverrides.clear();
+ defaultTheme = this;
+ LightweightThemeManager.fallbackThemeData = this.lwtData;
+ }
+ onUpdatedEmitter.emit("theme-updated", this.details, this.windowId);
+
+ Services.obs.notifyObservers(
+ this.lwtData,
+ "lightweight-theme-styling-update"
+ );
+ }
+
+ /**
+ * @param {Object} details Details
+ * @param {Object} styles Styles object in which to store the colors.
+ */
+ loadDetails(details, styles) {
+ if (details.colors) {
+ this.loadColors(details.colors, styles);
+ }
+
+ if (details.images) {
+ this.loadImages(details.images, styles);
+ }
+
+ if (details.properties) {
+ this.loadProperties(details.properties, styles);
+ }
+
+ this.loadMetadata(this.extension, styles);
+ }
+
+ /**
+ * Helper method for loading colors found in the extension's manifest.
+ *
+ * @param {Object} colors Dictionary mapping color properties to values.
+ * @param {Object} styles Styles object in which to store the colors.
+ */
+ loadColors(colors, styles) {
+ for (let color of Object.keys(colors)) {
+ let val = colors[color];
+
+ if (!val) {
+ continue;
+ }
+
+ let cssColor = val;
+ if (Array.isArray(val)) {
+ cssColor =
+ "rgb" + (val.length > 3 ? "a" : "") + "(" + val.join(",") + ")";
+ }
+
+ switch (color) {
+ case "frame":
+ styles.accentcolor = cssColor;
+ break;
+ case "frame_inactive":
+ styles.accentcolorInactive = cssColor;
+ break;
+ case "tab_background_text":
+ styles.textcolor = cssColor;
+ break;
+ case "toolbar":
+ styles.toolbarColor = cssColor;
+ break;
+ case "toolbar_text":
+ case "bookmark_text":
+ styles.toolbar_text = cssColor;
+ break;
+ case "icons":
+ styles.icon_color = cssColor;
+ break;
+ case "icons_attention":
+ styles.icon_attention_color = cssColor;
+ break;
+ case "tab_background_separator":
+ case "tab_loading":
+ case "tab_text":
+ case "tab_line":
+ case "tab_selected":
+ case "toolbar_field":
+ case "toolbar_field_text":
+ case "toolbar_field_border":
+ case "toolbar_field_separator":
+ case "toolbar_field_focus":
+ case "toolbar_field_text_focus":
+ case "toolbar_field_border_focus":
+ case "toolbar_top_separator":
+ case "toolbar_bottom_separator":
+ case "toolbar_vertical_separator":
+ case "button_background_hover":
+ case "button_background_active":
+ case "popup":
+ case "popup_text":
+ case "popup_border":
+ case "popup_highlight":
+ case "popup_highlight_text":
+ case "ntp_background":
+ case "ntp_text":
+ case "sidebar":
+ case "sidebar_border":
+ case "sidebar_text":
+ case "sidebar_highlight":
+ case "sidebar_highlight_text":
+ case "toolbar_field_highlight":
+ case "toolbar_field_highlight_text":
+ styles[color] = cssColor;
+ break;
+ default:
+ if (
+ this.experiment &&
+ this.experiment.colors &&
+ color in this.experiment.colors
+ ) {
+ styles.experimental.colors[color] = cssColor;
+ } else {
+ const { logger } = this.extension;
+ logger.warn(`Unrecognized theme property found: colors.${color}`);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Helper method for loading images found in the extension's manifest.
+ *
+ * @param {Object} images Dictionary mapping image properties to values.
+ * @param {Object} styles Styles object in which to store the colors.
+ */
+ loadImages(images, styles) {
+ const { baseURI, logger } = this.extension;
+
+ for (let image of Object.keys(images)) {
+ let val = images[image];
+
+ if (!val) {
+ continue;
+ }
+
+ switch (image) {
+ case "additional_backgrounds": {
+ let backgroundImages = val.map(img => baseURI.resolve(img));
+ styles.additionalBackgrounds = backgroundImages;
+ break;
+ }
+ case "theme_frame": {
+ let resolvedURL = baseURI.resolve(val);
+ styles.headerURL = resolvedURL;
+ break;
+ }
+ default: {
+ if (
+ this.experiment &&
+ this.experiment.images &&
+ image in this.experiment.images
+ ) {
+ styles.experimental.images[image] = baseURI.resolve(val);
+ } else {
+ logger.warn(`Unrecognized theme property found: images.${image}`);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method for preparing properties found in the extension's manifest.
+ * Properties are commonly used to specify more advanced behavior of colors,
+ * images or icons.
+ *
+ * @param {Object} properties Dictionary mapping properties to values.
+ * @param {Object} styles Styles object in which to store the colors.
+ */
+ loadProperties(properties, styles) {
+ let additionalBackgroundsCount =
+ (styles.additionalBackgrounds && styles.additionalBackgrounds.length) ||
+ 0;
+ const assertValidAdditionalBackgrounds = (property, valueCount) => {
+ const { logger } = this.extension;
+ if (!additionalBackgroundsCount) {
+ logger.warn(
+ `The '${property}' property takes effect only when one ` +
+ `or more additional background images are specified using the 'additional_backgrounds' property.`
+ );
+ return false;
+ }
+ if (additionalBackgroundsCount !== valueCount) {
+ logger.warn(
+ `The amount of values specified for '${property}' ` +
+ `(${valueCount}) is not equal to the amount of additional background ` +
+ `images (${additionalBackgroundsCount}), which may lead to unexpected results.`
+ );
+ }
+ return true;
+ };
+
+ for (let property of Object.getOwnPropertyNames(properties)) {
+ let val = properties[property];
+
+ if (!val) {
+ continue;
+ }
+
+ switch (property) {
+ case "additional_backgrounds_alignment": {
+ if (!assertValidAdditionalBackgrounds(property, val.length)) {
+ break;
+ }
+
+ styles.backgroundsAlignment = val.join(",");
+ break;
+ }
+ case "additional_backgrounds_tiling": {
+ if (!assertValidAdditionalBackgrounds(property, val.length)) {
+ break;
+ }
+
+ let tiling = [];
+ for (let i = 0, l = styles.additionalBackgrounds.length; i < l; ++i) {
+ tiling.push(val[i] || "no-repeat");
+ }
+ styles.backgroundsTiling = tiling.join(",");
+ break;
+ }
+ default: {
+ if (
+ this.experiment &&
+ this.experiment.properties &&
+ property in this.experiment.properties
+ ) {
+ styles.experimental.properties[property] = val;
+ } else {
+ const { logger } = this.extension;
+ logger.warn(
+ `Unrecognized theme property found: properties.${property}`
+ );
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method for loading extension metadata required by downstream
+ * consumers.
+ *
+ * @param {Object} extension Extension object.
+ * @param {Object} styles Styles object in which to store the colors.
+ */
+ loadMetadata(extension, styles) {
+ styles.id = extension.id;
+ styles.version = extension.version;
+ }
+
+ static unload(windowId) {
+ let lwtData = {
+ theme: null,
+ };
+
+ if (windowId) {
+ lwtData.window = windowTracker.getWindow(windowId).docShell.outerWindowID;
+ windowOverrides.delete(windowId);
+ } else {
+ windowOverrides.clear();
+ defaultTheme = emptyTheme;
+ LightweightThemeManager.fallbackThemeData = null;
+ }
+ onUpdatedEmitter.emit("theme-updated", {}, windowId);
+
+ Services.obs.notifyObservers(lwtData, "lightweight-theme-styling-update");
+ }
+}
+
+this.theme = class extends ExtensionAPI {
+ onManifestEntry(entryName) {
+ let { extension } = this;
+ let { manifest } = extension;
+
+ defaultTheme = new Theme({
+ extension,
+ details: manifest.theme,
+ darkDetails: manifest.dark_theme,
+ experiment: manifest.theme_experiment,
+ startupData: extension.startupData,
+ });
+ }
+
+ onShutdown(isAppShutdown) {
+ if (isAppShutdown) {
+ return;
+ }
+
+ let { extension } = this;
+ for (let [windowId, theme] of windowOverrides) {
+ if (theme.extension === extension) {
+ Theme.unload(windowId);
+ }
+ }
+
+ if (defaultTheme.extension === extension) {
+ Theme.unload();
+ }
+ }
+
+ getAPI(context) {
+ let { extension } = context;
+
+ return {
+ theme: {
+ getCurrent: windowId => {
+ // Take last focused window when no ID is supplied.
+ if (!windowId) {
+ windowId = windowTracker.getId(windowTracker.topWindow);
+ }
+ // Force access validation for incognito mode by getting the window.
+ if (!windowTracker.getWindow(windowId, context)) {
+ return Promise.reject(`Invalid window ID: ${windowId}`);
+ }
+
+ if (windowOverrides.has(windowId)) {
+ return Promise.resolve(windowOverrides.get(windowId).details);
+ }
+ return Promise.resolve(defaultTheme.details);
+ },
+ update: (windowId, details) => {
+ if (windowId) {
+ const browserWindow = windowTracker.getWindow(windowId, context);
+ if (!browserWindow) {
+ return Promise.reject(`Invalid window ID: ${windowId}`);
+ }
+ }
+
+ new Theme({
+ extension,
+ details,
+ windowId,
+ experiment: this.extension.manifest.theme_experiment,
+ });
+ },
+ reset: windowId => {
+ if (windowId) {
+ const browserWindow = windowTracker.getWindow(windowId, context);
+ if (!browserWindow) {
+ return Promise.reject(`Invalid window ID: ${windowId}`);
+ }
+
+ let theme = windowOverrides.get(windowId) || defaultTheme;
+ if (theme.extension !== extension) {
+ return;
+ }
+ } else if (defaultTheme.extension !== extension) {
+ return;
+ }
+
+ Theme.unload(windowId);
+ },
+ onUpdated: new EventManager({
+ context,
+ name: "theme.onUpdated",
+ register: fire => {
+ let callback = (event, theme, windowId) => {
+ if (windowId) {
+ // Force access validation for incognito mode by getting the window.
+ if (windowTracker.getWindow(windowId, context, false)) {
+ fire.async({ theme, windowId });
+ }
+ } else {
+ fire.async({ theme });
+ }
+ };
+
+ onUpdatedEmitter.on("theme-updated", callback);
+ return () => {
+ onUpdatedEmitter.off("theme-updated", callback);
+ };
+ },
+ }).api(),
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-toolkit.js b/toolkit/components/extensions/parent/ext-toolkit.js
new file mode 100644
index 0000000000..15b4c5c7e5
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-toolkit.js
@@ -0,0 +1,100 @@
+/* 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";
+
+// These are defined on "global" which is used for the same scopes as the other
+// ext-*.js files.
+/* exported getCookieStoreIdForTab, getCookieStoreIdForContainer,
+ getContainerForCookieStoreId,
+ isValidCookieStoreId, isContainerCookieStoreId,
+ EventManager, URL */
+/* global getCookieStoreIdForTab:false,
+ getCookieStoreIdForContainer:false,
+ getContainerForCookieStoreId: false,
+ isValidCookieStoreId:false, isContainerCookieStoreId:false,
+ isDefaultCookieStoreId: false, isPrivateCookieStoreId:false,
+ EventManager: false */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ContextualIdentityService",
+ "resource://gre/modules/ContextualIdentityService.jsm"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+var { ExtensionCommon } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionCommon.jsm"
+);
+
+global.EventEmitter = ExtensionCommon.EventEmitter;
+global.EventManager = ExtensionCommon.EventManager;
+
+/* globals DEFAULT_STORE, PRIVATE_STORE, CONTAINER_STORE */
+
+global.DEFAULT_STORE = "firefox-default";
+global.PRIVATE_STORE = "firefox-private";
+global.CONTAINER_STORE = "firefox-container-";
+
+global.getCookieStoreIdForTab = function(data, tab) {
+ if (data.incognito) {
+ return PRIVATE_STORE;
+ }
+
+ if (tab.userContextId) {
+ return getCookieStoreIdForContainer(tab.userContextId);
+ }
+
+ return DEFAULT_STORE;
+};
+
+global.getCookieStoreIdForOriginAttributes = function(originAttributes) {
+ if (originAttributes.privateBrowsingId) {
+ return PRIVATE_STORE;
+ }
+
+ if (originAttributes.userContextId) {
+ return getCookieStoreIdForContainer(originAttributes.userContextId);
+ }
+
+ return DEFAULT_STORE;
+};
+
+global.isPrivateCookieStoreId = function(storeId) {
+ return storeId == PRIVATE_STORE;
+};
+
+global.isDefaultCookieStoreId = function(storeId) {
+ return storeId == DEFAULT_STORE;
+};
+
+global.isContainerCookieStoreId = function(storeId) {
+ return storeId !== null && storeId.startsWith(CONTAINER_STORE);
+};
+
+global.getCookieStoreIdForContainer = function(containerId) {
+ return CONTAINER_STORE + containerId;
+};
+
+global.getContainerForCookieStoreId = function(storeId) {
+ if (!isContainerCookieStoreId(storeId)) {
+ return null;
+ }
+
+ let containerId = storeId.substring(CONTAINER_STORE.length);
+ if (ContextualIdentityService.getPublicIdentityFromId(containerId)) {
+ return parseInt(containerId, 10);
+ }
+
+ return null;
+};
+
+global.isValidCookieStoreId = function(storeId) {
+ return (
+ isDefaultCookieStoreId(storeId) ||
+ isPrivateCookieStoreId(storeId) ||
+ isContainerCookieStoreId(storeId)
+ );
+};
diff --git a/toolkit/components/extensions/parent/ext-userScripts.js b/toolkit/components/extensions/parent/ext-userScripts.js
new file mode 100644
index 0000000000..70255d9ae7
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-userScripts.js
@@ -0,0 +1,148 @@
+/* -*- 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";
+
+var { ExtensionUtils } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionUtils.jsm"
+);
+
+var { ExtensionError } = ExtensionUtils;
+
+/**
+ * Represents (in the main browser process) a user script.
+ *
+ * @param {UserScriptOptions} details
+ * The options object related to the user script
+ * (which has the properties described in the user_scripts.json
+ * JSON API schema file).
+ */
+class UserScriptParent {
+ constructor(details) {
+ this.scriptId = details.scriptId;
+ this.options = this._convertOptions(details);
+ }
+
+ destroy() {
+ if (this.destroyed) {
+ throw new Error("Unable to destroy UserScriptParent twice");
+ }
+
+ this.destroyed = true;
+ this.options = null;
+ }
+
+ _convertOptions(details) {
+ const options = {
+ matches: details.matches,
+ excludeMatches: details.excludeMatches,
+ includeGlobs: details.includeGlobs,
+ excludeGlobs: details.excludeGlobs,
+ allFrames: details.allFrames,
+ matchAboutBlank: details.matchAboutBlank,
+ runAt: details.runAt || "document_idle",
+ jsPaths: details.js,
+ userScriptOptions: {
+ scriptMetadata: details.scriptMetadata,
+ },
+ };
+
+ return options;
+ }
+
+ serialize() {
+ return this.options;
+ }
+}
+
+this.userScripts = class extends ExtensionAPI {
+ constructor(...args) {
+ super(...args);
+
+ // Map<scriptId -> UserScriptParent>
+ this.userScriptsMap = new Map();
+ }
+
+ getAPI(context) {
+ const { extension } = context;
+
+ // Set of the scriptIds registered from this context.
+ const registeredScriptIds = new Set();
+
+ const unregisterContentScripts = scriptIds => {
+ if (scriptIds.length === 0) {
+ return Promise.resolve();
+ }
+
+ for (let scriptId of scriptIds) {
+ registeredScriptIds.delete(scriptId);
+ extension.registeredContentScripts.delete(scriptId);
+ this.userScriptsMap.delete(scriptId);
+ }
+ extension.updateContentScripts();
+
+ return context.extension.broadcast("Extension:UnregisterContentScripts", {
+ id: context.extension.id,
+ scriptIds,
+ });
+ };
+
+ // Unregister all the scriptId related to a context when it is closed,
+ // and revoke all the created blob urls once the context is destroyed.
+ context.callOnClose({
+ close() {
+ unregisterContentScripts(Array.from(registeredScriptIds));
+ },
+ });
+
+ return {
+ userScripts: {
+ register: async details => {
+ for (let origin of details.matches) {
+ if (!extension.allowedOrigins.subsumes(new MatchPattern(origin))) {
+ throw new ExtensionError(
+ `Permission denied to register a user script for ${origin}`
+ );
+ }
+ }
+
+ const userScript = new UserScriptParent(details);
+ const { scriptId } = userScript;
+
+ this.userScriptsMap.set(scriptId, userScript);
+
+ const scriptOptions = userScript.serialize();
+
+ await extension.broadcast("Extension:RegisterContentScript", {
+ id: extension.id,
+ options: scriptOptions,
+ scriptId,
+ });
+
+ extension.registeredContentScripts.set(scriptId, scriptOptions);
+ extension.updateContentScripts();
+
+ return scriptId;
+ },
+
+ // This method is not available to the extension code, the extension code
+ // doesn't have access to the internally used scriptId, on the contrary
+ // the extension code will call script.unregister on the script API object
+ // that is resolved from the register API method returned promise.
+ unregister: async scriptId => {
+ const userScript = this.userScriptsMap.get(scriptId);
+ if (!userScript) {
+ throw new Error(`No such user script ID: ${scriptId}`);
+ }
+
+ userScript.destroy();
+
+ await unregisterContentScripts([scriptId]);
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-webNavigation.js b/toolkit/components/extensions/parent/ext-webNavigation.js
new file mode 100644
index 0000000000..4a0c1ea275
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-webNavigation.js
@@ -0,0 +1,294 @@
+/* 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 file expects tabTracker to be defined in the global scope (e.g.
+// by ext-browser.js or ext-android.js).
+/* global tabTracker */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "MatchURLFilters",
+ "resource://gre/modules/MatchURLFilters.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "WebNavigation",
+ "resource://gre/modules/WebNavigation.jsm"
+);
+
+const defaultTransitionTypes = {
+ topFrame: "link",
+ subFrame: "auto_subframe",
+};
+
+const frameTransitions = {
+ anyFrame: {
+ qualifiers: ["server_redirect", "client_redirect", "forward_back"],
+ },
+ topFrame: {
+ types: ["reload", "form_submit"],
+ },
+};
+
+const tabTransitions = {
+ topFrame: {
+ qualifiers: ["from_address_bar"],
+ types: ["auto_bookmark", "typed", "keyword", "generated", "link"],
+ },
+ subFrame: {
+ types: ["manual_subframe"],
+ },
+};
+
+const isTopLevelFrame = ({ frameId, parentFrameId }) => {
+ return frameId == 0 && parentFrameId == -1;
+};
+
+const fillTransitionProperties = (eventName, src, dst) => {
+ if (
+ eventName == "onCommitted" ||
+ eventName == "onHistoryStateUpdated" ||
+ eventName == "onReferenceFragmentUpdated"
+ ) {
+ let frameTransitionData = src.frameTransitionData || {};
+ let tabTransitionData = src.tabTransitionData || {};
+
+ let transitionType,
+ transitionQualifiers = [];
+
+ // Fill transition properties for any frame.
+ for (let qualifier of frameTransitions.anyFrame.qualifiers) {
+ if (frameTransitionData[qualifier]) {
+ transitionQualifiers.push(qualifier);
+ }
+ }
+
+ if (isTopLevelFrame(dst)) {
+ for (let type of frameTransitions.topFrame.types) {
+ if (frameTransitionData[type]) {
+ transitionType = type;
+ }
+ }
+
+ for (let qualifier of tabTransitions.topFrame.qualifiers) {
+ if (tabTransitionData[qualifier]) {
+ transitionQualifiers.push(qualifier);
+ }
+ }
+
+ for (let type of tabTransitions.topFrame.types) {
+ if (tabTransitionData[type]) {
+ transitionType = type;
+ }
+ }
+
+ // If transitionType is not defined, defaults it to "link".
+ if (!transitionType) {
+ transitionType = defaultTransitionTypes.topFrame;
+ }
+ } else {
+ // If it is sub-frame, transitionType defaults it to "auto_subframe",
+ // "manual_subframe" is set only in case of a recent user interaction.
+ transitionType = tabTransitionData.link
+ ? "manual_subframe"
+ : defaultTransitionTypes.subFrame;
+ }
+
+ // Fill the transition properties in the webNavigation event object.
+ dst.transitionType = transitionType;
+ dst.transitionQualifiers = transitionQualifiers;
+ }
+};
+
+// Similar to WebRequestEventManager but for WebNavigation.
+class WebNavigationEventManager extends EventManager {
+ constructor(context, eventName) {
+ let name = `webNavigation.${eventName}`;
+ let register = (fire, urlFilters) => {
+ // Don't create a MatchURLFilters instance if the listener does not include any filter.
+ let filters = urlFilters ? new MatchURLFilters(urlFilters.url) : null;
+
+ let listener = data => {
+ if (!data.browser) {
+ return;
+ }
+
+ let data2 = {
+ url: data.url,
+ timeStamp: Date.now(),
+ };
+
+ if (eventName == "onErrorOccurred") {
+ data2.error = data.error;
+ }
+
+ if (data.frameId != undefined) {
+ data2.frameId = data.frameId;
+ data2.parentFrameId = data.parentFrameId;
+ }
+
+ if (data.sourceFrameId != undefined) {
+ data2.sourceFrameId = data.sourceFrameId;
+ }
+
+ // Do not send a webNavigation event when the data.browser is related to a tab from a
+ // new window opened to adopt an existent tab (See Bug 1443221 for a rationale).
+ const chromeWin = data.browser.ownerGlobal;
+
+ if (
+ chromeWin &&
+ chromeWin.gBrowser &&
+ chromeWin.gBrowserInit &&
+ chromeWin.gBrowserInit.isAdoptingTab() &&
+ chromeWin.gBrowser.selectedBrowser === data.browser
+ ) {
+ return;
+ }
+
+ // Fills in tabId typically.
+ Object.assign(data2, tabTracker.getBrowserData(data.browser));
+ if (data2.tabId < 0) {
+ return;
+ }
+
+ if (data.sourceTabBrowser) {
+ data2.sourceTabId = tabTracker.getBrowserData(
+ data.sourceTabBrowser
+ ).tabId;
+ }
+
+ fillTransitionProperties(eventName, data, data2);
+
+ fire.async(data2);
+ };
+
+ WebNavigation[eventName].addListener(listener, filters, context);
+ return () => {
+ WebNavigation[eventName].removeListener(listener);
+ };
+ };
+
+ super({ context, name, register });
+ }
+}
+
+const convertGetFrameResult = (tabId, data) => {
+ return {
+ errorOccurred: data.errorOccurred,
+ url: data.url,
+ tabId,
+ frameId: data.frameId,
+ parentFrameId: data.parentFrameId,
+ };
+};
+
+this.webNavigation = class extends ExtensionAPI {
+ getAPI(context) {
+ let { tabManager } = context.extension;
+
+ return {
+ webNavigation: {
+ onTabReplaced: new EventManager({
+ context,
+ name: "webNavigation.onTabReplaced",
+ register: fire => {
+ return () => {};
+ },
+ }).api(),
+ onBeforeNavigate: new WebNavigationEventManager(
+ context,
+ "onBeforeNavigate"
+ ).api(),
+ onCommitted: new WebNavigationEventManager(
+ context,
+ "onCommitted"
+ ).api(),
+ onDOMContentLoaded: new WebNavigationEventManager(
+ context,
+ "onDOMContentLoaded"
+ ).api(),
+ onCompleted: new WebNavigationEventManager(
+ context,
+ "onCompleted"
+ ).api(),
+ onErrorOccurred: new WebNavigationEventManager(
+ context,
+ "onErrorOccurred"
+ ).api(),
+ onReferenceFragmentUpdated: new WebNavigationEventManager(
+ context,
+ "onReferenceFragmentUpdated"
+ ).api(),
+ onHistoryStateUpdated: new WebNavigationEventManager(
+ context,
+ "onHistoryStateUpdated"
+ ).api(),
+ onCreatedNavigationTarget: new WebNavigationEventManager(
+ context,
+ "onCreatedNavigationTarget"
+ ).api(),
+ getAllFrames(details) {
+ let tab = tabManager.get(details.tabId);
+
+ try {
+ if (tab.discarded) {
+ return null;
+ }
+ } catch (e) {
+ // accessing the tab.discarded getter may reject if not implemented
+ // on the current platform.
+ }
+
+ let { innerWindowID, messageManager } = tab.browser;
+ let recipient = { innerWindowID };
+
+ return context
+ .sendMessage(
+ messageManager,
+ "WebNavigation:GetAllFrames",
+ {},
+ { recipient }
+ )
+ .then(results =>
+ results.map(convertGetFrameResult.bind(null, details.tabId))
+ );
+ },
+ getFrame(details) {
+ let tab = tabManager.get(details.tabId);
+
+ try {
+ if (tab.discarded) {
+ return null;
+ }
+ } catch (e) {
+ // accessing the tab.discarded getter may reject if not implemented
+ // on the current platform.
+ }
+
+ let recipient = {
+ innerWindowID: tab.browser.innerWindowID,
+ };
+
+ let mm = tab.browser.messageManager;
+ return context
+ .sendMessage(
+ mm,
+ "WebNavigation:GetFrame",
+ { options: details },
+ { recipient }
+ )
+ .then(result => {
+ return result
+ ? convertGetFrameResult(details.tabId, result)
+ : Promise.reject({
+ message: `No frame found with frameId: ${details.frameId}`,
+ });
+ });
+ },
+ },
+ };
+ }
+};
diff --git a/toolkit/components/extensions/parent/ext-webRequest.js b/toolkit/components/extensions/parent/ext-webRequest.js
new file mode 100644
index 0000000000..b139326f26
--- /dev/null
+++ b/toolkit/components/extensions/parent/ext-webRequest.js
@@ -0,0 +1,162 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "WebRequest",
+ "resource://gre/modules/WebRequest.jsm"
+);
+
+var { parseMatchPatterns } = ExtensionUtils;
+
+// The guts of a WebRequest event handler. Takes care of converting
+// |details| parameter when invoking listeners.
+function registerEvent(
+ extension,
+ eventName,
+ fire,
+ filter,
+ info,
+ remoteTab = null
+) {
+ let listener = async data => {
+ let event = data.serialize(eventName);
+ if (data.registerTraceableChannel) {
+ // If this is a primed listener, no tabParent was passed in here,
+ // but the convert() callback later in this function will be called
+ // when the background page is started. Force that to happen here
+ // after which we'll have a valid tabParent.
+ if (fire.wakeup) {
+ await fire.wakeup();
+ }
+ data.registerTraceableChannel(extension.policy, remoteTab);
+ }
+
+ return fire.sync(event);
+ };
+
+ let filter2 = {};
+ if (filter.urls) {
+ let perms = new MatchPatternSet([
+ ...extension.allowedOrigins.patterns,
+ ...extension.optionalOrigins.patterns,
+ ]);
+
+ filter2.urls = parseMatchPatterns(filter.urls);
+
+ if (!perms.overlapsAll(filter2.urls)) {
+ Cu.reportError(
+ "The webRequest.addListener filter doesn't overlap with host permissions."
+ );
+ }
+ }
+ if (filter.types) {
+ filter2.types = filter.types;
+ }
+ if (filter.tabId !== undefined) {
+ filter2.tabId = filter.tabId;
+ }
+ if (filter.windowId !== undefined) {
+ filter2.windowId = filter.windowId;
+ }
+ if (filter.incognito !== undefined) {
+ filter2.incognito = filter.incognito;
+ }
+
+ let blockingAllowed = extension.hasPermission("webRequestBlocking");
+
+ let info2 = [];
+ if (info) {
+ for (let desc of info) {
+ if (desc == "blocking" && !blockingAllowed) {
+ // This is usually checked in the child process (based on the API schemas, where these options
+ // should be checked with the "webRequestBlockingPermissionRequired" postprocess property),
+ // but it is worth to also check it here just in case a new webRequest has been added and
+ // it has not yet using the expected postprocess property).
+ Cu.reportError(
+ "Using webRequest.addListener with the blocking option " +
+ "requires the 'webRequestBlocking' permission."
+ );
+ } else {
+ info2.push(desc);
+ }
+ }
+ }
+
+ let listenerDetails = {
+ addonId: extension.id,
+ policy: extension.policy,
+ blockingAllowed,
+ };
+ WebRequest[eventName].addListener(listener, filter2, info2, listenerDetails);
+
+ return {
+ unregister: () => {
+ WebRequest[eventName].removeListener(listener);
+ },
+ convert(_fire, context) {
+ fire = _fire;
+ remoteTab = context.xulBrowser.frameLoader.remoteTab;
+ },
+ };
+}
+
+function makeWebRequestEvent(context, name) {
+ return new EventManager({
+ context,
+ name: `webRequest.${name}`,
+ persistent: {
+ module: "webRequest",
+ event: name,
+ },
+ register: (fire, filter, info) => {
+ return registerEvent(
+ context.extension,
+ name,
+ fire,
+ filter,
+ info,
+ context.xulBrowser.frameLoader.remoteTab
+ ).unregister;
+ },
+ }).api();
+}
+
+this.webRequest = class extends ExtensionAPI {
+ primeListener(extension, event, fire, params) {
+ return registerEvent(extension, event, fire, ...params);
+ }
+
+ getAPI(context) {
+ return {
+ webRequest: {
+ onBeforeRequest: makeWebRequestEvent(context, "onBeforeRequest"),
+ onBeforeSendHeaders: makeWebRequestEvent(
+ context,
+ "onBeforeSendHeaders"
+ ),
+ onSendHeaders: makeWebRequestEvent(context, "onSendHeaders"),
+ onHeadersReceived: makeWebRequestEvent(context, "onHeadersReceived"),
+ onAuthRequired: makeWebRequestEvent(context, "onAuthRequired"),
+ onBeforeRedirect: makeWebRequestEvent(context, "onBeforeRedirect"),
+ onResponseStarted: makeWebRequestEvent(context, "onResponseStarted"),
+ onErrorOccurred: makeWebRequestEvent(context, "onErrorOccurred"),
+ onCompleted: makeWebRequestEvent(context, "onCompleted"),
+ getSecurityInfo: function(requestId, options = {}) {
+ return WebRequest.getSecurityInfo({
+ id: requestId,
+ policy: context.extension.policy,
+ remoteTab: context.xulBrowser.frameLoader.remoteTab,
+ options,
+ });
+ },
+ handlerBehaviorChanged: function() {
+ // TODO: Flush all caches.
+ },
+ },
+ };
+ }
+};