forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1633 lines
		
	
	
	
		
			55 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1633 lines
		
	
	
	
		
			55 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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.defineESModuleGetters(this, {
 | |
|   BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs",
 | |
|   DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs",
 | |
|   ExtensionControlledPopup:
 | |
|     "resource:///modules/ExtensionControlledPopup.sys.mjs",
 | |
|   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
 | |
|   PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
 | |
|   SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
 | |
| });
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(this, "strBundle", function () {
 | |
|   return Services.strings.createBundle(
 | |
|     "chrome://global/locale/extensions.properties"
 | |
|   );
 | |
| });
 | |
| 
 | |
| var { DefaultMap, ExtensionError } = ExtensionUtils;
 | |
| 
 | |
| const TAB_HIDE_CONFIRMED_TYPE = "tabHideNotification";
 | |
| 
 | |
| const TAB_ID_NONE = -1;
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(this, "tabHidePopup", () => {
 | |
|   return new ExtensionControlledPopup({
 | |
|     confirmedType: TAB_HIDE_CONFIRMED_TYPE,
 | |
|     anchorId: "alltabs-button",
 | |
|     popupnotificationId: "extension-tab-hide-notification",
 | |
|     descriptionId: "extension-tab-hide-notification-description",
 | |
|     descriptionMessageId: "tabHideControlled.message",
 | |
|     getLocalizedDescription: (doc, message, addonDetails) => {
 | |
|       let image = doc.createXULElement("image");
 | |
|       image.setAttribute("class", "extension-controlled-icon alltabs-icon");
 | |
|       return BrowserUIUtils.getLocalizedFragment(
 | |
|         doc,
 | |
|         message,
 | |
|         addonDetails,
 | |
|         image
 | |
|       );
 | |
|     },
 | |
|     learnMoreLink: "extension-hiding-tabs",
 | |
|   });
 | |
| });
 | |
| 
 | |
| function showHiddenTabs(id) {
 | |
|   for (let win of Services.wm.getEnumerator("navigator:browser")) {
 | |
|     if (win.closed || !win.gBrowser) {
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     for (let tab of win.gBrowser.tabs) {
 | |
|       if (
 | |
|         tab.hidden &&
 | |
|         tab.ownerGlobal &&
 | |
|         SessionStore.getCustomTabValue(tab, "hiddenBy") === id
 | |
|       ) {
 | |
|         win.gBrowser.showTab(tab);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| let tabListener = {
 | |
|   tabReadyInitialized: false,
 | |
|   // Map[tab -> Promise]
 | |
|   tabBlockedPromises: new WeakMap(),
 | |
|   // Map[tab -> Deferred]
 | |
|   tabReadyPromises: new WeakMap(),
 | |
|   initializingTabs: new WeakSet(),
 | |
| 
 | |
|   initTabReady() {
 | |
|     if (!this.tabReadyInitialized) {
 | |
|       windowTracker.addListener("progress", this);
 | |
| 
 | |
|       this.tabReadyInitialized = true;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onLocationChange(browser, webProgress, request, locationURI, flags) {
 | |
|     if (webProgress.isTopLevel) {
 | |
|       let { gBrowser } = browser.ownerGlobal;
 | |
|       let nativeTab = gBrowser.getTabForBrowser(browser);
 | |
| 
 | |
|       // Now we are certain that the first page in the tab was loaded.
 | |
|       this.initializingTabs.delete(nativeTab);
 | |
| 
 | |
|       // browser.innerWindowID is now set, resolve the promises if any.
 | |
|       let deferred = this.tabReadyPromises.get(nativeTab);
 | |
|       if (deferred) {
 | |
|         deferred.resolve(nativeTab);
 | |
|         this.tabReadyPromises.delete(nativeTab);
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   blockTabUntilRestored(nativeTab) {
 | |
|     let promise = ExtensionUtils.promiseEvent(nativeTab, "SSTabRestored").then(
 | |
|       ({ target }) => {
 | |
|         this.tabBlockedPromises.delete(target);
 | |
|         return target;
 | |
|       }
 | |
|     );
 | |
| 
 | |
|     this.tabBlockedPromises.set(nativeTab, promise);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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} nativeTab The <tab> element.
 | |
|    * @returns {Promise} Resolves with the given tab once ready.
 | |
|    */
 | |
|   awaitTabReady(nativeTab) {
 | |
|     let deferred = this.tabReadyPromises.get(nativeTab);
 | |
|     if (!deferred) {
 | |
|       let promise = this.tabBlockedPromises.get(nativeTab);
 | |
|       if (promise) {
 | |
|         return promise;
 | |
|       }
 | |
|       deferred = PromiseUtils.defer();
 | |
|       if (
 | |
|         !this.initializingTabs.has(nativeTab) &&
 | |
|         (nativeTab.linkedBrowser.innerWindowID ||
 | |
|           nativeTab.linkedBrowser.currentURI.spec === "about:blank")
 | |
|       ) {
 | |
|         deferred.resolve(nativeTab);
 | |
|       } else {
 | |
|         this.initTabReady();
 | |
|         this.tabReadyPromises.set(nativeTab, deferred);
 | |
|       }
 | |
|     }
 | |
|     return deferred.promise;
 | |
|   },
 | |
| };
 | |
| 
 | |
| const allAttrs = new Set([
 | |
|   "attention",
 | |
|   "audible",
 | |
|   "favIconUrl",
 | |
|   "mutedInfo",
 | |
|   "sharingState",
 | |
|   "title",
 | |
|   "autoDiscardable",
 | |
| ]);
 | |
| const allProperties = new Set([
 | |
|   "attention",
 | |
|   "audible",
 | |
|   "autoDiscardable",
 | |
|   "discarded",
 | |
|   "favIconUrl",
 | |
|   "hidden",
 | |
|   "isArticle",
 | |
|   "mutedInfo",
 | |
|   "pinned",
 | |
|   "sharingState",
 | |
|   "status",
 | |
|   "title",
 | |
|   "url",
 | |
| ]);
 | |
| const restricted = new Set(["url", "favIconUrl", "title"]);
 | |
| 
 | |
| this.tabs = class extends ExtensionAPIPersistent {
 | |
|   static onUpdate(id, manifest) {
 | |
|     if (!manifest.permissions || !manifest.permissions.includes("tabHide")) {
 | |
|       showHiddenTabs(id);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   static onDisable(id) {
 | |
|     showHiddenTabs(id);
 | |
|     tabHidePopup.clearConfirmation(id);
 | |
|   }
 | |
| 
 | |
|   static onUninstall(id) {
 | |
|     tabHidePopup.clearConfirmation(id);
 | |
|   }
 | |
| 
 | |
|   tabEventRegistrar({ event, listener }) {
 | |
|     let { extension } = this;
 | |
|     let { tabManager } = extension;
 | |
|     return ({ fire }) => {
 | |
|       let listener2 = (eventName, eventData, ...args) => {
 | |
|         if (!tabManager.canAccessTab(eventData.nativeTab)) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         listener(fire, eventData, ...args);
 | |
|       };
 | |
| 
 | |
|       tabTracker.on(event, listener2);
 | |
|       return {
 | |
|         unregister() {
 | |
|           tabTracker.off(event, listener2);
 | |
|         },
 | |
|         convert(_fire) {
 | |
|           fire = _fire;
 | |
|         },
 | |
|       };
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   PERSISTENT_EVENTS = {
 | |
|     onActivated: this.tabEventRegistrar({
 | |
|       event: "tab-activated",
 | |
|       listener: (fire, event) => {
 | |
|         let { extension } = this;
 | |
|         let { tabId, windowId, previousTabId, previousTabIsPrivate } = event;
 | |
|         if (previousTabIsPrivate && !extension.privateBrowsingAllowed) {
 | |
|           previousTabId = undefined;
 | |
|         }
 | |
|         fire.async({ tabId, previousTabId, windowId });
 | |
|       },
 | |
|     }),
 | |
|     onAttached: this.tabEventRegistrar({
 | |
|       event: "tab-attached",
 | |
|       listener: (fire, event) => {
 | |
|         fire.async(event.tabId, {
 | |
|           newWindowId: event.newWindowId,
 | |
|           newPosition: event.newPosition,
 | |
|         });
 | |
|       },
 | |
|     }),
 | |
|     onCreated: this.tabEventRegistrar({
 | |
|       event: "tab-created",
 | |
|       listener: (fire, event) => {
 | |
|         let { tabManager } = this.extension;
 | |
|         fire.async(tabManager.convert(event.nativeTab, event.currentTabSize));
 | |
|       },
 | |
|     }),
 | |
|     onDetached: this.tabEventRegistrar({
 | |
|       event: "tab-detached",
 | |
|       listener: (fire, event) => {
 | |
|         fire.async(event.tabId, {
 | |
|           oldWindowId: event.oldWindowId,
 | |
|           oldPosition: event.oldPosition,
 | |
|         });
 | |
|       },
 | |
|     }),
 | |
|     onRemoved: this.tabEventRegistrar({
 | |
|       event: "tab-removed",
 | |
|       listener: (fire, event) => {
 | |
|         fire.async(event.tabId, {
 | |
|           windowId: event.windowId,
 | |
|           isWindowClosing: event.isWindowClosing,
 | |
|         });
 | |
|       },
 | |
|     }),
 | |
|     onMoved({ fire }) {
 | |
|       let { tabManager } = this.extension;
 | |
|       let moveListener = event => {
 | |
|         let nativeTab = event.originalTarget;
 | |
|         if (tabManager.canAccessTab(nativeTab)) {
 | |
|           fire.async(tabTracker.getId(nativeTab), {
 | |
|             windowId: windowTracker.getId(nativeTab.ownerGlobal),
 | |
|             fromIndex: event.detail,
 | |
|             toIndex: nativeTab._tPos,
 | |
|           });
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       windowTracker.addListener("TabMove", moveListener);
 | |
|       return {
 | |
|         unregister() {
 | |
|           windowTracker.removeListener("TabMove", moveListener);
 | |
|         },
 | |
|         convert(_fire) {
 | |
|           fire = _fire;
 | |
|         },
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     onHighlighted({ fire, context }) {
 | |
|       let { windowManager } = this.extension;
 | |
|       let highlightListener = (eventName, event) => {
 | |
|         // TODO see if we can avoid "context" here
 | |
|         let window = windowTracker.getWindow(event.windowId, context, false);
 | |
|         if (!window) {
 | |
|           return;
 | |
|         }
 | |
|         let windowWrapper = windowManager.getWrapper(window);
 | |
|         if (!windowWrapper) {
 | |
|           return;
 | |
|         }
 | |
|         let tabIds = Array.from(
 | |
|           windowWrapper.getHighlightedTabs(),
 | |
|           tab => tab.id
 | |
|         );
 | |
|         fire.async({ tabIds: tabIds, windowId: event.windowId });
 | |
|       };
 | |
| 
 | |
|       tabTracker.on("tabs-highlighted", highlightListener);
 | |
|       return {
 | |
|         unregister() {
 | |
|           tabTracker.off("tabs-highlighted", highlightListener);
 | |
|         },
 | |
|         convert(_fire, _context) {
 | |
|           fire = _fire;
 | |
|           context = _context;
 | |
|         },
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     onUpdated({ fire, context }, params) {
 | |
|       let { extension } = this;
 | |
|       let { tabManager } = extension;
 | |
|       let [filterProps] = params;
 | |
|       let filter = { ...filterProps };
 | |
|       if (filter.urls) {
 | |
|         filter.urls = new MatchPatternSet(filter.urls, {
 | |
|           restrictSchemes: false,
 | |
|         });
 | |
|       }
 | |
|       let needsModified = true;
 | |
|       if (filter.properties) {
 | |
|         // Default is to listen for all events.
 | |
|         needsModified = filter.properties.some(p => allAttrs.has(p));
 | |
|         filter.properties = new Set(filter.properties);
 | |
|       } else {
 | |
|         filter.properties = allProperties;
 | |
|       }
 | |
| 
 | |
|       function sanitize(tab, changeInfo) {
 | |
|         let result = {};
 | |
|         let nonempty = false;
 | |
|         for (let prop in changeInfo) {
 | |
|           // In practice, changeInfo contains at most one property from
 | |
|           // restricted. Therefore it is not necessary to cache the value
 | |
|           // of tab.hasTabPermission outside the loop.
 | |
|           // Unnecessarily accessing tab.hasTabPermission can cause bugs, see
 | |
|           // https://bugzilla.mozilla.org/show_bug.cgi?id=1694699#c21
 | |
|           if (!restricted.has(prop) || tab.hasTabPermission) {
 | |
|             nonempty = true;
 | |
|             result[prop] = changeInfo[prop];
 | |
|           }
 | |
|         }
 | |
|         return nonempty && result;
 | |
|       }
 | |
| 
 | |
|       function getWindowID(windowId) {
 | |
|         if (windowId === Window.WINDOW_ID_CURRENT) {
 | |
|           let window = windowTracker.getTopWindow(context);
 | |
|           if (!window) {
 | |
|             return undefined;
 | |
|           }
 | |
|           return windowTracker.getId(window);
 | |
|         }
 | |
|         return windowId;
 | |
|       }
 | |
| 
 | |
|       function matchFilters(tab, changed) {
 | |
|         if (!filterProps) {
 | |
|           return true;
 | |
|         }
 | |
|         if (filter.tabId != null && tab.id != filter.tabId) {
 | |
|           return false;
 | |
|         }
 | |
|         if (
 | |
|           filter.windowId != null &&
 | |
|           tab.windowId != getWindowID(filter.windowId)
 | |
|         ) {
 | |
|           return false;
 | |
|         }
 | |
|         if (filter.urls) {
 | |
|           return filter.urls.matches(tab._uri) && tab.hasTabPermission;
 | |
|         }
 | |
|         return true;
 | |
|       }
 | |
| 
 | |
|       let fireForTab = (tab, changed, nativeTab) => {
 | |
|         // Tab may be null if private and not_allowed.
 | |
|         if (!tab || !matchFilters(tab, changed)) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         let changeInfo = sanitize(tab, changed);
 | |
|         if (changeInfo) {
 | |
|           tabTracker.maybeWaitForTabOpen(nativeTab).then(() => {
 | |
|             if (!nativeTab.parentNode) {
 | |
|               // If the tab is already be destroyed, do nothing.
 | |
|               return;
 | |
|             }
 | |
|             fire.async(tab.id, changeInfo, tab.convert());
 | |
|           });
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       let listener = event => {
 | |
|         // Ignore any events prior to TabOpen
 | |
|         // and events that are triggered while tabs are swapped between windows.
 | |
|         if (event.originalTarget.initializingTab) {
 | |
|           return;
 | |
|         }
 | |
|         if (!extension.canAccessWindow(event.originalTarget.ownerGlobal)) {
 | |
|           return;
 | |
|         }
 | |
|         let needed = [];
 | |
|         if (event.type == "TabAttrModified") {
 | |
|           let changed = event.detail.changed;
 | |
|           if (
 | |
|             changed.includes("image") &&
 | |
|             filter.properties.has("favIconUrl")
 | |
|           ) {
 | |
|             needed.push("favIconUrl");
 | |
|           }
 | |
|           if (changed.includes("muted") && filter.properties.has("mutedInfo")) {
 | |
|             needed.push("mutedInfo");
 | |
|           }
 | |
|           if (
 | |
|             changed.includes("soundplaying") &&
 | |
|             filter.properties.has("audible")
 | |
|           ) {
 | |
|             needed.push("audible");
 | |
|           }
 | |
|           if (
 | |
|             changed.includes("undiscardable") &&
 | |
|             filter.properties.has("autoDiscardable")
 | |
|           ) {
 | |
|             needed.push("autoDiscardable");
 | |
|           }
 | |
|           if (changed.includes("label") && filter.properties.has("title")) {
 | |
|             needed.push("title");
 | |
|           }
 | |
|           if (
 | |
|             changed.includes("sharing") &&
 | |
|             filter.properties.has("sharingState")
 | |
|           ) {
 | |
|             needed.push("sharingState");
 | |
|           }
 | |
|           if (
 | |
|             changed.includes("attention") &&
 | |
|             filter.properties.has("attention")
 | |
|           ) {
 | |
|             needed.push("attention");
 | |
|           }
 | |
|         } else if (event.type == "TabPinned") {
 | |
|           needed.push("pinned");
 | |
|         } else if (event.type == "TabUnpinned") {
 | |
|           needed.push("pinned");
 | |
|         } else if (event.type == "TabBrowserInserted") {
 | |
|           // This may be an adopted tab. Bail early to avoid asking tabManager
 | |
|           // about the tab before we run the adoption logic in ext-browser.js.
 | |
|           if (event.detail.insertedOnTabCreation) {
 | |
|             return;
 | |
|           }
 | |
|           needed.push("discarded");
 | |
|         } else if (event.type == "TabBrowserDiscarded") {
 | |
|           needed.push("discarded");
 | |
|         } else if (event.type == "TabShow") {
 | |
|           needed.push("hidden");
 | |
|         } else if (event.type == "TabHide") {
 | |
|           needed.push("hidden");
 | |
|         }
 | |
| 
 | |
|         let tab = tabManager.getWrapper(event.originalTarget);
 | |
| 
 | |
|         let changeInfo = {};
 | |
|         for (let prop of needed) {
 | |
|           changeInfo[prop] = tab[prop];
 | |
|         }
 | |
| 
 | |
|         fireForTab(tab, changeInfo, event.originalTarget);
 | |
|       };
 | |
| 
 | |
|       let statusListener = ({ browser, status, url }) => {
 | |
|         let { gBrowser } = browser.ownerGlobal;
 | |
|         let tabElem = gBrowser.getTabForBrowser(browser);
 | |
|         if (tabElem) {
 | |
|           if (!extension.canAccessWindow(tabElem.ownerGlobal)) {
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           let changed = {};
 | |
|           if (filter.properties.has("status")) {
 | |
|             changed.status = status;
 | |
|           }
 | |
|           if (url && filter.properties.has("url")) {
 | |
|             changed.url = url;
 | |
|           }
 | |
| 
 | |
|           fireForTab(tabManager.wrapTab(tabElem), changed, tabElem);
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       let isArticleChangeListener = (messageName, message) => {
 | |
|         let { gBrowser } = message.target.ownerGlobal;
 | |
|         let nativeTab = gBrowser.getTabForBrowser(message.target);
 | |
| 
 | |
|         if (nativeTab && extension.canAccessWindow(nativeTab.ownerGlobal)) {
 | |
|           let tab = tabManager.getWrapper(nativeTab);
 | |
|           fireForTab(tab, { isArticle: message.data.isArticle }, nativeTab);
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       let listeners = new Map();
 | |
|       if (filter.properties.has("status") || filter.properties.has("url")) {
 | |
|         listeners.set("status", statusListener);
 | |
|       }
 | |
|       if (needsModified) {
 | |
|         listeners.set("TabAttrModified", listener);
 | |
|       }
 | |
|       if (filter.properties.has("pinned")) {
 | |
|         listeners.set("TabPinned", listener);
 | |
|         listeners.set("TabUnpinned", listener);
 | |
|       }
 | |
|       if (filter.properties.has("discarded")) {
 | |
|         listeners.set("TabBrowserInserted", listener);
 | |
|         listeners.set("TabBrowserDiscarded", listener);
 | |
|       }
 | |
|       if (filter.properties.has("hidden")) {
 | |
|         listeners.set("TabShow", listener);
 | |
|         listeners.set("TabHide", listener);
 | |
|       }
 | |
| 
 | |
|       for (let [name, listener] of listeners) {
 | |
|         windowTracker.addListener(name, listener);
 | |
|       }
 | |
| 
 | |
|       if (filter.properties.has("isArticle")) {
 | |
|         tabTracker.on("tab-isarticle", isArticleChangeListener);
 | |
|       }
 | |
| 
 | |
|       return {
 | |
|         unregister() {
 | |
|           for (let [name, listener] of listeners) {
 | |
|             windowTracker.removeListener(name, listener);
 | |
|           }
 | |
| 
 | |
|           if (filter.properties.has("isArticle")) {
 | |
|             tabTracker.off("tab-isarticle", isArticleChangeListener);
 | |
|           }
 | |
|         },
 | |
|         convert(_fire, _context) {
 | |
|           fire = _fire;
 | |
|           context = _context;
 | |
|         },
 | |
|       };
 | |
|     },
 | |
|   };
 | |
| 
 | |
|   getAPI(context) {
 | |
|     let { extension } = context;
 | |
|     let { tabManager, windowManager } = extension;
 | |
|     let extensionApi = this;
 | |
|     let module = "tabs";
 | |
| 
 | |
|     function getTabOrActive(tabId) {
 | |
|       let tab =
 | |
|         tabId !== null ? tabTracker.getTab(tabId) : tabTracker.activeTab;
 | |
|       if (!tabManager.canAccessTab(tab)) {
 | |
|         throw new ExtensionError(
 | |
|           tabId === null
 | |
|             ? "Cannot access activeTab"
 | |
|             : `Invalid tab ID: ${tabId}`
 | |
|         );
 | |
|       }
 | |
|       return tab;
 | |
|     }
 | |
| 
 | |
|     function getNativeTabsFromIDArray(tabIds) {
 | |
|       if (!Array.isArray(tabIds)) {
 | |
|         tabIds = [tabIds];
 | |
|       }
 | |
|       return tabIds.map(tabId => {
 | |
|         let tab = tabTracker.getTab(tabId);
 | |
|         if (!tabManager.canAccessTab(tab)) {
 | |
|           throw new ExtensionError(`Invalid tab ID: ${tabId}`);
 | |
|         }
 | |
|         return tab;
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     async function promiseTabWhenReady(tabId) {
 | |
|       let tab;
 | |
|       if (tabId !== null) {
 | |
|         tab = tabManager.get(tabId);
 | |
|       } else {
 | |
|         tab = tabManager.getWrapper(tabTracker.activeTab);
 | |
|       }
 | |
|       if (!tab) {
 | |
|         throw new ExtensionError(
 | |
|           tabId == null ? "Cannot access activeTab" : `Invalid tab ID: ${tabId}`
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       await tabListener.awaitTabReady(tab.nativeTab);
 | |
| 
 | |
|       return tab;
 | |
|     }
 | |
| 
 | |
|     function setContentTriggeringPrincipal(url, browser, options) {
 | |
|       // For urls that we want to allow an extension to open in a tab, but
 | |
|       // that it may not otherwise have access to, we set the triggering
 | |
|       // principal to the url that is being opened.  This is used for newtab,
 | |
|       // about: and moz-extension: protocols.
 | |
|       options.triggeringPrincipal =
 | |
|         Services.scriptSecurityManager.createContentPrincipal(
 | |
|           Services.io.newURI(url),
 | |
|           {
 | |
|             userContextId: options.userContextId,
 | |
|             privateBrowsingId: PrivateBrowsingUtils.isBrowserPrivate(browser)
 | |
|               ? 1
 | |
|               : 0,
 | |
|           }
 | |
|         );
 | |
|     }
 | |
| 
 | |
|     let tabsApi = {
 | |
|       tabs: {
 | |
|         onActivated: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onActivated",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         onCreated: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onCreated",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         onHighlighted: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onHighlighted",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         onAttached: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onAttached",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         onDetached: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onDetached",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         onRemoved: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onRemoved",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         onReplaced: new EventManager({
 | |
|           context,
 | |
|           name: "tabs.onReplaced",
 | |
|           register: fire => {
 | |
|             return () => {};
 | |
|           },
 | |
|         }).api(),
 | |
| 
 | |
|         onMoved: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onMoved",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         onUpdated: new EventManager({
 | |
|           context,
 | |
|           module,
 | |
|           event: "onUpdated",
 | |
|           extensionApi,
 | |
|         }).api(),
 | |
| 
 | |
|         create(createProperties) {
 | |
|           return new Promise((resolve, reject) => {
 | |
|             let window =
 | |
|               createProperties.windowId !== null
 | |
|                 ? windowTracker.getWindow(createProperties.windowId, context)
 | |
|                 : windowTracker.getTopNormalWindow(context);
 | |
|             if (!window || !context.canAccessWindow(window)) {
 | |
|               throw new Error(
 | |
|                 "Not allowed to create tabs on the target window"
 | |
|               );
 | |
|             }
 | |
|             let { gBrowserInit } = window;
 | |
|             if (!gBrowserInit || !gBrowserInit.delayedStartupFinished) {
 | |
|               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");
 | |
|             } else {
 | |
|               resolve(window);
 | |
|             }
 | |
|           }).then(window => {
 | |
|             let url;
 | |
| 
 | |
|             let options = { triggeringPrincipal: context.principal };
 | |
|             if (createProperties.cookieStoreId) {
 | |
|               // May throw if validation fails.
 | |
|               options.userContextId = getUserContextIdForCookieStoreId(
 | |
|                 extension,
 | |
|                 createProperties.cookieStoreId,
 | |
|                 PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser)
 | |
|               );
 | |
|             }
 | |
| 
 | |
|             if (createProperties.url !== null) {
 | |
|               url = context.uri.resolve(createProperties.url);
 | |
| 
 | |
|               if (
 | |
|                 !url.startsWith("moz-extension://") &&
 | |
|                 !context.checkLoadURL(url, { dontReportErrors: true })
 | |
|               ) {
 | |
|                 return Promise.reject({ message: `Illegal URL: ${url}` });
 | |
|               }
 | |
| 
 | |
|               if (createProperties.openInReaderMode) {
 | |
|                 url = `about:reader?url=${encodeURIComponent(url)}`;
 | |
|               }
 | |
|             } else {
 | |
|               url = window.BROWSER_NEW_TAB_URL;
 | |
|             }
 | |
|             let discardable = url && !url.startsWith("about:");
 | |
|             // Handle moz-ext separately from the discardable flag to retain prior behavior.
 | |
|             if (!discardable || url.startsWith("moz-extension://")) {
 | |
|               setContentTriggeringPrincipal(url, window.gBrowser, options);
 | |
|             }
 | |
| 
 | |
|             tabListener.initTabReady();
 | |
|             const currentTab = window.gBrowser.selectedTab;
 | |
|             const { frameLoader } = currentTab.linkedBrowser;
 | |
|             const currentTabSize = {
 | |
|               width: frameLoader.lazyWidth,
 | |
|               height: frameLoader.lazyHeight,
 | |
|             };
 | |
| 
 | |
|             if (createProperties.openerTabId !== null) {
 | |
|               options.ownerTab = tabTracker.getTab(
 | |
|                 createProperties.openerTabId
 | |
|               );
 | |
|               options.openerBrowser = options.ownerTab.linkedBrowser;
 | |
|               if (options.ownerTab.ownerGlobal !== window) {
 | |
|                 return Promise.reject({
 | |
|                   message:
 | |
|                     "Opener tab must be in the same window as the tab being created",
 | |
|                 });
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             // Simple properties
 | |
|             const properties = ["index", "pinned"];
 | |
|             for (let prop of properties) {
 | |
|               if (createProperties[prop] != null) {
 | |
|                 options[prop] = createProperties[prop];
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             let active =
 | |
|               createProperties.active !== null
 | |
|                 ? createProperties.active
 | |
|                 : !createProperties.discarded;
 | |
|             if (createProperties.discarded) {
 | |
|               if (active) {
 | |
|                 return Promise.reject({
 | |
|                   message: `Active tabs cannot be created and discarded.`,
 | |
|                 });
 | |
|               }
 | |
|               if (createProperties.pinned) {
 | |
|                 return Promise.reject({
 | |
|                   message: `Pinned tabs cannot be created and discarded.`,
 | |
|                 });
 | |
|               }
 | |
|               if (!discardable) {
 | |
|                 return Promise.reject({
 | |
|                   message: `Cannot create a discarded new tab or "about" urls.`,
 | |
|                 });
 | |
|               }
 | |
|               options.createLazyBrowser = true;
 | |
|               options.lazyTabTitle = createProperties.title;
 | |
|             } else if (createProperties.title) {
 | |
|               return Promise.reject({
 | |
|                 message: `Title may only be set for discarded tabs.`,
 | |
|               });
 | |
|             }
 | |
| 
 | |
|             let nativeTab = window.gBrowser.addTab(url, options);
 | |
| 
 | |
|             if (active) {
 | |
|               window.gBrowser.selectedTab = nativeTab;
 | |
|               if (!createProperties.url) {
 | |
|                 window.gURLBar.select();
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             if (
 | |
|               createProperties.url &&
 | |
|               createProperties.url !== window.BROWSER_NEW_TAB_URL
 | |
|             ) {
 | |
|               // 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(nativeTab);
 | |
|             }
 | |
| 
 | |
|             if (createProperties.muted) {
 | |
|               nativeTab.toggleMuteAudio(extension.id);
 | |
|             }
 | |
| 
 | |
|             return tabManager.convert(nativeTab, currentTabSize);
 | |
|           });
 | |
|         },
 | |
| 
 | |
|         async remove(tabIds) {
 | |
|           let nativeTabs = getNativeTabsFromIDArray(tabIds);
 | |
| 
 | |
|           if (nativeTabs.length === 1) {
 | |
|             nativeTabs[0].ownerGlobal.gBrowser.removeTab(nativeTabs[0]);
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           // Or for multiple tabs, first group them by window
 | |
|           let windowTabMap = new DefaultMap(() => []);
 | |
|           for (let nativeTab of nativeTabs) {
 | |
|             windowTabMap.get(nativeTab.ownerGlobal).push(nativeTab);
 | |
|           }
 | |
| 
 | |
|           // Then make one call to removeTabs() for each window, to keep the
 | |
|           // count accurate for SessionStore.getLastClosedTabCount().
 | |
|           // Note: always pass options to disable animation and the warning
 | |
|           // dialogue box, so that way all tabs are actually closed when the
 | |
|           // browser.tabs.remove() promise resolves
 | |
|           for (let [eachWindow, tabsToClose] of windowTabMap.entries()) {
 | |
|             eachWindow.gBrowser.removeTabs(tabsToClose, {
 | |
|               animate: false,
 | |
|               suppressWarnAboutClosingWindow: true,
 | |
|             });
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         async discard(tabIds) {
 | |
|           for (let nativeTab of getNativeTabsFromIDArray(tabIds)) {
 | |
|             nativeTab.ownerGlobal.gBrowser.discardBrowser(nativeTab);
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         async update(tabId, updateProperties) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           let tabbrowser = nativeTab.ownerGlobal.gBrowser;
 | |
| 
 | |
|           if (updateProperties.url !== null) {
 | |
|             let url = context.uri.resolve(updateProperties.url);
 | |
| 
 | |
|             let options = {
 | |
|               flags: updateProperties.loadReplace
 | |
|                 ? Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY
 | |
|                 : Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
 | |
|               triggeringPrincipal: context.principal,
 | |
|             };
 | |
| 
 | |
|             if (!context.checkLoadURL(url, { dontReportErrors: true })) {
 | |
|               // We allow loading top level tabs for "other" extensions.
 | |
|               if (url.startsWith("moz-extension://")) {
 | |
|                 setContentTriggeringPrincipal(url, tabbrowser, options);
 | |
|               } else {
 | |
|                 return Promise.reject({ message: `Illegal URL: ${url}` });
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             let browser = nativeTab.linkedBrowser;
 | |
|             if (nativeTab.linkedPanel) {
 | |
|               browser.fixupAndLoadURIString(url, options);
 | |
|             } else {
 | |
|               // Shift to fully loaded browser and make
 | |
|               // sure load handler is instantiated.
 | |
|               nativeTab.addEventListener(
 | |
|                 "SSTabRestoring",
 | |
|                 () => browser.fixupAndLoadURIString(url, options),
 | |
|                 { once: true }
 | |
|               );
 | |
|               tabbrowser._insertBrowser(nativeTab);
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           if (updateProperties.active) {
 | |
|             tabbrowser.selectedTab = nativeTab;
 | |
|           }
 | |
|           if (updateProperties.autoDiscardable !== null) {
 | |
|             nativeTab.undiscardable = !updateProperties.autoDiscardable;
 | |
|           }
 | |
|           if (updateProperties.highlighted !== null) {
 | |
|             if (updateProperties.highlighted) {
 | |
|               if (!nativeTab.selected && !nativeTab.multiselected) {
 | |
|                 tabbrowser.addToMultiSelectedTabs(nativeTab);
 | |
|                 // Select the highlighted tab unless active:false is provided.
 | |
|                 // Note that Chrome selects it even in that case.
 | |
|                 if (updateProperties.active !== false) {
 | |
|                   tabbrowser.lockClearMultiSelectionOnce();
 | |
|                   tabbrowser.selectedTab = nativeTab;
 | |
|                 }
 | |
|               }
 | |
|             } else {
 | |
|               tabbrowser.removeFromMultiSelectedTabs(nativeTab);
 | |
|             }
 | |
|           }
 | |
|           if (updateProperties.muted !== null) {
 | |
|             if (nativeTab.muted != updateProperties.muted) {
 | |
|               nativeTab.toggleMuteAudio(extension.id);
 | |
|             }
 | |
|           }
 | |
|           if (updateProperties.pinned !== null) {
 | |
|             if (updateProperties.pinned) {
 | |
|               tabbrowser.pinTab(nativeTab);
 | |
|             } else {
 | |
|               tabbrowser.unpinTab(nativeTab);
 | |
|             }
 | |
|           }
 | |
|           if (updateProperties.openerTabId !== null) {
 | |
|             let opener = tabTracker.getTab(updateProperties.openerTabId);
 | |
|             if (opener.ownerDocument !== nativeTab.ownerDocument) {
 | |
|               return Promise.reject({
 | |
|                 message:
 | |
|                   "Opener tab must be in the same window as the tab being updated",
 | |
|               });
 | |
|             }
 | |
|             tabTracker.setOpener(nativeTab, opener);
 | |
|           }
 | |
|           if (updateProperties.successorTabId !== null) {
 | |
|             let successor = null;
 | |
|             if (updateProperties.successorTabId !== TAB_ID_NONE) {
 | |
|               successor = tabTracker.getTab(
 | |
|                 updateProperties.successorTabId,
 | |
|                 null
 | |
|               );
 | |
|               if (!successor) {
 | |
|                 throw new ExtensionError("Invalid successorTabId");
 | |
|               }
 | |
|               // This also ensures "privateness" matches.
 | |
|               if (successor.ownerDocument !== nativeTab.ownerDocument) {
 | |
|                 throw new ExtensionError(
 | |
|                   "Successor tab must be in the same window as the tab being updated"
 | |
|                 );
 | |
|               }
 | |
|             }
 | |
|             tabbrowser.setSuccessor(nativeTab, successor);
 | |
|           }
 | |
| 
 | |
|           return tabManager.convert(nativeTab);
 | |
|         },
 | |
| 
 | |
|         async reload(tabId, reloadProperties) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
 | |
|           if (reloadProperties && reloadProperties.bypassCache) {
 | |
|             flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
 | |
|           }
 | |
|           nativeTab.linkedBrowser.reloadWithFlags(flags);
 | |
|         },
 | |
| 
 | |
|         async warmup(tabId) {
 | |
|           let nativeTab = tabTracker.getTab(tabId);
 | |
|           if (!tabManager.canAccessTab(nativeTab)) {
 | |
|             throw new ExtensionError(`Invalid tab ID: ${tabId}`);
 | |
|           }
 | |
|           let tabbrowser = nativeTab.ownerGlobal.gBrowser;
 | |
|           tabbrowser.warmupTab(nativeTab);
 | |
|         },
 | |
| 
 | |
|         async get(tabId) {
 | |
|           return tabManager.get(tabId).convert();
 | |
|         },
 | |
| 
 | |
|         getCurrent() {
 | |
|           let tabData;
 | |
|           if (context.tabId) {
 | |
|             tabData = tabManager.get(context.tabId).convert();
 | |
|           }
 | |
|           return Promise.resolve(tabData);
 | |
|         },
 | |
| 
 | |
|         async query(queryInfo) {
 | |
|           return Array.from(tabManager.query(queryInfo, context), tab =>
 | |
|             tab.convert()
 | |
|           );
 | |
|         },
 | |
| 
 | |
|         async captureTab(tabId, options) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
|           await tabListener.awaitTabReady(nativeTab);
 | |
| 
 | |
|           let browser = nativeTab.linkedBrowser;
 | |
|           let window = browser.ownerGlobal;
 | |
|           let zoom = window.ZoomManager.getZoomForBrowser(browser);
 | |
| 
 | |
|           let tab = tabManager.wrapTab(nativeTab);
 | |
|           return tab.capture(context, zoom, options);
 | |
|         },
 | |
| 
 | |
|         async captureVisibleTab(windowId, options) {
 | |
|           let window =
 | |
|             windowId == null
 | |
|               ? windowTracker.getTopWindow(context)
 | |
|               : windowTracker.getWindow(windowId, context);
 | |
| 
 | |
|           let tab = tabManager.wrapTab(window.gBrowser.selectedTab);
 | |
|           await tabListener.awaitTabReady(tab.nativeTab);
 | |
| 
 | |
|           let zoom = window.ZoomManager.getZoomForBrowser(
 | |
|             tab.nativeTab.linkedBrowser
 | |
|           );
 | |
|           return tab.capture(context, zoom, options);
 | |
|         },
 | |
| 
 | |
|         async detectLanguage(tabId) {
 | |
|           let tab = await promiseTabWhenReady(tabId);
 | |
|           let results = await tab.queryContent("DetectLanguage", {});
 | |
|           return results[0];
 | |
|         },
 | |
| 
 | |
|         async executeScript(tabId, details) {
 | |
|           let tab = await promiseTabWhenReady(tabId);
 | |
|           return tab.executeScript(context, details);
 | |
|         },
 | |
| 
 | |
|         async insertCSS(tabId, details) {
 | |
|           let tab = await promiseTabWhenReady(tabId);
 | |
|           return tab.insertCSS(context, details);
 | |
|         },
 | |
| 
 | |
|         async removeCSS(tabId, details) {
 | |
|           let tab = await promiseTabWhenReady(tabId);
 | |
|           return tab.removeCSS(context, details);
 | |
|         },
 | |
| 
 | |
|         async move(tabIds, moveProperties) {
 | |
|           let tabsMoved = [];
 | |
|           if (!Array.isArray(tabIds)) {
 | |
|             tabIds = [tabIds];
 | |
|           }
 | |
| 
 | |
|           let destinationWindow = null;
 | |
|           if (moveProperties.windowId !== null) {
 | |
|             destinationWindow = windowTracker.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 lastInsertionMap = new Map();
 | |
| 
 | |
|           for (let nativeTab of getNativeTabsFromIDArray(tabIds)) {
 | |
|             // If the window is not specified, use the window from the tab.
 | |
|             let window = destinationWindow || nativeTab.ownerGlobal;
 | |
|             let isSameWindow = nativeTab.ownerGlobal == window;
 | |
|             let gBrowser = window.gBrowser;
 | |
| 
 | |
|             // If we are not moving the tab to a different window, and the window
 | |
|             // only has one tab, do nothing.
 | |
|             if (isSameWindow && gBrowser.tabs.length === 1) {
 | |
|               lastInsertionMap.set(window, 0);
 | |
|               continue;
 | |
|             }
 | |
|             // If moving between windows, be sure privacy matches.  While gBrowser
 | |
|             // prevents this, we want to silently ignore it.
 | |
|             if (
 | |
|               !isSameWindow &&
 | |
|               PrivateBrowsingUtils.isBrowserPrivate(gBrowser) !=
 | |
|                 PrivateBrowsingUtils.isBrowserPrivate(
 | |
|                   nativeTab.ownerGlobal.gBrowser
 | |
|                 )
 | |
|             ) {
 | |
|               continue;
 | |
|             }
 | |
| 
 | |
|             let insertionPoint;
 | |
|             let lastInsertion = lastInsertionMap.get(window);
 | |
|             if (lastInsertion == null) {
 | |
|               insertionPoint = moveProperties.index;
 | |
|               let maxIndex = gBrowser.tabs.length - (isSameWindow ? 1 : 0);
 | |
|               if (insertionPoint == -1) {
 | |
|                 // If the index is -1 it should go to the end of the tabs.
 | |
|                 insertionPoint = maxIndex;
 | |
|               } else {
 | |
|                 insertionPoint = Math.min(insertionPoint, maxIndex);
 | |
|               }
 | |
|             } else if (isSameWindow && nativeTab._tPos <= lastInsertion) {
 | |
|               // lastInsertion is the current index of the last inserted tab.
 | |
|               // insertionPoint is the desired index of the current tab *after* moving it.
 | |
|               // When the tab is moved, the last inserted tab will no longer be at index
 | |
|               // lastInsertion, but (lastInsertion - 1). To position the tabs adjacent to
 | |
|               // each other, the tab should therefore be at index (lastInsertion - 1 + 1).
 | |
|               insertionPoint = lastInsertion;
 | |
|             } else {
 | |
|               // In this case the last inserted tab will stay at index lastInsertion,
 | |
|               // so we should move the current tab to index (lastInsertion + 1).
 | |
|               insertionPoint = lastInsertion + 1;
 | |
|             }
 | |
| 
 | |
|             // 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 = nativeTab.pinned
 | |
|               ? insertionPoint <= numPinned
 | |
|               : insertionPoint >= numPinned;
 | |
|             if (!ok) {
 | |
|               continue;
 | |
|             }
 | |
| 
 | |
|             if (isSameWindow) {
 | |
|               // If the window we are moving is the same, just move the tab.
 | |
|               gBrowser.moveTabTo(nativeTab, insertionPoint);
 | |
|             } else {
 | |
|               // If the window we are moving the tab in is different, then move the tab
 | |
|               // to the new window.
 | |
|               nativeTab = gBrowser.adoptTab(nativeTab, insertionPoint, false);
 | |
|             }
 | |
|             lastInsertionMap.set(window, nativeTab._tPos);
 | |
|             tabsMoved.push(nativeTab);
 | |
|           }
 | |
| 
 | |
|           return tabsMoved.map(nativeTab => tabManager.convert(nativeTab));
 | |
|         },
 | |
| 
 | |
|         duplicate(tabId, duplicateProperties) {
 | |
|           const { active, index } = duplicateProperties || {};
 | |
|           const inBackground = active === undefined ? false : !active;
 | |
| 
 | |
|           // Schema requires tab id.
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           let gBrowser = nativeTab.ownerGlobal.gBrowser;
 | |
|           let newTab = gBrowser.duplicateTab(nativeTab, true, {
 | |
|             inBackground,
 | |
|             index,
 | |
|           });
 | |
| 
 | |
|           tabListener.blockTabUntilRestored(newTab);
 | |
|           return new Promise(resolve => {
 | |
|             // Use SSTabRestoring to ensure that the tab's URL is ready before
 | |
|             // resolving the promise.
 | |
|             newTab.addEventListener(
 | |
|               "SSTabRestoring",
 | |
|               () => resolve(tabManager.convert(newTab)),
 | |
|               { once: true }
 | |
|             );
 | |
|           });
 | |
|         },
 | |
| 
 | |
|         getZoom(tabId) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           let { ZoomManager } = nativeTab.ownerGlobal;
 | |
|           let zoom = ZoomManager.getZoomForBrowser(nativeTab.linkedBrowser);
 | |
| 
 | |
|           return Promise.resolve(zoom);
 | |
|         },
 | |
| 
 | |
|         setZoom(tabId, zoom) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           let { FullZoom, ZoomManager } = nativeTab.ownerGlobal;
 | |
| 
 | |
|           if (zoom === 0) {
 | |
|             // A value of zero means use the default zoom factor.
 | |
|             return FullZoom.reset(nativeTab.linkedBrowser);
 | |
|           } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
 | |
|             FullZoom.setZoom(zoom, nativeTab.linkedBrowser);
 | |
|           } else {
 | |
|             return Promise.reject({
 | |
|               message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
 | |
|             });
 | |
|           }
 | |
| 
 | |
|           return Promise.resolve();
 | |
|         },
 | |
| 
 | |
|         async getZoomSettings(tabId) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           let { FullZoom, ZoomUI } = nativeTab.ownerGlobal;
 | |
| 
 | |
|           return {
 | |
|             mode: "automatic",
 | |
|             scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
 | |
|             defaultZoomFactor: await ZoomUI.getGlobalValue(),
 | |
|           };
 | |
|         },
 | |
| 
 | |
|         async setZoomSettings(tabId, settings) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           let currentSettings = await this.getZoomSettings(
 | |
|             tabTracker.getId(nativeTab)
 | |
|           );
 | |
| 
 | |
|           if (
 | |
|             !Object.keys(settings).every(
 | |
|               key => settings[key] === currentSettings[key]
 | |
|             )
 | |
|           ) {
 | |
|             throw new ExtensionError(
 | |
|               `Unsupported zoom settings: ${JSON.stringify(settings)}`
 | |
|             );
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         onZoomChange: new EventManager({
 | |
|           context,
 | |
|           name: "tabs.onZoomChange",
 | |
|           register: 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 windowTracker.browserWindows()) {
 | |
|               if (!context.canAccessWindow(window)) {
 | |
|                 continue;
 | |
|               }
 | |
|               for (let nativeTab of window.gBrowser.tabs) {
 | |
|                 let browser = nativeTab.linkedBrowser;
 | |
|                 zoomLevels.set(browser, getZoomLevel(browser));
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             let tabCreated = (eventName, event) => {
 | |
|               let browser = event.nativeTab.linkedBrowser;
 | |
|               if (!event.isPrivate || context.privateBrowsingAllowed) {
 | |
|                 zoomLevels.set(browser, getZoomLevel(browser));
 | |
|               }
 | |
|             };
 | |
| 
 | |
|             let zoomListener = async event => {
 | |
|               let browser = event.originalTarget;
 | |
| 
 | |
|               // For non-remote browsers, this event is dispatched on the document
 | |
|               // rather than on the <browser>.  But either way we have a node here.
 | |
|               if (browser.nodeType == browser.DOCUMENT_NODE) {
 | |
|                 browser = browser.docShell.chromeEventHandler;
 | |
|               }
 | |
| 
 | |
|               if (!context.canAccessWindow(browser.ownerGlobal)) {
 | |
|                 return;
 | |
|               }
 | |
| 
 | |
|               let { gBrowser } = browser.ownerGlobal;
 | |
|               let nativeTab = gBrowser.getTabForBrowser(browser);
 | |
|               if (!nativeTab) {
 | |
|                 // 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 = tabTracker.getId(nativeTab);
 | |
|                 fire.async({
 | |
|                   tabId,
 | |
|                   oldZoomFactor,
 | |
|                   newZoomFactor,
 | |
|                   zoomSettings: await tabsApi.tabs.getZoomSettings(tabId),
 | |
|                 });
 | |
|               }
 | |
|             };
 | |
| 
 | |
|             tabTracker.on("tab-attached", tabCreated);
 | |
|             tabTracker.on("tab-created", tabCreated);
 | |
| 
 | |
|             windowTracker.addListener("FullZoomChange", zoomListener);
 | |
|             windowTracker.addListener("TextZoomChange", zoomListener);
 | |
|             return () => {
 | |
|               tabTracker.off("tab-attached", tabCreated);
 | |
|               tabTracker.off("tab-created", tabCreated);
 | |
| 
 | |
|               windowTracker.removeListener("FullZoomChange", zoomListener);
 | |
|               windowTracker.removeListener("TextZoomChange", zoomListener);
 | |
|             };
 | |
|           },
 | |
|         }).api(),
 | |
| 
 | |
|         print() {
 | |
|           let activeTab = getTabOrActive(null);
 | |
|           let { PrintUtils } = activeTab.ownerGlobal;
 | |
|           PrintUtils.startPrintWindow(activeTab.linkedBrowser.browsingContext);
 | |
|         },
 | |
| 
 | |
|         // Legacy API
 | |
|         printPreview() {
 | |
|           return Promise.resolve(this.print());
 | |
|         },
 | |
| 
 | |
|         saveAsPDF(pageSettings) {
 | |
|           let activeTab = getTabOrActive(null);
 | |
|           let picker = Cc["@mozilla.org/filepicker;1"].createInstance(
 | |
|             Ci.nsIFilePicker
 | |
|           );
 | |
|           let title = strBundle.GetStringFromName(
 | |
|             "saveaspdf.saveasdialog.title"
 | |
|           );
 | |
|           let filename;
 | |
|           if (
 | |
|             pageSettings.toFileName !== null &&
 | |
|             pageSettings.toFileName != ""
 | |
|           ) {
 | |
|             filename = pageSettings.toFileName;
 | |
|           } else if (activeTab.linkedBrowser.contentTitle != "") {
 | |
|             filename = activeTab.linkedBrowser.contentTitle;
 | |
|           } else {
 | |
|             let url = new URL(activeTab.linkedBrowser.currentURI.spec);
 | |
|             let path = decodeURIComponent(url.pathname);
 | |
|             path = path.replace(/\/$/, "");
 | |
|             filename = path.split("/").pop();
 | |
|             if (filename == "") {
 | |
|               filename = url.hostname;
 | |
|             }
 | |
|           }
 | |
|           filename = DownloadPaths.sanitize(filename);
 | |
| 
 | |
|           picker.init(activeTab.ownerGlobal, title, Ci.nsIFilePicker.modeSave);
 | |
|           picker.appendFilter("PDF", "*.pdf");
 | |
|           picker.defaultExtension = "pdf";
 | |
|           picker.defaultString = filename;
 | |
| 
 | |
|           return new Promise(resolve => {
 | |
|             picker.open(function (retval) {
 | |
|               if (retval == 0 || retval == 2) {
 | |
|                 // OK clicked (retval == 0) or replace confirmed (retval == 2)
 | |
| 
 | |
|                 // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file),
 | |
|                 // the print progress listener is never called. This workaround ensures that a correct status is always returned.
 | |
|                 try {
 | |
|                   let fstream = Cc[
 | |
|                     "@mozilla.org/network/file-output-stream;1"
 | |
|                   ].createInstance(Ci.nsIFileOutputStream);
 | |
|                   fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw-
 | |
|                   fstream.close();
 | |
|                 } catch (e) {
 | |
|                   resolve(retval == 0 ? "not_saved" : "not_replaced");
 | |
|                   return;
 | |
|                 }
 | |
| 
 | |
|                 let psService = Cc[
 | |
|                   "@mozilla.org/gfx/printsettings-service;1"
 | |
|                 ].getService(Ci.nsIPrintSettingsService);
 | |
|                 let printSettings = psService.createNewPrintSettings();
 | |
| 
 | |
|                 printSettings.printerName = "";
 | |
|                 printSettings.isInitializedFromPrinter = true;
 | |
|                 printSettings.isInitializedFromPrefs = true;
 | |
| 
 | |
|                 printSettings.outputDestination =
 | |
|                   Ci.nsIPrintSettings.kOutputDestinationFile;
 | |
|                 printSettings.toFileName = picker.file.path;
 | |
| 
 | |
|                 printSettings.printSilent = true;
 | |
| 
 | |
|                 printSettings.outputFormat =
 | |
|                   Ci.nsIPrintSettings.kOutputFormatPDF;
 | |
| 
 | |
|                 if (pageSettings.paperSizeUnit !== null) {
 | |
|                   printSettings.paperSizeUnit = pageSettings.paperSizeUnit;
 | |
|                 }
 | |
|                 if (pageSettings.paperWidth !== null) {
 | |
|                   printSettings.paperWidth = pageSettings.paperWidth;
 | |
|                 }
 | |
|                 if (pageSettings.paperHeight !== null) {
 | |
|                   printSettings.paperHeight = pageSettings.paperHeight;
 | |
|                 }
 | |
|                 if (pageSettings.orientation !== null) {
 | |
|                   printSettings.orientation = pageSettings.orientation;
 | |
|                 }
 | |
|                 if (pageSettings.scaling !== null) {
 | |
|                   printSettings.scaling = pageSettings.scaling;
 | |
|                 }
 | |
|                 if (pageSettings.shrinkToFit !== null) {
 | |
|                   printSettings.shrinkToFit = pageSettings.shrinkToFit;
 | |
|                 }
 | |
|                 if (pageSettings.showBackgroundColors !== null) {
 | |
|                   printSettings.printBGColors =
 | |
|                     pageSettings.showBackgroundColors;
 | |
|                 }
 | |
|                 if (pageSettings.showBackgroundImages !== null) {
 | |
|                   printSettings.printBGImages =
 | |
|                     pageSettings.showBackgroundImages;
 | |
|                 }
 | |
|                 if (pageSettings.edgeLeft !== null) {
 | |
|                   printSettings.edgeLeft = pageSettings.edgeLeft;
 | |
|                 }
 | |
|                 if (pageSettings.edgeRight !== null) {
 | |
|                   printSettings.edgeRight = pageSettings.edgeRight;
 | |
|                 }
 | |
|                 if (pageSettings.edgeTop !== null) {
 | |
|                   printSettings.edgeTop = pageSettings.edgeTop;
 | |
|                 }
 | |
|                 if (pageSettings.edgeBottom !== null) {
 | |
|                   printSettings.edgeBottom = pageSettings.edgeBottom;
 | |
|                 }
 | |
|                 if (pageSettings.marginLeft !== null) {
 | |
|                   printSettings.marginLeft = pageSettings.marginLeft;
 | |
|                 }
 | |
|                 if (pageSettings.marginRight !== null) {
 | |
|                   printSettings.marginRight = pageSettings.marginRight;
 | |
|                 }
 | |
|                 if (pageSettings.marginTop !== null) {
 | |
|                   printSettings.marginTop = pageSettings.marginTop;
 | |
|                 }
 | |
|                 if (pageSettings.marginBottom !== null) {
 | |
|                   printSettings.marginBottom = pageSettings.marginBottom;
 | |
|                 }
 | |
|                 if (pageSettings.headerLeft !== null) {
 | |
|                   printSettings.headerStrLeft = pageSettings.headerLeft;
 | |
|                 }
 | |
|                 if (pageSettings.headerCenter !== null) {
 | |
|                   printSettings.headerStrCenter = pageSettings.headerCenter;
 | |
|                 }
 | |
|                 if (pageSettings.headerRight !== null) {
 | |
|                   printSettings.headerStrRight = pageSettings.headerRight;
 | |
|                 }
 | |
|                 if (pageSettings.footerLeft !== null) {
 | |
|                   printSettings.footerStrLeft = pageSettings.footerLeft;
 | |
|                 }
 | |
|                 if (pageSettings.footerCenter !== null) {
 | |
|                   printSettings.footerStrCenter = pageSettings.footerCenter;
 | |
|                 }
 | |
|                 if (pageSettings.footerRight !== null) {
 | |
|                   printSettings.footerStrRight = pageSettings.footerRight;
 | |
|                 }
 | |
| 
 | |
|                 activeTab.linkedBrowser.browsingContext
 | |
|                   .print(printSettings)
 | |
|                   .then(() => resolve(retval == 0 ? "saved" : "replaced"))
 | |
|                   .catch(() =>
 | |
|                     resolve(retval == 0 ? "not_saved" : "not_replaced")
 | |
|                   );
 | |
|               } else {
 | |
|                 // Cancel clicked (retval == 1)
 | |
|                 resolve("canceled");
 | |
|               }
 | |
|             });
 | |
|           });
 | |
|         },
 | |
| 
 | |
|         async toggleReaderMode(tabId) {
 | |
|           let tab = await promiseTabWhenReady(tabId);
 | |
|           if (!tab.isInReaderMode && !tab.isArticle) {
 | |
|             throw new ExtensionError(
 | |
|               "The specified tab cannot be placed into reader mode."
 | |
|             );
 | |
|           }
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
| 
 | |
|           nativeTab.linkedBrowser.sendMessageToActor(
 | |
|             "Reader:ToggleReaderMode",
 | |
|             {},
 | |
|             "AboutReader"
 | |
|           );
 | |
|         },
 | |
| 
 | |
|         moveInSuccession(tabIds, tabId, options) {
 | |
|           const { insert, append } = options || {};
 | |
|           const tabIdSet = new Set(tabIds);
 | |
|           if (tabIdSet.size !== tabIds.length) {
 | |
|             throw new ExtensionError(
 | |
|               "IDs must not occur more than once in tabIds"
 | |
|             );
 | |
|           }
 | |
|           if ((append || insert) && tabIdSet.has(tabId)) {
 | |
|             throw new ExtensionError(
 | |
|               "Value of tabId must not occur in tabIds if append or insert is true"
 | |
|             );
 | |
|           }
 | |
| 
 | |
|           const referenceTab = tabTracker.getTab(tabId, null);
 | |
|           let referenceWindow = referenceTab && referenceTab.ownerGlobal;
 | |
|           if (referenceWindow && !context.canAccessWindow(referenceWindow)) {
 | |
|             throw new ExtensionError(`Invalid tab ID: ${tabId}`);
 | |
|           }
 | |
|           let previousTab, lastSuccessor;
 | |
|           if (append) {
 | |
|             previousTab = referenceTab;
 | |
|             lastSuccessor =
 | |
|               (insert && referenceTab && referenceTab.successor) || null;
 | |
|           } else {
 | |
|             lastSuccessor = referenceTab;
 | |
|           }
 | |
| 
 | |
|           let firstTab;
 | |
|           for (const tabId of tabIds) {
 | |
|             const tab = tabTracker.getTab(tabId, null);
 | |
|             if (tab === null) {
 | |
|               continue;
 | |
|             }
 | |
|             if (!tabManager.canAccessTab(tab)) {
 | |
|               throw new ExtensionError(`Invalid tab ID: ${tabId}`);
 | |
|             }
 | |
|             if (referenceWindow === null) {
 | |
|               referenceWindow = tab.ownerGlobal;
 | |
|             } else if (tab.ownerGlobal !== referenceWindow) {
 | |
|               continue;
 | |
|             }
 | |
|             referenceWindow.gBrowser.replaceInSuccession(tab, tab.successor);
 | |
|             if (append && tab === lastSuccessor) {
 | |
|               lastSuccessor = tab.successor;
 | |
|             }
 | |
|             if (previousTab) {
 | |
|               referenceWindow.gBrowser.setSuccessor(previousTab, tab);
 | |
|             } else {
 | |
|               firstTab = tab;
 | |
|             }
 | |
|             previousTab = tab;
 | |
|           }
 | |
| 
 | |
|           if (previousTab) {
 | |
|             if (!append && insert && lastSuccessor !== null) {
 | |
|               referenceWindow.gBrowser.replaceInSuccession(
 | |
|                 lastSuccessor,
 | |
|                 firstTab
 | |
|               );
 | |
|             }
 | |
|             referenceWindow.gBrowser.setSuccessor(previousTab, lastSuccessor);
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         show(tabIds) {
 | |
|           for (let tab of getNativeTabsFromIDArray(tabIds)) {
 | |
|             if (tab.ownerGlobal) {
 | |
|               tab.ownerGlobal.gBrowser.showTab(tab);
 | |
|             }
 | |
|           }
 | |
|         },
 | |
| 
 | |
|         hide(tabIds) {
 | |
|           let hidden = [];
 | |
|           for (let tab of getNativeTabsFromIDArray(tabIds)) {
 | |
|             if (tab.ownerGlobal && !tab.hidden) {
 | |
|               tab.ownerGlobal.gBrowser.hideTab(tab, extension.id);
 | |
|               if (tab.hidden) {
 | |
|                 hidden.push(tabTracker.getId(tab));
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|           if (hidden.length) {
 | |
|             let win = Services.wm.getMostRecentWindow("navigator:browser");
 | |
|             tabHidePopup.open(win, extension.id);
 | |
|           }
 | |
|           return hidden;
 | |
|         },
 | |
| 
 | |
|         highlight(highlightInfo) {
 | |
|           let { windowId, tabs, populate } = highlightInfo;
 | |
|           if (windowId == null) {
 | |
|             windowId = Window.WINDOW_ID_CURRENT;
 | |
|           }
 | |
|           let window = windowTracker.getWindow(windowId, context);
 | |
|           if (!context.canAccessWindow(window)) {
 | |
|             throw new ExtensionError(`Invalid window ID: ${windowId}`);
 | |
|           }
 | |
| 
 | |
|           if (!Array.isArray(tabs)) {
 | |
|             tabs = [tabs];
 | |
|           } else if (!tabs.length) {
 | |
|             throw new ExtensionError("No highlighted tab.");
 | |
|           }
 | |
|           window.gBrowser.selectedTabs = tabs.map(tabIndex => {
 | |
|             let tab = window.gBrowser.tabs[tabIndex];
 | |
|             if (!tab || !tabManager.canAccessTab(tab)) {
 | |
|               throw new ExtensionError("No tab at index: " + tabIndex);
 | |
|             }
 | |
|             return tab;
 | |
|           });
 | |
|           return windowManager.convert(window, { populate });
 | |
|         },
 | |
| 
 | |
|         goForward(tabId) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
|           nativeTab.linkedBrowser.goForward();
 | |
|         },
 | |
| 
 | |
|         goBack(tabId) {
 | |
|           let nativeTab = getTabOrActive(tabId);
 | |
|           nativeTab.linkedBrowser.goBack();
 | |
|         },
 | |
|       },
 | |
|     };
 | |
|     return tabsApi;
 | |
|   }
 | |
| };
 | 
