ext-browser.js (39030B)
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set sts=2 sw=2 et tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 "use strict"; 8 9 // This file provides some useful code for the |tabs| and |windows| 10 // modules. All of the code is installed on |global|, which is a scope 11 // shared among the different ext-*.js scripts. 12 13 ChromeUtils.defineESModuleGetters(this, { 14 AboutReaderParent: "resource:///actors/AboutReaderParent.sys.mjs", 15 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 16 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 17 }); 18 19 var { ExtensionError } = ExtensionUtils; 20 21 var { defineLazyGetter } = ExtensionCommon; 22 23 const READER_MODE_PREFIX = "about:reader"; 24 25 let tabTracker; 26 let windowTracker; 27 28 function isPrivateTab(nativeTab) { 29 return PrivateBrowsingUtils.isBrowserPrivate(nativeTab.linkedBrowser); 30 } 31 32 /* eslint-disable mozilla/balanced-listeners */ 33 extensions.on("uninstalling", (msg, extension) => { 34 if (extension.uninstallURL) { 35 let browser = windowTracker.topWindow.gBrowser; 36 browser.addTab(extension.uninstallURL, { 37 relatedToCurrent: true, 38 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 39 {} 40 ), 41 }); 42 } 43 }); 44 45 extensions.on("page-shutdown", (type, context) => { 46 if (context.viewType == "tab") { 47 if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) { 48 // Only close extension tabs. 49 // This check prevents about:addons from closing when it contains a 50 // WebExtension as an embedded inline options page. 51 return; 52 } 53 let { gBrowser } = context.xulBrowser.ownerGlobal; 54 if (gBrowser && gBrowser.getTabForBrowser) { 55 let nativeTab = gBrowser.getTabForBrowser(context.xulBrowser); 56 if (nativeTab) { 57 gBrowser.removeTab(nativeTab); 58 } 59 } 60 } 61 }); 62 /* eslint-enable mozilla/balanced-listeners */ 63 64 global.openOptionsPage = extension => { 65 let window = windowTracker.topWindow; 66 if (!window) { 67 return Promise.reject({ message: "No browser window available" }); 68 } 69 70 const { optionsPageProperties } = extension; 71 if (!optionsPageProperties) { 72 return Promise.reject({ message: "No options page" }); 73 } 74 if (optionsPageProperties.open_in_tab) { 75 window.switchToTabHavingURI(optionsPageProperties.page, true, { 76 triggeringPrincipal: extension.principal, 77 }); 78 return Promise.resolve(); 79 } 80 81 let viewId = `addons://detail/${encodeURIComponent( 82 extension.id 83 )}/preferences`; 84 85 return window.BrowserAddonUI.openAddonsMgr(viewId); 86 }; 87 88 global.makeWidgetId = id => { 89 id = id.toLowerCase(); 90 // FIXME: This allows for collisions. 91 return id.replace(/[^a-z0-9_-]/g, "_"); 92 }; 93 94 global.clickModifiersFromEvent = event => { 95 const map = { 96 shiftKey: "Shift", 97 altKey: "Alt", 98 metaKey: "Command", 99 ctrlKey: "Ctrl", 100 }; 101 let modifiers = Object.keys(map) 102 .filter(key => event[key]) 103 .map(key => map[key]); 104 105 if (event.ctrlKey && AppConstants.platform === "macosx") { 106 modifiers.push("MacCtrl"); 107 } 108 109 return modifiers; 110 }; 111 112 global.waitForTabLoaded = (tab, url) => { 113 return new Promise(resolve => { 114 windowTracker.addListener("progress", { 115 onLocationChange(browser, webProgress, request, locationURI) { 116 if ( 117 webProgress.isTopLevel && 118 browser.ownerGlobal.gBrowser.getTabForBrowser(browser) == tab && 119 (!url || locationURI.spec == url) 120 ) { 121 windowTracker.removeListener("progress", this); 122 resolve(); 123 } 124 }, 125 }); 126 }); 127 }; 128 129 global.replaceUrlInTab = (gBrowser, tab, uri) => { 130 let loaded = waitForTabLoaded(tab, uri.spec); 131 gBrowser.loadURI(uri, { 132 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, 133 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), // This is safe from this functions usage however it would be preferred not to dot his. 134 }); 135 return loaded; 136 }; 137 138 // The tabs.Tab.groupId type in the public extension API is an integer, 139 // but tabbrowser's tab group ID are strings. This handles the conversion. 140 // 141 // tabbrowser.addTabGroup() generates the internal tab group ID as follows: 142 // internal group id = `${Date.now()}-${Math.round(Math.random() * 100)}`; 143 // After dropping the hyphen ("-"), the result can be coerced into a safe 144 // integer. 145 // 146 // As a safeguard, in case the format changes, we fall back to maintaining 147 // an internal mapping (that never gets cleaned up). 148 // This may change in https://bugzilla.mozilla.org/show_bug.cgi?id=1960104 149 const fallbackTabGroupIdMap = new Map(); 150 let nextFallbackTabGroupId = 1; 151 global.getExtTabGroupIdForInternalTabGroupId = groupIdStr => { 152 const parsedTabId = /^(\d{13})-(\d{1,3})$/.exec(groupIdStr); 153 if (parsedTabId) { 154 const groupId = parsedTabId[1] * 1000 + parseInt(parsedTabId[2], 10); 155 if (Number.isSafeInteger(groupId)) { 156 return groupId; 157 } 158 } 159 // Fall back. 160 let fallbackGroupId = fallbackTabGroupIdMap.get(groupIdStr); 161 if (!fallbackGroupId) { 162 fallbackGroupId = nextFallbackTabGroupId++; 163 fallbackTabGroupIdMap.set(groupIdStr, fallbackGroupId); 164 } 165 return fallbackGroupId; 166 }; 167 global.getInternalTabGroupIdForExtTabGroupId = groupId => { 168 if (Number.isSafeInteger(groupId) && groupId >= 1e15) { 169 // 16 digits - this inverts getExtTabGroupIdForInternalTabGroupId. 170 const groupIdStr = `${Math.floor(groupId / 1000)}-${groupId % 1000}`; 171 return groupIdStr; 172 } 173 for (let [groupIdStr, fallbackGroupId] of fallbackTabGroupIdMap) { 174 if (fallbackGroupId === groupId) { 175 return groupIdStr; 176 } 177 } 178 return null; 179 }; 180 181 /** 182 * Manages tab-specific and window-specific context data, and dispatches 183 * tab select events across all windows. 184 */ 185 global.TabContext = class extends EventEmitter { 186 /** 187 * @param {Function} getDefaultPrototype 188 * Provides the prototype of the context value for a tab or window when there is none. 189 * Called with a XULElement or ChromeWindow argument. 190 * Should return an object or null. 191 */ 192 constructor(getDefaultPrototype) { 193 super(); 194 195 this.getDefaultPrototype = getDefaultPrototype; 196 197 this.tabData = new WeakMap(); 198 199 windowTracker.addListener("progress", this); 200 windowTracker.addListener("TabSelect", this); 201 202 this.tabAdopted = this.tabAdopted.bind(this); 203 tabTracker.on("tab-adopted", this.tabAdopted); 204 } 205 206 /** 207 * Returns the context data associated with `keyObject`. 208 * 209 * @param {XULElement|ChromeWindow} keyObject 210 * Browser tab or browser chrome window. 211 * @returns {object} 212 */ 213 get(keyObject) { 214 if (!this.tabData.has(keyObject)) { 215 let data = Object.create(this.getDefaultPrototype(keyObject)); 216 this.tabData.set(keyObject, data); 217 } 218 219 return this.tabData.get(keyObject); 220 } 221 222 /** 223 * Clears the context data associated with `keyObject`. 224 * 225 * @param {XULElement|ChromeWindow} keyObject 226 * Browser tab or browser chrome window. 227 */ 228 clear(keyObject) { 229 this.tabData.delete(keyObject); 230 } 231 232 handleEvent(event) { 233 if (event.type == "TabSelect") { 234 let nativeTab = event.target; 235 this.emit("tab-select", nativeTab); 236 this.emit("location-change", nativeTab); 237 } 238 } 239 240 onLocationChange(browser, webProgress, request, locationURI, flags) { 241 if (!webProgress.isTopLevel) { 242 // Only pageAction and browserAction are consuming the "location-change" event 243 // to update their per-tab status, and they should only do so in response of 244 // location changes related to the top level frame (See Bug 1493470 for a rationale). 245 return; 246 } 247 let gBrowser = browser.ownerGlobal.gBrowser; 248 let tab = gBrowser.getTabForBrowser(browser); 249 // fromBrowse will be false in case of e.g. a hash change or history.pushState 250 let fromBrowse = !( 251 flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT 252 ); 253 this.emit("location-change", tab, fromBrowse); 254 } 255 256 /** 257 * Persists context data when a tab is moved between windows. 258 * 259 * @param {string} eventType 260 * Event type, should be "tab-adopted". 261 * @param {NativeTab} adoptingTab 262 * The tab which is being opened and adopting `adoptedTab`. 263 * @param {NativeTab} adoptedTab 264 * The tab which is being closed and adopted by `adoptingTab`. 265 */ 266 tabAdopted(eventType, adoptingTab, adoptedTab) { 267 if (!this.tabData.has(adoptedTab)) { 268 return; 269 } 270 // Create a new object (possibly with different inheritance) when a tab is moved 271 // into a new window. But then reassign own properties from the old object. 272 let newData = this.get(adoptingTab); 273 let oldData = this.tabData.get(adoptedTab); 274 this.tabData.delete(adoptedTab); 275 Object.assign(newData, oldData); 276 } 277 278 /** 279 * Makes the TabContext instance stop emitting events. 280 */ 281 shutdown() { 282 windowTracker.removeListener("progress", this); 283 windowTracker.removeListener("TabSelect", this); 284 tabTracker.off("tab-adopted", this.tabAdopted); 285 } 286 }; 287 288 class WindowTracker extends WindowTrackerBase { 289 addProgressListener(window, listener) { 290 window.gBrowser.addTabsProgressListener(listener); 291 } 292 293 removeProgressListener(window, listener) { 294 window.gBrowser.removeTabsProgressListener(listener); 295 } 296 297 /** 298 * @param {BaseContext} context 299 * The extension context 300 * @returns {DOMWindow|null} topNormalWindow 301 * The currently active, or topmost, browser window, or null if no 302 * browser window is currently open. 303 * Will return the topmost "normal" (i.e., not popup) window. 304 */ 305 getTopNormalWindow(context) { 306 let options = { allowPopups: false }; 307 if (!context.privateBrowsingAllowed) { 308 options.private = false; 309 } 310 // bug 1983854 - should this only look for windows on the current 311 // workspace? 312 options.allowFromInactiveWorkspace = true; 313 return BrowserWindowTracker.getTopWindow(options); 314 } 315 } 316 317 class TabTracker extends TabTrackerBase { 318 constructor() { 319 super(); 320 321 this._tabs = new WeakMap(); 322 this._browsers = new WeakMap(); 323 this._tabIds = new Map(); 324 this._nextId = 1; 325 this._deferredTabOpenEvents = new WeakMap(); 326 327 this._handleTabDestroyed = this._handleTabDestroyed.bind(this); 328 } 329 330 init() { 331 if (this.initialized) { 332 return; 333 } 334 this.initialized = true; 335 336 this.adoptedTabs = new WeakSet(); 337 338 this._handleWindowOpen = this._handleWindowOpen.bind(this); 339 this._handleWindowClose = this._handleWindowClose.bind(this); 340 341 windowTracker.addListener("TabClose", this); 342 windowTracker.addListener("TabOpen", this); 343 windowTracker.addListener("TabSelect", this); 344 windowTracker.addListener("TabMultiSelect", this); 345 windowTracker.addOpenListener(this._handleWindowOpen); 346 windowTracker.addCloseListener(this._handleWindowClose); 347 348 AboutReaderParent.addMessageListener("Reader:UpdateReaderButton", this); 349 350 /* eslint-disable mozilla/balanced-listeners */ 351 this.on("tab-detached", this._handleTabDestroyed); 352 this.on("tab-removed", this._handleTabDestroyed); 353 /* eslint-enable mozilla/balanced-listeners */ 354 } 355 356 getId(nativeTab) { 357 let id = this._tabs.get(nativeTab); 358 if (id) { 359 return id; 360 } 361 362 this.init(); 363 364 id = this._nextId++; 365 this.setId(nativeTab, id); 366 return id; 367 } 368 369 getBrowserTabId(browser) { 370 let id = this._browsers.get(browser); 371 if (id) { 372 return id; 373 } 374 375 let tab = browser.ownerGlobal.gBrowser.getTabForBrowser(browser); 376 if (tab) { 377 id = this.getId(tab); 378 this._browsers.set(browser, id); 379 return id; 380 } 381 return -1; 382 } 383 384 setId(nativeTab, id) { 385 if (!nativeTab.parentNode) { 386 throw new Error("Cannot attach ID to a destroyed tab."); 387 } 388 if (nativeTab.ownerGlobal.closed) { 389 throw new Error("Cannot attach ID to a tab in a closed window."); 390 } 391 392 this._tabs.set(nativeTab, id); 393 if (nativeTab.linkedBrowser) { 394 this._browsers.set(nativeTab.linkedBrowser, id); 395 } 396 this._tabIds.set(id, nativeTab); 397 } 398 399 /** 400 * Handles tab adoption when a tab is moved between windows. 401 * Ensures the new tab will have the same ID as the old one, and 402 * emits "tab-adopted", "tab-detached" and "tab-attached" events. 403 * 404 * @param {NativeTab} adoptingTab 405 * The tab which is being opened and adopting `adoptedTab`. 406 * @param {NativeTab} adoptedTab 407 * The tab which is being closed and adopted by `adoptingTab`. 408 */ 409 adopt(adoptingTab, adoptedTab) { 410 if (this.adoptedTabs.has(adoptedTab)) { 411 // The adoption has already been handled. 412 return; 413 } 414 this.adoptedTabs.add(adoptedTab); 415 let tabId = this.getId(adoptedTab); 416 this.setId(adoptingTab, tabId); 417 this.emit("tab-adopted", adoptingTab, adoptedTab); 418 if (this.has("tab-detached")) { 419 let nativeTab = adoptedTab; 420 let adoptedBy = adoptingTab; 421 let oldWindowId = windowTracker.getId(nativeTab.ownerGlobal); 422 let oldPosition = nativeTab._tPos; 423 this.emit("tab-detached", { 424 nativeTab, 425 adoptedBy, 426 tabId, 427 oldWindowId, 428 oldPosition, 429 }); 430 } 431 if (this.has("tab-attached")) { 432 let nativeTab = adoptingTab; 433 let newWindowId = windowTracker.getId(nativeTab.ownerGlobal); 434 let newPosition = nativeTab._tPos; 435 this.emit("tab-attached", { 436 nativeTab, 437 tabId, 438 newWindowId, 439 newPosition, 440 }); 441 } 442 } 443 444 _handleTabDestroyed(event, { nativeTab }) { 445 let id = this._tabs.get(nativeTab); 446 if (id) { 447 this._tabs.delete(nativeTab); 448 if (this._tabIds.get(id) === nativeTab) { 449 this._tabIds.delete(id); 450 } 451 } 452 } 453 454 /** 455 * Returns the XUL <tab> element associated with the given tab ID. If no tab 456 * with the given ID exists, and no default value is provided, an error is 457 * raised, belonging to the scope of the given context. 458 * 459 * @param {integer} tabId 460 * The ID of the tab to retrieve. 461 * @param {*} default_ 462 * The value to return if no tab exists with the given ID. 463 * @returns {Element<tab>} 464 * A XUL <tab> element. 465 */ 466 getTab(tabId, default_ = undefined) { 467 let nativeTab = this._tabIds.get(tabId); 468 if (nativeTab) { 469 return nativeTab; 470 } 471 if (default_ !== undefined) { 472 return default_; 473 } 474 throw new ExtensionError(`Invalid tab ID: ${tabId}`); 475 } 476 477 /** 478 * Sets the opener of `tab` to the ID `openerTabId`. Both tabs must be in the 479 * same window, or this function will throw an error. if `openerTabId` is `-1` 480 * the opener tab is cleared. 481 * 482 * @param {Element} nativeTab The tab for which to set the owner. 483 * @param {number} openerTabId The openerTabId of <tab>. 484 */ 485 setOpener(nativeTab, openerTabId) { 486 let nativeOpenerTab = null; 487 488 if (openerTabId > -1) { 489 nativeOpenerTab = tabTracker.getTab(openerTabId); 490 if (nativeTab.ownerDocument !== nativeOpenerTab.ownerDocument) { 491 throw new ExtensionError( 492 "Opener tab must be in the same window as the tab being updated" 493 ); 494 } 495 } 496 497 if (nativeTab.openerTab !== nativeOpenerTab) { 498 nativeTab.openerTab = nativeOpenerTab; 499 this.emit("tab-openerTabId", { nativeTab, openerTabId }); 500 } 501 } 502 503 deferredForTabOpen(nativeTab) { 504 let deferred = this._deferredTabOpenEvents.get(nativeTab); 505 if (!deferred) { 506 deferred = Promise.withResolvers(); 507 this._deferredTabOpenEvents.set(nativeTab, deferred); 508 deferred.promise.then(() => { 509 this._deferredTabOpenEvents.delete(nativeTab); 510 }); 511 } 512 return deferred; 513 } 514 515 async maybeWaitForTabOpen(nativeTab) { 516 let deferred = this._deferredTabOpenEvents.get(nativeTab); 517 return deferred && deferred.promise; 518 } 519 520 /** 521 * @param {Event} event 522 * The DOM Event to handle. 523 * @private 524 */ 525 handleEvent(event) { 526 let nativeTab = event.target; 527 528 switch (event.type) { 529 case "TabOpen": { 530 let { adoptedTab } = event.detail; 531 if (adoptedTab) { 532 // This tab is being created to adopt a tab from a different window. 533 // Handle the adoption. 534 this.adopt(nativeTab, adoptedTab); 535 } else { 536 // Save the size of the current tab, since the newly-created tab will 537 // likely be active by the time the promise below resolves and the 538 // event is dispatched. 539 const currentTab = nativeTab.ownerGlobal.gBrowser.selectedTab; 540 const { frameLoader } = currentTab.linkedBrowser; 541 const currentTabSize = { 542 width: frameLoader.lazyWidth, 543 height: frameLoader.lazyHeight, 544 }; 545 546 // We need to delay sending this event until the next tick, since the 547 // tab can become selected immediately after "TabOpen", then onCreated 548 // should be fired with `active: true`. 549 let deferred = this.deferredForTabOpen(event.originalTarget); 550 Promise.resolve().then(() => { 551 deferred.resolve(); 552 if (!event.originalTarget.parentNode) { 553 // If the tab is already be destroyed, do nothing. 554 return; 555 } 556 this.emitCreated(event.originalTarget, currentTabSize); 557 }); 558 } 559 break; 560 } 561 562 case "TabClose": { 563 let { adoptedBy } = event.detail; 564 if (adoptedBy) { 565 // This tab is being closed because it was adopted by a new window. 566 // Handle the adoption in case it was created as the first tab of a 567 // new window, and did not have an `adoptedTab` detail when it was 568 // opened. 569 this.adopt(adoptedBy, nativeTab); 570 } else { 571 this.emitRemoved(nativeTab, false); 572 } 573 break; 574 } 575 576 case "TabSelect": 577 // Because we are delaying calling emitCreated above, we also need to 578 // delay sending this event because it shouldn't fire before onCreated. 579 this.maybeWaitForTabOpen(nativeTab).then(() => { 580 if (!nativeTab.parentNode) { 581 // If the tab is already be destroyed, do nothing. 582 return; 583 } 584 this.emitActivated(nativeTab, event.detail.previousTab); 585 }); 586 break; 587 588 case "TabMultiSelect": 589 if (this.has("tabs-highlighted")) { 590 // Because we are delaying calling emitCreated above, we also need to 591 // delay sending this event because it shouldn't fire before onCreated. 592 // event.target is gBrowser, so we don't use maybeWaitForTabOpen. 593 Promise.resolve().then(() => { 594 this.emitHighlighted(event.target.ownerGlobal); 595 }); 596 } 597 break; 598 } 599 } 600 601 /** 602 * @param {object} message 603 * The message to handle. 604 * @private 605 */ 606 receiveMessage(message) { 607 switch (message.name) { 608 case "Reader:UpdateReaderButton": 609 if (message.data && message.data.isArticle !== undefined) { 610 this.emit("tab-isarticle", message); 611 } 612 break; 613 } 614 } 615 616 /** 617 * A private method which is called whenever a new browser window is opened, 618 * and dispatches the necessary events for it. 619 * 620 * @param {DOMWindow} window 621 * The window being opened. 622 * @private 623 */ 624 _handleWindowOpen(window) { 625 const tabToAdopt = window.gBrowserInit.getTabToAdopt(); 626 if (tabToAdopt) { 627 // Note that this event handler depends on running before the 628 // delayed startup code in browser.js, which is currently triggered 629 // by the first MozAfterPaint event. That code handles finally 630 // adopting the tab, and clears it from the arguments list in the 631 // process, so if we run later than it, we're too late. 632 if (window.gBrowser.isTab(tabToAdopt)) { 633 let adoptedBy = window.gBrowser.tabs[0]; 634 this.adopt(adoptedBy, tabToAdopt); 635 } 636 } else { 637 for (let nativeTab of window.gBrowser.tabs) { 638 this.emitCreated(nativeTab); 639 } 640 641 // emitActivated to trigger tab.onActivated/tab.onHighlighted for a newly opened window. 642 this.emitActivated(window.gBrowser.tabs[0]); 643 if (this.has("tabs-highlighted")) { 644 this.emitHighlighted(window); 645 } 646 } 647 } 648 649 /** 650 * A private method which is called whenever a browser window is closed, 651 * and dispatches the necessary events for it. 652 * 653 * @param {DOMWindow} window 654 * The window being closed. 655 * @private 656 */ 657 _handleWindowClose(window) { 658 for (let nativeTab of window.gBrowser.tabs) { 659 if (!this.adoptedTabs.has(nativeTab)) { 660 this.emitRemoved(nativeTab, true); 661 } 662 } 663 } 664 665 /** 666 * Emits a "tab-activated" event for the given tab element. 667 * 668 * @param {NativeTab} nativeTab 669 * The tab element which has been activated. 670 * @param {NativeTab} previousTab 671 * The tab element which was previously activated. 672 * @private 673 */ 674 emitActivated(nativeTab, previousTab = undefined) { 675 let previousTabIsPrivate, previousTabId; 676 if (previousTab && !previousTab.closing) { 677 previousTabId = this.getId(previousTab); 678 previousTabIsPrivate = isPrivateTab(previousTab); 679 } 680 this.emit("tab-activated", { 681 tabId: this.getId(nativeTab), 682 previousTabId, 683 previousTabIsPrivate, 684 windowId: windowTracker.getId(nativeTab.ownerGlobal), 685 nativeTab, 686 }); 687 } 688 689 /** 690 * Emits a "tabs-highlighted" event for the given tab element. 691 * 692 * @param {ChromeWindow} window 693 * The window in which the active tab or the set of multiselected tabs changed. 694 * @private 695 */ 696 emitHighlighted(window) { 697 let tabIds = window.gBrowser.selectedTabs.map(tab => this.getId(tab)); 698 let windowId = windowTracker.getId(window); 699 this.emit("tabs-highlighted", { 700 tabIds, 701 windowId, 702 }); 703 } 704 705 /** 706 * Emits a "tab-created" event for the given tab element. 707 * 708 * @param {NativeTab} nativeTab 709 * The tab element which is being created. 710 * @param {object} [currentTabSize] 711 * The size of the tab element for the currently active tab. 712 * @private 713 */ 714 emitCreated(nativeTab, currentTabSize) { 715 this.emit("tab-created", { 716 nativeTab, 717 currentTabSize, 718 }); 719 } 720 721 /** 722 * Emits a "tab-removed" event for the given tab element. 723 * 724 * @param {NativeTab} nativeTab 725 * The tab element which is being removed. 726 * @param {boolean} isWindowClosing 727 * True if the tab is being removed because the browser window is 728 * closing. 729 * @private 730 */ 731 emitRemoved(nativeTab, isWindowClosing) { 732 let windowId = windowTracker.getId(nativeTab.ownerGlobal); 733 let tabId = this.getId(nativeTab); 734 735 this.emit("tab-removed", { 736 nativeTab, 737 tabId, 738 windowId, 739 isWindowClosing, 740 }); 741 } 742 743 getBrowserData(browser) { 744 let window = browser.ownerGlobal; 745 if (!window) { 746 return { 747 tabId: -1, 748 windowId: -1, 749 }; 750 } 751 let { gBrowser } = window; 752 // Some non-browser windows have gBrowser but not getTabForBrowser! 753 if (!gBrowser || !gBrowser.getTabForBrowser) { 754 if (window.top.document.documentURI === "about:addons") { 755 // When we're loaded into a <browser> inside about:addons, we need to go up 756 // one more level. 757 browser = window.docShell.chromeEventHandler; 758 759 ({ gBrowser } = browser.ownerGlobal); 760 } else { 761 return { 762 tabId: -1, 763 windowId: -1, 764 }; 765 } 766 } 767 768 return { 769 tabId: this.getBrowserTabId(browser), 770 windowId: windowTracker.getId(browser.ownerGlobal), 771 }; 772 } 773 774 getBrowserDataForContext(context) { 775 if (["tab", "background"].includes(context.viewType)) { 776 return this.getBrowserData(context.xulBrowser); 777 } else if (["popup", "sidebar"].includes(context.viewType)) { 778 // popups and sidebars are nested inside a browser element 779 // (with url "chrome://browser/content/webext-panels.xhtml") 780 // and so we look for the corresponding topChromeWindow to 781 // determine the windowId the panel belongs to. 782 const chromeWindow = 783 context.xulBrowser?.ownerGlobal?.browsingContext?.topChromeWindow; 784 const windowId = chromeWindow ? windowTracker.getId(chromeWindow) : -1; 785 return { tabId: -1, windowId }; 786 } 787 788 return { tabId: -1, windowId: -1 }; 789 } 790 791 get activeTab() { 792 let window = windowTracker.topWindow; 793 if (window && window.gBrowser) { 794 return window.gBrowser.selectedTab; 795 } 796 return null; 797 } 798 } 799 800 windowTracker = new WindowTracker(); 801 tabTracker = new TabTracker(); 802 803 Object.assign(global, { tabTracker, windowTracker }); 804 805 class Tab extends TabBase { 806 get _favIconUrl() { 807 return this.window.gBrowser.getIcon(this.nativeTab); 808 } 809 810 get attention() { 811 return this.nativeTab.hasAttribute("attention"); 812 } 813 814 get audible() { 815 return this.nativeTab.soundPlaying; 816 } 817 818 get autoDiscardable() { 819 return !this.nativeTab.undiscardable; 820 } 821 822 get browser() { 823 return this.nativeTab.linkedBrowser; 824 } 825 826 get discarded() { 827 return !this.nativeTab.linkedPanel; 828 } 829 830 get frameLoader() { 831 // If we don't have a frameLoader yet, just return a dummy with no width and 832 // height. 833 return super.frameLoader || { lazyWidth: 0, lazyHeight: 0 }; 834 } 835 836 get hidden() { 837 return this.nativeTab.hidden; 838 } 839 840 get sharingState() { 841 return this.window.gBrowser.getTabSharingState(this.nativeTab); 842 } 843 844 get cookieStoreId() { 845 return getCookieStoreIdForTab(this, this.nativeTab); 846 } 847 848 get openerTabId() { 849 let opener = this.nativeTab.openerTab; 850 if ( 851 opener && 852 opener.parentNode && 853 opener.ownerDocument == this.nativeTab.ownerDocument 854 ) { 855 return tabTracker.getId(opener); 856 } 857 return null; 858 } 859 860 get height() { 861 return this.frameLoader.lazyHeight; 862 } 863 864 get index() { 865 return this.nativeTab._tPos; 866 } 867 868 get mutedInfo() { 869 let { nativeTab } = this; 870 871 let mutedInfo = { muted: nativeTab.muted }; 872 if (nativeTab.muteReason === null) { 873 mutedInfo.reason = "user"; 874 } else if (nativeTab.muteReason) { 875 mutedInfo.reason = "extension"; 876 mutedInfo.extensionId = nativeTab.muteReason; 877 } 878 879 return mutedInfo; 880 } 881 882 get lastAccessed() { 883 return this.nativeTab.lastAccessed; 884 } 885 886 get pinned() { 887 return this.nativeTab.pinned; 888 } 889 890 get active() { 891 return this.nativeTab.selected; 892 } 893 894 get highlighted() { 895 let { selected, multiselected } = this.nativeTab; 896 return selected || multiselected; 897 } 898 899 get status() { 900 if (this.nativeTab.getAttribute("busy") === "true") { 901 return "loading"; 902 } 903 return "complete"; 904 } 905 906 get width() { 907 return this.frameLoader.lazyWidth; 908 } 909 910 get window() { 911 return this.nativeTab.ownerGlobal; 912 } 913 914 get windowId() { 915 return windowTracker.getId(this.window); 916 } 917 918 get isArticle() { 919 return this.nativeTab.linkedBrowser.isArticle; 920 } 921 922 get isInReaderMode() { 923 return this.url && this.url.startsWith(READER_MODE_PREFIX); 924 } 925 926 get successorTabId() { 927 const { successor } = this.nativeTab; 928 return successor ? tabTracker.getId(successor) : -1; 929 } 930 931 get groupId() { 932 const { group } = this.nativeTab; 933 return group ? getExtTabGroupIdForInternalTabGroupId(group.id) : -1; 934 } 935 936 /** 937 * Converts session store data to an object compatible with the return value 938 * of the convert() method, representing that data. 939 * 940 * @param {Extension} extension 941 * The extension for which to convert the data. 942 * @param {object} tabData 943 * Session store data for a closed tab, as returned by 944 * `SessionStore.getClosedTabData()`. 945 * @param {DOMWindow} [window = null] 946 * The browser window which the tab belonged to before it was closed. 947 * May be null if the window the tab belonged to no longer exists. 948 * 949 * @returns {object} 950 * @static 951 */ 952 static convertFromSessionStoreClosedData(extension, tabData, window = null) { 953 let result = { 954 sessionId: String(tabData.closedId), 955 index: tabData.pos ? tabData.pos : 0, 956 windowId: window && windowTracker.getId(window), 957 highlighted: false, 958 active: false, 959 pinned: false, 960 hidden: tabData.state ? tabData.state.hidden : tabData.hidden, 961 incognito: Boolean(tabData.state && tabData.state.isPrivate), 962 lastAccessed: tabData.state 963 ? tabData.state.lastAccessed 964 : tabData.lastAccessed, 965 }; 966 967 let entries = tabData.state ? tabData.state.entries : tabData.entries; 968 let lastTabIndex = tabData.state ? tabData.state.index : tabData.index; 969 970 // Tab may have empty history. 971 if (entries.length) { 972 // We need to take lastTabIndex - 1 because the index in the tab data is 973 // 1-based rather than 0-based. 974 let entry = entries[lastTabIndex - 1]; 975 976 // tabData is a representation of a tab, as stored in the session data, 977 // and given that is not a real nativeTab, we only need to check if the extension 978 // has the "tabs" or host permission (because tabData represents a closed tab, 979 // and so we already know that it can't be the activeTab). 980 if ( 981 extension.hasPermission("tabs") || 982 extension.allowedOrigins.matches(entry.url) 983 ) { 984 result.url = entry.url; 985 result.title = entry.title; 986 if (tabData.image) { 987 result.favIconUrl = tabData.image; 988 } 989 } 990 } 991 992 return result; 993 } 994 } 995 996 class Window extends WindowBase { 997 /** 998 * Update the geometry of the browser window. 999 * 1000 * @param {object} options 1001 * An object containing new values for the window's geometry. 1002 * @param {integer} [options.left] 1003 * The new pixel distance of the left side of the browser window from 1004 * the left of the screen. 1005 * @param {integer} [options.top] 1006 * The new pixel distance of the top side of the browser window from 1007 * the top of the screen. 1008 * @param {integer} [options.width] 1009 * The new pixel width of the window. 1010 * @param {integer} [options.height] 1011 * The new pixel height of the window. 1012 */ 1013 updateGeometry(options) { 1014 let { window } = this; 1015 1016 if (options.left !== null || options.top !== null) { 1017 let left = options.left !== null ? options.left : window.screenX; 1018 let top = options.top !== null ? options.top : window.screenY; 1019 window.moveTo(left, top); 1020 } 1021 1022 if (options.width !== null || options.height !== null) { 1023 let width = options.width !== null ? options.width : window.outerWidth; 1024 let height = 1025 options.height !== null ? options.height : window.outerHeight; 1026 window.resizeTo(width, height); 1027 } 1028 } 1029 1030 get _title() { 1031 return this.window.document.title; 1032 } 1033 1034 setTitlePreface(titlePreface) { 1035 this.window.document.documentElement.setAttribute( 1036 "titlepreface", 1037 titlePreface 1038 ); 1039 } 1040 1041 get focused() { 1042 return this.window.document.hasFocus(); 1043 } 1044 1045 get top() { 1046 return this.window.screenY; 1047 } 1048 1049 get left() { 1050 return this.window.screenX; 1051 } 1052 1053 get width() { 1054 return this.window.outerWidth; 1055 } 1056 1057 get height() { 1058 return this.window.outerHeight; 1059 } 1060 1061 get incognito() { 1062 return PrivateBrowsingUtils.isWindowPrivate(this.window); 1063 } 1064 1065 get alwaysOnTop() { 1066 // We never create alwaysOnTop browser windows. 1067 return false; 1068 } 1069 1070 get isLastFocused() { 1071 return this.window === windowTracker.topWindow; 1072 } 1073 1074 static getState(window) { 1075 const STATES = { 1076 [window.STATE_MAXIMIZED]: "maximized", 1077 [window.STATE_MINIMIZED]: "minimized", 1078 [window.STATE_FULLSCREEN]: "fullscreen", 1079 [window.STATE_NORMAL]: "normal", 1080 }; 1081 return STATES[window.windowState]; 1082 } 1083 1084 get state() { 1085 return Window.getState(this.window); 1086 } 1087 1088 async setState(state) { 1089 let { window } = this; 1090 1091 const expectedState = (function () { 1092 switch (state) { 1093 case "maximized": 1094 return window.STATE_MAXIMIZED; 1095 case "minimized": 1096 case "docked": 1097 return window.STATE_MINIMIZED; 1098 case "normal": 1099 return window.STATE_NORMAL; 1100 case "fullscreen": 1101 return window.STATE_FULLSCREEN; 1102 } 1103 throw new Error(`Unexpected window state: ${state}`); 1104 })(); 1105 1106 const initialState = window.windowState; 1107 if (expectedState == initialState) { 1108 return; 1109 } 1110 1111 // We check for window.fullScreen here to make sure to exit fullscreen even 1112 // if DOM and widget disagree on what the state is. This is a speculative 1113 // fix for bug 1780876, ideally it should not be needed. 1114 if (initialState == window.STATE_FULLSCREEN || window.fullScreen) { 1115 window.fullScreen = false; 1116 } 1117 1118 switch (expectedState) { 1119 case window.STATE_MAXIMIZED: 1120 window.maximize(); 1121 break; 1122 case window.STATE_MINIMIZED: 1123 window.minimize(); 1124 break; 1125 1126 case window.STATE_NORMAL: 1127 // Restore sometimes returns the window to its previous state, rather 1128 // than to the "normal" state, so it may need to be called anywhere from 1129 // zero to two times. 1130 window.restore(); 1131 if (window.windowState !== window.STATE_NORMAL) { 1132 window.restore(); 1133 } 1134 break; 1135 1136 case window.STATE_FULLSCREEN: 1137 window.fullScreen = true; 1138 break; 1139 1140 default: 1141 throw new Error(`Unexpected window state: ${state}`); 1142 } 1143 1144 if (window.windowState != expectedState) { 1145 // On Linux, sizemode changes are asynchronous. Some of them might not 1146 // even happen if the window manager doesn't want to, so wait for a bit 1147 // instead of forever for a sizemode change that might not ever happen. 1148 const noWindowManagerTimeout = 2000; 1149 1150 let onSizeModeChange; 1151 const promiseExpectedSizeMode = new Promise(resolve => { 1152 onSizeModeChange = function () { 1153 if (window.windowState == expectedState) { 1154 resolve(); 1155 } 1156 }; 1157 window.addEventListener("sizemodechange", onSizeModeChange); 1158 }); 1159 1160 await Promise.any([ 1161 promiseExpectedSizeMode, 1162 new Promise(resolve => setTimeout(resolve, noWindowManagerTimeout)), 1163 ]); 1164 1165 window.removeEventListener("sizemodechange", onSizeModeChange); 1166 } 1167 } 1168 1169 *getTabs() { 1170 // A new window is being opened and it is adopting an existing tab, we return 1171 // an empty iterator here because there should be no other tabs to return during 1172 // that duration (See Bug 1458918 for a rationale). 1173 if (this.window.gBrowserInit.isAdoptingTab()) { 1174 return; 1175 } 1176 1177 let { tabManager } = this.extension; 1178 1179 for (let nativeTab of this.window.gBrowser.tabs) { 1180 let tab = tabManager.getWrapper(nativeTab); 1181 if (tab) { 1182 yield tab; 1183 } 1184 } 1185 } 1186 1187 *getHighlightedTabs() { 1188 let { tabManager } = this.extension; 1189 for (let nativeTab of this.window.gBrowser.selectedTabs) { 1190 let tab = tabManager.getWrapper(nativeTab); 1191 if (tab) { 1192 yield tab; 1193 } 1194 } 1195 } 1196 1197 get activeTab() { 1198 let { tabManager } = this.extension; 1199 1200 // A new window is being opened and it is adopting a tab, and we do not create 1201 // a TabWrapper for the tab being adopted because it will go away once the tab 1202 // adoption has been completed (See Bug 1458918 for rationale). 1203 if (this.window.gBrowserInit.isAdoptingTab()) { 1204 return null; 1205 } 1206 1207 return tabManager.getWrapper(this.window.gBrowser.selectedTab); 1208 } 1209 1210 getTabAtIndex(index) { 1211 let nativeTab = this.window.gBrowser.tabs[index]; 1212 if (nativeTab) { 1213 return this.extension.tabManager.getWrapper(nativeTab); 1214 } 1215 } 1216 1217 /** 1218 * Converts session store data to an object compatible with the return value 1219 * of the convert() method, representing that data. 1220 * 1221 * @param {Extension} extension 1222 * The extension for which to convert the data. 1223 * @param {object} windowData 1224 * Session store data for a closed window, as returned by 1225 * `SessionStore.getClosedWindowData()`. 1226 * 1227 * @returns {object} 1228 * @static 1229 */ 1230 static convertFromSessionStoreClosedData(extension, windowData) { 1231 let result = { 1232 sessionId: String(windowData.closedId), 1233 focused: false, 1234 incognito: false, 1235 type: "normal", // this is always "normal" for a closed window 1236 // Bug 1781226: we assert "state" is "normal" in tests, but we could use 1237 // the "sizemode" property if we wanted. 1238 state: "normal", 1239 alwaysOnTop: false, 1240 }; 1241 1242 if (windowData.tabs.length) { 1243 result.tabs = windowData.tabs.map(tabData => { 1244 return Tab.convertFromSessionStoreClosedData(extension, tabData); 1245 }); 1246 } 1247 1248 return result; 1249 } 1250 } 1251 1252 Object.assign(global, { Tab, Window }); 1253 1254 class TabManager extends TabManagerBase { 1255 get(tabId, default_ = undefined) { 1256 let nativeTab = tabTracker.getTab(tabId, default_); 1257 1258 if (nativeTab) { 1259 if (!this.canAccessTab(nativeTab)) { 1260 throw new ExtensionError(`Invalid tab ID: ${tabId}`); 1261 } 1262 return this.getWrapper(nativeTab); 1263 } 1264 return default_; 1265 } 1266 1267 addActiveTabPermission(nativeTab = tabTracker.activeTab) { 1268 return super.addActiveTabPermission(nativeTab); 1269 } 1270 1271 revokeActiveTabPermission(nativeTab = tabTracker.activeTab) { 1272 return super.revokeActiveTabPermission(nativeTab); 1273 } 1274 1275 canAccessTab(nativeTab) { 1276 // Check private browsing access at browser window level. 1277 if (!this.extension.canAccessWindow(nativeTab.ownerGlobal)) { 1278 return false; 1279 } 1280 if ( 1281 this.extension.userContextIsolation && 1282 !this.extension.canAccessContainer(nativeTab.userContextId) 1283 ) { 1284 return false; 1285 } 1286 return true; 1287 } 1288 1289 wrapTab(nativeTab) { 1290 return new Tab(this.extension, nativeTab, tabTracker.getId(nativeTab)); 1291 } 1292 1293 getWrapper(nativeTab) { 1294 if (!nativeTab.ownerGlobal.gBrowserInit.isAdoptingTab()) { 1295 return super.getWrapper(nativeTab); 1296 } 1297 } 1298 } 1299 1300 class WindowManager extends WindowManagerBase { 1301 get(windowId, context) { 1302 let window = windowTracker.getWindow(windowId, context); 1303 1304 return this.getWrapper(window); 1305 } 1306 1307 *getAll(context) { 1308 for (let window of windowTracker.browserWindows()) { 1309 if (!this.canAccessWindow(window, context)) { 1310 continue; 1311 } 1312 let wrapped = this.getWrapper(window); 1313 if (wrapped) { 1314 yield wrapped; 1315 } 1316 } 1317 } 1318 1319 wrapWindow(window) { 1320 return new Window(this.extension, window, windowTracker.getId(window)); 1321 } 1322 } 1323 1324 // eslint-disable-next-line mozilla/balanced-listeners 1325 extensions.on("startup", (type, extension) => { 1326 defineLazyGetter(extension, "tabManager", () => new TabManager(extension)); 1327 defineLazyGetter( 1328 extension, 1329 "windowManager", 1330 () => new WindowManager(extension) 1331 ); 1332 });