summaryrefslogtreecommitdiffstats
path: root/browser/components/webextensions/ext-tabs.js
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-09 10:39:41 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-09 10:39:41 -0500
commit30de4018913f0cdaea19d1dd12ecd8209e2ed08e (patch)
tree6deebaede10def16e627d489455b4856c4f49386 /browser/components/webextensions/ext-tabs.js
parentac46df8daea09899ce30dc8fd70986e258c746bf (diff)
downloadUXP-30de4018913f0cdaea19d1dd12ecd8209e2ed08e.tar
UXP-30de4018913f0cdaea19d1dd12ecd8209e2ed08e.tar.gz
UXP-30de4018913f0cdaea19d1dd12ecd8209e2ed08e.tar.lz
UXP-30de4018913f0cdaea19d1dd12ecd8209e2ed08e.tar.xz
UXP-30de4018913f0cdaea19d1dd12ecd8209e2ed08e.zip
Rename Basilisk's webextensions component directory to better reflect what it is. Also, add an Application level configure option to disable webextensions
Diffstat (limited to 'browser/components/webextensions/ext-tabs.js')
-rw-r--r--browser/components/webextensions/ext-tabs.js1093
1 files changed, 1093 insertions, 0 deletions
diff --git a/browser/components/webextensions/ext-tabs.js b/browser/components/webextensions/ext-tabs.js
new file mode 100644
index 000000000..bb575aaab
--- /dev/null
+++ b/browser/components/webextensions/ext-tabs.js
@@ -0,0 +1,1093 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+ "@mozilla.org/browser/aboutnewtab-service;1",
+ "nsIAboutNewTabService");
+
+XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern",
+ "resource://gre/modules/MatchPattern.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ EventManager,
+ ignoreEvent,
+} = ExtensionUtils;
+
+// This function is pretty tightly tied to Extension.jsm.
+// Its job is to fill in the |tab| property of the sender.
+function getSender(extension, target, sender) {
+ if ("tabId" in sender) {
+ // The message came from an ExtensionContext. In that case, it should
+ // include a tabId property (which is filled in by the page-open
+ // listener below).
+ let tab = TabManager.getTab(sender.tabId, null, null);
+ delete sender.tabId;
+ if (tab) {
+ sender.tab = TabManager.convert(extension, tab);
+ return;
+ }
+ }
+ if (target instanceof Ci.nsIDOMXULElement) {
+ // If the message was sent from a content script to a <browser> element,
+ // then we can just get the `tab` from `target`.
+ let tabbrowser = target.ownerGlobal.gBrowser;
+ if (tabbrowser) {
+ let tab = tabbrowser.getTabForBrowser(target);
+
+ // `tab` can be `undefined`, e.g. for extension popups. This condition is
+ // reached if `getSender` is called for a popup without a valid `tabId`.
+ if (tab) {
+ sender.tab = TabManager.convert(extension, tab);
+ }
+ }
+ }
+}
+
+// Used by Extension.jsm
+global.tabGetSender = getSender;
+
+/* eslint-disable mozilla/balanced-listeners */
+
+extensions.on("page-shutdown", (type, context) => {
+ if (context.viewType == "tab") {
+ if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) {
+ // Only close extension tabs.
+ // This check prevents about:addons from closing when it contains a
+ // WebExtension as an embedded inline options page.
+ return;
+ }
+ let {gBrowser} = context.xulBrowser.ownerGlobal;
+ if (gBrowser) {
+ let tab = gBrowser.getTabForBrowser(context.xulBrowser);
+ if (tab) {
+ gBrowser.removeTab(tab);
+ }
+ }
+ }
+});
+
+extensions.on("fill-browser-data", (type, browser, data) => {
+ data.tabId = browser ? TabManager.getBrowserId(browser) : -1;
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+global.currentWindow = function(context) {
+ let {xulWindow} = context;
+ if (xulWindow && context.viewType != "background") {
+ return xulWindow;
+ }
+ return WindowManager.topWindow;
+};
+
+let tabListener = {
+ init() {
+ if (this.initialized) {
+ return;
+ }
+
+ this.adoptedTabs = new WeakMap();
+
+ this.handleWindowOpen = this.handleWindowOpen.bind(this);
+ this.handleWindowClose = this.handleWindowClose.bind(this);
+
+ AllWindowEvents.addListener("TabClose", this);
+ AllWindowEvents.addListener("TabOpen", this);
+ WindowListManager.addOpenListener(this.handleWindowOpen);
+ WindowListManager.addCloseListener(this.handleWindowClose);
+
+ EventEmitter.decorate(this);
+
+ this.initialized = true;
+ },
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "TabOpen":
+ if (event.detail.adoptedTab) {
+ this.adoptedTabs.set(event.detail.adoptedTab, event.target);
+ }
+
+ // We need to delay sending this event until the next tick, since the
+ // tab does not have its final index when the TabOpen event is dispatched.
+ Promise.resolve().then(() => {
+ if (event.detail.adoptedTab) {
+ this.emitAttached(event.originalTarget);
+ } else {
+ this.emitCreated(event.originalTarget);
+ }
+ });
+ break;
+
+ case "TabClose":
+ let tab = event.originalTarget;
+
+ if (event.detail.adoptedBy) {
+ this.emitDetached(tab, event.detail.adoptedBy);
+ } else {
+ this.emitRemoved(tab, false);
+ }
+ break;
+ }
+ },
+
+ handleWindowOpen(window) {
+ if (window.arguments[0] instanceof window.XULElement) {
+ // If the first window argument is a XUL element, it means the
+ // window is about to adopt a tab from another window to replace its
+ // initial tab.
+ //
+ // Note that this event handler depends on running before the
+ // delayed startup code in browser.js, which is currently triggered
+ // by the first MozAfterPaint event. That code handles finally
+ // adopting the tab, and clears it from the arguments list in the
+ // process, so if we run later than it, we're too late.
+ let tab = window.arguments[0];
+ this.adoptedTabs.set(tab, window.gBrowser.tabs[0]);
+
+ // We need to be sure to fire this event after the onDetached event
+ // for the original tab.
+ let listener = (event, details) => {
+ if (details.tab == tab) {
+ this.off("tab-detached", listener);
+
+ Promise.resolve().then(() => {
+ this.emitAttached(details.adoptedBy);
+ });
+ }
+ };
+
+ this.on("tab-detached", listener);
+ } else {
+ for (let tab of window.gBrowser.tabs) {
+ this.emitCreated(tab);
+ }
+ }
+ },
+
+ handleWindowClose(window) {
+ for (let tab of window.gBrowser.tabs) {
+ if (this.adoptedTabs.has(tab)) {
+ this.emitDetached(tab, this.adoptedTabs.get(tab));
+ } else {
+ this.emitRemoved(tab, true);
+ }
+ }
+ },
+
+ emitAttached(tab) {
+ let newWindowId = WindowManager.getId(tab.ownerGlobal);
+ let tabId = TabManager.getId(tab);
+
+ this.emit("tab-attached", {tab, tabId, newWindowId, newPosition: tab._tPos});
+ },
+
+ emitDetached(tab, adoptedBy) {
+ let oldWindowId = WindowManager.getId(tab.ownerGlobal);
+ let tabId = TabManager.getId(tab);
+
+ this.emit("tab-detached", {tab, adoptedBy, tabId, oldWindowId, oldPosition: tab._tPos});
+ },
+
+ emitCreated(tab) {
+ this.emit("tab-created", {tab});
+ },
+
+ emitRemoved(tab, isWindowClosing) {
+ let windowId = WindowManager.getId(tab.ownerGlobal);
+ let tabId = TabManager.getId(tab);
+
+ // When addons run in-process, `window.close()` is synchronous. Most other
+ // addon-invoked calls are asynchronous since they go through a proxy
+ // context via the message manager. This includes event registrations such
+ // as `tabs.onRemoved.addListener`.
+ // So, even if `window.close()` were to be called (in-process) after calling
+ // `tabs.onRemoved.addListener`, then the tab would be closed before the
+ // event listener is registered. To make sure that the event listener is
+ // notified, we dispatch `tabs.onRemoved` asynchronously.
+ Services.tm.mainThread.dispatch(() => {
+ this.emit("tab-removed", {tab, tabId, windowId, isWindowClosing});
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ tabReadyInitialized: false,
+ tabReadyPromises: new WeakMap(),
+ initializingTabs: new WeakSet(),
+
+ initTabReady() {
+ if (!this.tabReadyInitialized) {
+ AllWindowEvents.addListener("progress", this);
+
+ this.tabReadyInitialized = true;
+ }
+ },
+
+ onLocationChange(browser, webProgress, request, locationURI, flags) {
+ if (webProgress.isTopLevel) {
+ let gBrowser = browser.ownerGlobal.gBrowser;
+ let tab = gBrowser.getTabForBrowser(browser);
+
+ // Now we are certain that the first page in the tab was loaded.
+ this.initializingTabs.delete(tab);
+
+ // browser.innerWindowID is now set, resolve the promises if any.
+ let deferred = this.tabReadyPromises.get(tab);
+ if (deferred) {
+ deferred.resolve(tab);
+ this.tabReadyPromises.delete(tab);
+ }
+ }
+ },
+
+ /**
+ * Returns a promise that resolves when the tab is ready.
+ * Tabs created via the `tabs.create` method are "ready" once the location
+ * changes to the requested URL. Other tabs are assumed to be ready once their
+ * inner window ID is known.
+ *
+ * @param {XULElement} tab The <tab> element.
+ * @returns {Promise} Resolves with the given tab once ready.
+ */
+ awaitTabReady(tab) {
+ let deferred = this.tabReadyPromises.get(tab);
+ if (!deferred) {
+ deferred = PromiseUtils.defer();
+ if (!this.initializingTabs.has(tab) && tab.linkedBrowser.innerWindowID) {
+ deferred.resolve(tab);
+ } else {
+ this.initTabReady();
+ this.tabReadyPromises.set(tab, deferred);
+ }
+ }
+ return deferred.promise;
+ },
+};
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("startup", () => {
+ tabListener.init();
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("tabs", "addon_parent", context => {
+ let {extension} = context;
+ let self = {
+ tabs: {
+ onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
+ let tab = event.originalTarget;
+ let tabId = TabManager.getId(tab);
+ let windowId = WindowManager.getId(tab.ownerGlobal);
+ fire({tabId, windowId});
+ }).api(),
+
+ onCreated: new EventManager(context, "tabs.onCreated", fire => {
+ let listener = (eventName, event) => {
+ fire(TabManager.convert(extension, event.tab));
+ };
+
+ tabListener.on("tab-created", listener);
+ return () => {
+ tabListener.off("tab-created", listener);
+ };
+ }).api(),
+
+ /**
+ * Since multiple tabs currently can't be highlighted, onHighlighted
+ * essentially acts an alias for self.tabs.onActivated but returns
+ * the tabId in an array to match the API.
+ * @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
+ */
+ onHighlighted: new WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => {
+ let tab = event.originalTarget;
+ let tabIds = [TabManager.getId(tab)];
+ let windowId = WindowManager.getId(tab.ownerGlobal);
+ fire({tabIds, windowId});
+ }).api(),
+
+ onAttached: new EventManager(context, "tabs.onAttached", fire => {
+ let listener = (eventName, event) => {
+ fire(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
+ };
+
+ tabListener.on("tab-attached", listener);
+ return () => {
+ tabListener.off("tab-attached", listener);
+ };
+ }).api(),
+
+ onDetached: new EventManager(context, "tabs.onDetached", fire => {
+ let listener = (eventName, event) => {
+ fire(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
+ };
+
+ tabListener.on("tab-detached", listener);
+ return () => {
+ tabListener.off("tab-detached", listener);
+ };
+ }).api(),
+
+ onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
+ let listener = (eventName, event) => {
+ fire(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
+ };
+
+ tabListener.on("tab-removed", listener);
+ return () => {
+ tabListener.off("tab-removed", listener);
+ };
+ }).api(),
+
+ onReplaced: ignoreEvent(context, "tabs.onReplaced"),
+
+ onMoved: new EventManager(context, "tabs.onMoved", fire => {
+ // There are certain circumstances where we need to ignore a move event.
+ //
+ // Namely, the first time the tab is moved after it's created, we need
+ // to report the final position as the initial position in the tab's
+ // onAttached or onCreated event. This is because most tabs are inserted
+ // in a temporary location and then moved after the TabOpen event fires,
+ // which generates a TabOpen event followed by a TabMove event, which
+ // does not match the contract of our API.
+ let ignoreNextMove = new WeakSet();
+
+ let openListener = event => {
+ ignoreNextMove.add(event.target);
+ // Remove the tab from the set on the next tick, since it will already
+ // have been moved by then.
+ Promise.resolve().then(() => {
+ ignoreNextMove.delete(event.target);
+ });
+ };
+
+ let moveListener = event => {
+ let tab = event.originalTarget;
+
+ if (ignoreNextMove.has(tab)) {
+ ignoreNextMove.delete(tab);
+ return;
+ }
+
+ fire(TabManager.getId(tab), {
+ windowId: WindowManager.getId(tab.ownerGlobal),
+ fromIndex: event.detail,
+ toIndex: tab._tPos,
+ });
+ };
+
+ AllWindowEvents.addListener("TabMove", moveListener);
+ AllWindowEvents.addListener("TabOpen", openListener);
+ return () => {
+ AllWindowEvents.removeListener("TabMove", moveListener);
+ AllWindowEvents.removeListener("TabOpen", openListener);
+ };
+ }).api(),
+
+ onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
+ function sanitize(extension, changeInfo) {
+ let result = {};
+ let nonempty = false;
+ for (let prop in changeInfo) {
+ if ((prop != "favIconUrl" && prop != "url") || extension.hasPermission("tabs")) {
+ nonempty = true;
+ result[prop] = changeInfo[prop];
+ }
+ }
+ return [nonempty, result];
+ }
+
+ let fireForBrowser = (browser, changed) => {
+ let [needed, changeInfo] = sanitize(extension, changed);
+ if (needed) {
+ let gBrowser = browser.ownerGlobal.gBrowser;
+ let tabElem = gBrowser.getTabForBrowser(browser);
+
+ let tab = TabManager.convert(extension, tabElem);
+ fire(tab.id, changeInfo, tab);
+ }
+ };
+
+ let listener = event => {
+ let needed = [];
+ if (event.type == "TabAttrModified") {
+ let changed = event.detail.changed;
+ if (changed.includes("image")) {
+ needed.push("favIconUrl");
+ }
+ if (changed.includes("muted")) {
+ needed.push("mutedInfo");
+ }
+ if (changed.includes("soundplaying")) {
+ needed.push("audible");
+ }
+ } else if (event.type == "TabPinned") {
+ needed.push("pinned");
+ } else if (event.type == "TabUnpinned") {
+ needed.push("pinned");
+ }
+
+ if (needed.length && !extension.hasPermission("tabs")) {
+ needed = needed.filter(attr => attr != "url" && attr != "favIconUrl");
+ }
+
+ if (needed.length) {
+ let tab = TabManager.convert(extension, event.originalTarget);
+
+ let changeInfo = {};
+ for (let prop of needed) {
+ changeInfo[prop] = tab[prop];
+ }
+ fire(tab.id, changeInfo, tab);
+ }
+ };
+ let progressListener = {
+ 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";
+ }
+
+ fireForBrowser(browser, {status});
+ },
+
+ onLocationChange(browser, webProgress, request, locationURI, flags) {
+ if (!webProgress.isTopLevel) {
+ return;
+ }
+
+ fireForBrowser(browser, {
+ status: webProgress.isLoadingDocument ? "loading" : "complete",
+ url: locationURI.spec,
+ });
+ },
+ };
+
+ AllWindowEvents.addListener("progress", progressListener);
+ AllWindowEvents.addListener("TabAttrModified", listener);
+ AllWindowEvents.addListener("TabPinned", listener);
+ AllWindowEvents.addListener("TabUnpinned", listener);
+
+ return () => {
+ AllWindowEvents.removeListener("progress", progressListener);
+ AllWindowEvents.removeListener("TabAttrModified", listener);
+ AllWindowEvents.removeListener("TabPinned", listener);
+ AllWindowEvents.removeListener("TabUnpinned", listener);
+ };
+ }).api(),
+
+ create: function(createProperties) {
+ return new Promise((resolve, reject) => {
+ let window = createProperties.windowId !== null ?
+ WindowManager.getWindow(createProperties.windowId, context) :
+ WindowManager.topWindow;
+ if (!window.gBrowser) {
+ let obs = (finishedWindow, topic, data) => {
+ if (finishedWindow != window) {
+ return;
+ }
+ Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
+ resolve(window);
+ };
+ Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
+ } else {
+ resolve(window);
+ }
+ }).then(window => {
+ let url;
+
+ if (createProperties.url !== null) {
+ url = context.uri.resolve(createProperties.url);
+
+ if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+ return Promise.reject({message: `Illegal URL: ${url}`});
+ }
+ }
+
+ if (createProperties.cookieStoreId && !extension.hasPermission("cookies")) {
+ return Promise.reject({message: `No permission for cookieStoreId: ${createProperties.cookieStoreId}`});
+ }
+
+ let options = {};
+ if (createProperties.cookieStoreId) {
+ if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
+ return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
+ }
+
+ let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
+ if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
+ return Promise.reject({message: `Illegal to set non-private cookieStorageId in a private window`});
+ }
+
+ if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
+ return Promise.reject({message: `Illegal to set private cookieStorageId in a non-private window`});
+ }
+
+ if (global.isContainerCookieStoreId(createProperties.cookieStoreId)) {
+ let containerId = global.getContainerForCookieStoreId(createProperties.cookieStoreId);
+ if (!containerId) {
+ return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
+ }
+
+ options.userContextId = containerId;
+ }
+ }
+
+ tabListener.initTabReady();
+ let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
+
+ let active = true;
+ if (createProperties.active !== null) {
+ active = createProperties.active;
+ }
+ if (active) {
+ window.gBrowser.selectedTab = tab;
+ }
+
+ if (createProperties.index !== null) {
+ window.gBrowser.moveTabTo(tab, createProperties.index);
+ }
+
+ if (createProperties.pinned) {
+ window.gBrowser.pinTab(tab);
+ }
+
+ if (createProperties.url && !createProperties.url.startsWith("about:")) {
+ // We can't wait for a location change event for about:newtab,
+ // since it may be pre-rendered, in which case its initial
+ // location change event has already fired.
+
+ // Mark the tab as initializing, so that operations like
+ // `executeScript` wait until the requested URL is loaded in
+ // the tab before dispatching messages to the inner window
+ // that contains the URL we're attempting to load.
+ tabListener.initializingTabs.add(tab);
+ }
+
+ return TabManager.convert(extension, tab);
+ });
+ },
+
+ remove: function(tabs) {
+ if (!Array.isArray(tabs)) {
+ tabs = [tabs];
+ }
+
+ for (let tabId of tabs) {
+ let tab = TabManager.getTab(tabId, context);
+ tab.ownerGlobal.gBrowser.removeTab(tab);
+ }
+
+ return Promise.resolve();
+ },
+
+ update: function(tabId, updateProperties) {
+ let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ let tabbrowser = tab.ownerGlobal.gBrowser;
+
+ if (updateProperties.url !== null) {
+ let url = context.uri.resolve(updateProperties.url);
+
+ if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+ return Promise.reject({message: `Illegal URL: ${url}`});
+ }
+
+ tab.linkedBrowser.loadURI(url);
+ }
+
+ if (updateProperties.active !== null) {
+ if (updateProperties.active) {
+ tabbrowser.selectedTab = tab;
+ } else {
+ // Not sure what to do here? Which tab should we select?
+ }
+ }
+ if (updateProperties.muted !== null) {
+ if (tab.muted != updateProperties.muted) {
+ tab.toggleMuteAudio(extension.uuid);
+ }
+ }
+ if (updateProperties.pinned !== null) {
+ if (updateProperties.pinned) {
+ tabbrowser.pinTab(tab);
+ } else {
+ tabbrowser.unpinTab(tab);
+ }
+ }
+ // FIXME: highlighted/selected, openerTabId
+
+ return Promise.resolve(TabManager.convert(extension, tab));
+ },
+
+ reload: function(tabId, reloadProperties) {
+ let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (reloadProperties && reloadProperties.bypassCache) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+ }
+ tab.linkedBrowser.reloadWithFlags(flags);
+
+ return Promise.resolve();
+ },
+
+ get: function(tabId) {
+ let tab = TabManager.getTab(tabId, context);
+
+ return Promise.resolve(TabManager.convert(extension, tab));
+ },
+
+ getCurrent() {
+ let tab;
+ if (context.tabId) {
+ tab = TabManager.convert(extension, TabManager.getTab(context.tabId, context));
+ }
+ return Promise.resolve(tab);
+ },
+
+ query: function(queryInfo) {
+ let pattern = null;
+ if (queryInfo.url !== null) {
+ if (!extension.hasPermission("tabs")) {
+ return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
+ }
+
+ pattern = new MatchPattern(queryInfo.url);
+ }
+
+ function matches(tab) {
+ let props = ["active", "pinned", "highlighted", "status", "title", "index"];
+ for (let prop of props) {
+ if (queryInfo[prop] !== null && queryInfo[prop] != tab[prop]) {
+ return false;
+ }
+ }
+
+ if (queryInfo.audible !== null) {
+ if (queryInfo.audible != tab.audible) {
+ return false;
+ }
+ }
+
+ if (queryInfo.muted !== null) {
+ if (queryInfo.muted != tab.mutedInfo.muted) {
+ return false;
+ }
+ }
+
+ if (queryInfo.cookieStoreId !== null &&
+ tab.cookieStoreId != queryInfo.cookieStoreId) {
+ return false;
+ }
+
+ if (pattern && !pattern.matches(Services.io.newURI(tab.url, null, null))) {
+ return false;
+ }
+
+ return true;
+ }
+
+ let result = [];
+ for (let window of WindowListManager.browserWindows()) {
+ let lastFocused = window === WindowManager.topWindow;
+ if (queryInfo.lastFocusedWindow !== null && queryInfo.lastFocusedWindow !== lastFocused) {
+ continue;
+ }
+
+ let windowType = WindowManager.windowType(window);
+ if (queryInfo.windowType !== null && queryInfo.windowType !== windowType) {
+ continue;
+ }
+
+ if (queryInfo.windowId !== null) {
+ if (queryInfo.windowId === WindowManager.WINDOW_ID_CURRENT) {
+ if (currentWindow(context) !== window) {
+ continue;
+ }
+ } else if (queryInfo.windowId !== WindowManager.getId(window)) {
+ continue;
+ }
+ }
+
+ if (queryInfo.currentWindow !== null) {
+ let eq = window === currentWindow(context);
+ if (queryInfo.currentWindow != eq) {
+ continue;
+ }
+ }
+
+ let tabs = TabManager.for(extension).getTabs(window);
+ for (let tab of tabs) {
+ if (matches(tab)) {
+ result.push(tab);
+ }
+ }
+ }
+ return Promise.resolve(result);
+ },
+
+ captureVisibleTab: function(windowId, options) {
+ if (!extension.hasPermission("<all_urls>")) {
+ return Promise.reject({message: "The <all_urls> permission is required to use the captureVisibleTab API"});
+ }
+
+ let window = windowId == null ?
+ WindowManager.topWindow :
+ WindowManager.getWindow(windowId, context);
+
+ let tab = window.gBrowser.selectedTab;
+ return tabListener.awaitTabReady(tab).then(() => {
+ let browser = tab.linkedBrowser;
+ let recipient = {
+ innerWindowID: browser.innerWindowID,
+ };
+
+ if (!options) {
+ options = {};
+ }
+ if (options.format == null) {
+ options.format = "png";
+ }
+ if (options.quality == null) {
+ options.quality = 92;
+ }
+
+ let message = {
+ options,
+ width: browser.clientWidth,
+ height: browser.clientHeight,
+ };
+
+ return context.sendMessage(browser.messageManager, "Extension:Capture",
+ message, {recipient});
+ });
+ },
+
+ detectLanguage: function(tabId) {
+ let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ return tabListener.awaitTabReady(tab).then(() => {
+ let browser = tab.linkedBrowser;
+ let recipient = {innerWindowID: browser.innerWindowID};
+
+ return context.sendMessage(browser.messageManager, "Extension:DetectLanguage",
+ {}, {recipient});
+ });
+ },
+
+ // Used to executeScript, insertCSS and removeCSS.
+ _execute: function(tabId, details, kind, method) {
+ let tab = tabId !== null ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ let options = {
+ js: [],
+ css: [],
+ remove_css: method == "removeCSS",
+ };
+
+ // 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`});
+ }
+
+ if (TabManager.for(extension).hasActiveTabPermission(tab)) {
+ // If we have the "activeTab" permission for this tab, ignore
+ // the host whitelist.
+ options.matchesHost = ["<all_urls>"];
+ } else {
+ options.matchesHost = extension.whiteListedHosts.serialize();
+ }
+
+ if (details.code !== null) {
+ options[kind + "Code"] = details.code;
+ }
+ if (details.file !== null) {
+ let url = context.uri.resolve(details.file);
+ if (!extension.isExtensionURL(url)) {
+ return Promise.reject({message: "Files to be injected must be within the extension"});
+ }
+ options[kind].push(url);
+ }
+ if (details.allFrames) {
+ options.all_frames = details.allFrames;
+ }
+ if (details.frameId !== null) {
+ options.frame_id = details.frameId;
+ }
+ if (details.matchAboutBlank) {
+ options.match_about_blank = details.matchAboutBlank;
+ }
+ if (details.runAt !== null) {
+ options.run_at = details.runAt;
+ } else {
+ options.run_at = "document_idle";
+ }
+
+ return tabListener.awaitTabReady(tab).then(() => {
+ let browser = tab.linkedBrowser;
+ let recipient = {
+ innerWindowID: browser.innerWindowID,
+ };
+
+ return context.sendMessage(browser.messageManager, "Extension:Execute", {options}, {recipient});
+ });
+ },
+
+ executeScript: function(tabId, details) {
+ return self.tabs._execute(tabId, details, "js", "executeScript");
+ },
+
+ insertCSS: function(tabId, details) {
+ return self.tabs._execute(tabId, details, "css", "insertCSS").then(() => {});
+ },
+
+ removeCSS: function(tabId, details) {
+ return self.tabs._execute(tabId, details, "css", "removeCSS").then(() => {});
+ },
+
+ move: function(tabIds, moveProperties) {
+ let index = moveProperties.index;
+ let tabsMoved = [];
+ if (!Array.isArray(tabIds)) {
+ tabIds = [tabIds];
+ }
+
+ let destinationWindow = null;
+ if (moveProperties.windowId !== null) {
+ destinationWindow = WindowManager.getWindow(moveProperties.windowId, context);
+ // Fail on an invalid window.
+ if (!destinationWindow) {
+ return Promise.reject({message: `Invalid window ID: ${moveProperties.windowId}`});
+ }
+ }
+
+ /*
+ Indexes are maintained on a per window basis so that a call to
+ move([tabA, tabB], {index: 0})
+ -> tabA to 0, tabB to 1 if tabA and tabB are in the same window
+ move([tabA, tabB], {index: 0})
+ -> tabA to 0, tabB to 0 if tabA and tabB are in different windows
+ */
+ let indexMap = new Map();
+
+ let tabs = tabIds.map(tabId => TabManager.getTab(tabId, context));
+ for (let tab of tabs) {
+ // If the window is not specified, use the window from the tab.
+ let window = destinationWindow || tab.ownerGlobal;
+ let gBrowser = window.gBrowser;
+
+ let insertionPoint = indexMap.get(window) || index;
+ // If the index is -1 it should go to the end of the tabs.
+ if (insertionPoint == -1) {
+ insertionPoint = gBrowser.tabs.length;
+ }
+
+ // We can only move pinned tabs to a point within, or just after,
+ // the current set of pinned tabs. Unpinned tabs, likewise, can only
+ // be moved to a position after the current set of pinned tabs.
+ // Attempts to move a tab to an illegal position are ignored.
+ let numPinned = gBrowser._numPinnedTabs;
+ let ok = tab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned;
+ if (!ok) {
+ continue;
+ }
+
+ indexMap.set(window, insertionPoint + 1);
+
+ if (tab.ownerGlobal != window) {
+ // If the window we are moving the tab in is different, then move the tab
+ // to the new window.
+ tab = gBrowser.adoptTab(tab, insertionPoint, false);
+ } else {
+ // If the window we are moving is the same, just move the tab.
+ gBrowser.moveTabTo(tab, insertionPoint);
+ }
+ tabsMoved.push(tab);
+ }
+
+ return Promise.resolve(tabsMoved.map(tab => TabManager.convert(extension, tab)));
+ },
+
+ duplicate: function(tabId) {
+ let tab = TabManager.getTab(tabId, context);
+
+ let gBrowser = tab.ownerGlobal.gBrowser;
+ let newTab = gBrowser.duplicateTab(tab);
+
+ return new Promise(resolve => {
+ // We need to use SSTabRestoring because any attributes set before
+ // are ignored. SSTabRestored is too late and results in a jump in
+ // the UI. See http://bit.ly/session-store-api for more information.
+ newTab.addEventListener("SSTabRestoring", function listener() {
+ // As the tab is restoring, move it to the correct position.
+ newTab.removeEventListener("SSTabRestoring", listener);
+ // Pinned tabs that are duplicated are inserted
+ // after the existing pinned tab and pinned.
+ if (tab.pinned) {
+ gBrowser.pinTab(newTab);
+ }
+ gBrowser.moveTabTo(newTab, tab._tPos + 1);
+ });
+
+ newTab.addEventListener("SSTabRestored", function listener() {
+ // Once it has been restored, select it and return the promise.
+ newTab.removeEventListener("SSTabRestored", listener);
+ gBrowser.selectedTab = newTab;
+ return resolve(TabManager.convert(extension, newTab));
+ });
+ });
+ },
+
+ getZoom(tabId) {
+ let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ let {ZoomManager} = tab.ownerGlobal;
+ let zoom = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
+
+ return Promise.resolve(zoom);
+ },
+
+ setZoom(tabId, zoom) {
+ let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ let {FullZoom, ZoomManager} = tab.ownerGlobal;
+
+ if (zoom === 0) {
+ // A value of zero means use the default zoom factor.
+ return FullZoom.reset(tab.linkedBrowser);
+ } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
+ FullZoom.setZoom(zoom, tab.linkedBrowser);
+ } else {
+ return Promise.reject({
+ message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
+ });
+ }
+
+ return Promise.resolve();
+ },
+
+ _getZoomSettings(tabId) {
+ let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ let {FullZoom} = tab.ownerGlobal;
+
+ return {
+ mode: "automatic",
+ scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
+ defaultZoomFactor: 1,
+ };
+ },
+
+ getZoomSettings(tabId) {
+ return Promise.resolve(this._getZoomSettings(tabId));
+ },
+
+ setZoomSettings(tabId, settings) {
+ let tab = tabId ? TabManager.getTab(tabId, context) : TabManager.activeTab;
+
+ let currentSettings = this._getZoomSettings(tab.id);
+
+ if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
+ return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
+ }
+ return Promise.resolve();
+ },
+
+ onZoomChange: new EventManager(context, "tabs.onZoomChange", fire => {
+ let getZoomLevel = browser => {
+ let {ZoomManager} = browser.ownerGlobal;
+
+ return ZoomManager.getZoomForBrowser(browser);
+ };
+
+ // Stores the last known zoom level for each tab's browser.
+ // WeakMap[<browser> -> number]
+ let zoomLevels = new WeakMap();
+
+ // Store the zoom level for all existing tabs.
+ for (let window of WindowListManager.browserWindows()) {
+ for (let tab of window.gBrowser.tabs) {
+ let browser = tab.linkedBrowser;
+ zoomLevels.set(browser, getZoomLevel(browser));
+ }
+ }
+
+ let tabCreated = (eventName, event) => {
+ let browser = event.tab.linkedBrowser;
+ zoomLevels.set(browser, getZoomLevel(browser));
+ };
+
+
+ let zoomListener = event => {
+ let browser = event.originalTarget;
+
+ // For non-remote browsers, this event is dispatched on the document
+ // rather than on the <browser>.
+ if (browser instanceof Ci.nsIDOMDocument) {
+ browser = browser.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ }
+
+ let {gBrowser} = browser.ownerGlobal;
+ let tab = gBrowser.getTabForBrowser(browser);
+ if (!tab) {
+ // We only care about zoom events in the top-level browser of a tab.
+ return;
+ }
+
+ let oldZoomFactor = zoomLevels.get(browser);
+ let newZoomFactor = getZoomLevel(browser);
+
+ if (oldZoomFactor != newZoomFactor) {
+ zoomLevels.set(browser, newZoomFactor);
+
+ let tabId = TabManager.getId(tab);
+ fire({
+ tabId,
+ oldZoomFactor,
+ newZoomFactor,
+ zoomSettings: self.tabs._getZoomSettings(tabId),
+ });
+ }
+ };
+
+ tabListener.on("tab-attached", tabCreated);
+ tabListener.on("tab-created", tabCreated);
+
+ AllWindowEvents.addListener("FullZoomChange", zoomListener);
+ AllWindowEvents.addListener("TextZoomChange", zoomListener);
+ return () => {
+ tabListener.off("tab-attached", tabCreated);
+ tabListener.off("tab-created", tabCreated);
+
+ AllWindowEvents.removeListener("FullZoomChange", zoomListener);
+ AllWindowEvents.removeListener("TextZoomChange", zoomListener);
+ };
+ }).api(),
+ },
+ };
+ return self;
+});