ext-tabs.js (63502B)
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 ChromeUtils.defineESModuleGetters(this, { 10 BrowserUIUtils: "resource:///modules/BrowserUIUtils.sys.mjs", 11 CustomizableUI: 12 "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs", 13 DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs", 14 ExtensionControlledPopup: 15 "resource:///modules/ExtensionControlledPopup.sys.mjs", 16 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 17 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 18 }); 19 20 ChromeUtils.defineLazyGetter(this, "strBundle", function () { 21 return Services.strings.createBundle( 22 "chrome://global/locale/extensions.properties" 23 ); 24 }); 25 26 var { DefaultMap, ExtensionError } = ExtensionUtils; 27 28 const TAB_HIDE_CONFIRMED_TYPE = "tabHideNotification"; 29 30 const TAB_ID_NONE = -1; 31 32 ChromeUtils.defineLazyGetter(this, "tabHidePopup", () => { 33 return new ExtensionControlledPopup({ 34 confirmedType: TAB_HIDE_CONFIRMED_TYPE, 35 popupnotificationId: "extension-tab-hide-notification", 36 descriptionId: "extension-tab-hide-notification-description", 37 descriptionMessageId: "tabHideControlled.message", 38 getLocalizedDescription: (doc, message, addonDetails) => { 39 let image = doc.createXULElement("image"); 40 image.classList.add("extension-controlled-icon", "alltabs-icon"); 41 if (!doc.getElementById("alltabs-button")?.closest("#TabsToolbar")) { 42 image.classList.add("alltabs-icon-generic"); 43 } 44 return BrowserUIUtils.getLocalizedFragment( 45 doc, 46 message, 47 addonDetails, 48 image 49 ); 50 }, 51 learnMoreLink: "extension-hiding-tabs", 52 }); 53 }); 54 55 function showHiddenTabs(id) { 56 for (let win of Services.wm.getEnumerator("navigator:browser")) { 57 if (win.closed || !win.gBrowser) { 58 continue; 59 } 60 61 for (let tab of win.gBrowser.tabs) { 62 if ( 63 tab.hidden && 64 tab.ownerGlobal && 65 SessionStore.getCustomTabValue(tab, "hiddenBy") === id 66 ) { 67 win.gBrowser.showTab(tab); 68 } 69 } 70 } 71 } 72 73 let tabListener = { 74 tabReadyInitialized: false, 75 // Map[tab -> Promise] 76 tabBlockedPromises: new WeakMap(), 77 // Map[tab -> Deferred] 78 tabReadyPromises: new WeakMap(), 79 initializingTabs: new WeakSet(), 80 81 initTabReady() { 82 if (!this.tabReadyInitialized) { 83 windowTracker.addListener("progress", this); 84 85 this.tabReadyInitialized = true; 86 } 87 }, 88 89 onLocationChange(browser, webProgress) { 90 if (webProgress.isTopLevel) { 91 let { gBrowser } = browser.ownerGlobal; 92 let nativeTab = gBrowser.getTabForBrowser(browser); 93 94 // Now we are certain that the first page in the tab was loaded. 95 this.initializingTabs.delete(nativeTab); 96 97 // browser.innerWindowID is now set, resolve the promises if any. 98 let deferred = this.tabReadyPromises.get(nativeTab); 99 if (deferred) { 100 deferred.resolve(nativeTab); 101 this.tabReadyPromises.delete(nativeTab); 102 } 103 } 104 }, 105 106 blockTabUntilRestored(nativeTab) { 107 let promise = ExtensionUtils.promiseEvent(nativeTab, "SSTabRestored").then( 108 ({ target }) => { 109 this.tabBlockedPromises.delete(target); 110 return target; 111 } 112 ); 113 114 this.tabBlockedPromises.set(nativeTab, promise); 115 }, 116 117 /** 118 * Returns a promise that resolves when the tab is ready. 119 * Tabs created via the `tabs.create` method are "ready" once the location 120 * changes to the requested URL. Other tabs are assumed to be ready once their 121 * inner window ID is known. 122 * 123 * @param {XULElement} nativeTab The <tab> element. 124 * @returns {Promise} Resolves with the given tab once ready. 125 */ 126 awaitTabReady(nativeTab) { 127 let deferred = this.tabReadyPromises.get(nativeTab); 128 if (!deferred) { 129 let promise = this.tabBlockedPromises.get(nativeTab); 130 if (promise) { 131 return promise; 132 } 133 deferred = Promise.withResolvers(); 134 if ( 135 !this.initializingTabs.has(nativeTab) && 136 (nativeTab.linkedBrowser.innerWindowID || 137 nativeTab.linkedBrowser.currentURI.spec === "about:blank") 138 ) { 139 deferred.resolve(nativeTab); 140 } else { 141 this.initTabReady(); 142 this.tabReadyPromises.set(nativeTab, deferred); 143 } 144 } 145 return deferred.promise; 146 }, 147 }; 148 149 const allAttrs = new Set([ 150 "attention", 151 "audible", 152 "favIconUrl", 153 "mutedInfo", 154 "sharingState", 155 "title", 156 "autoDiscardable", 157 ]); 158 const allProperties = new Set([ 159 "attention", 160 "audible", 161 "autoDiscardable", 162 "discarded", 163 "favIconUrl", 164 "groupId", 165 "hidden", 166 "isArticle", 167 "mutedInfo", 168 "openerTabId", 169 "pinned", 170 "sharingState", 171 "status", 172 "title", 173 "url", 174 ]); 175 const restricted = new Set(["url", "favIconUrl", "title"]); 176 177 this.tabs = class extends ExtensionAPIPersistent { 178 static onUpdate(id, manifest) { 179 if (!manifest.permissions || !manifest.permissions.includes("tabHide")) { 180 showHiddenTabs(id); 181 } 182 } 183 184 static onDisable(id) { 185 showHiddenTabs(id); 186 tabHidePopup.clearConfirmation(id); 187 } 188 189 static onUninstall(id) { 190 tabHidePopup.clearConfirmation(id); 191 } 192 193 tabEventRegistrar({ event, listener }) { 194 let { extension } = this; 195 let { tabManager } = extension; 196 return ({ fire }) => { 197 let listener2 = (eventName, eventData, ...args) => { 198 if (!tabManager.canAccessTab(eventData.nativeTab)) { 199 return; 200 } 201 202 listener(fire, eventData, ...args); 203 }; 204 205 tabTracker.on(event, listener2); 206 return { 207 unregister() { 208 tabTracker.off(event, listener2); 209 }, 210 convert(_fire) { 211 fire = _fire; 212 }, 213 }; 214 }; 215 } 216 217 PERSISTENT_EVENTS = { 218 onActivated: this.tabEventRegistrar({ 219 event: "tab-activated", 220 listener: (fire, event) => { 221 let { extension } = this; 222 let { tabId, windowId, previousTabId, previousTabIsPrivate } = event; 223 if (previousTabIsPrivate && !extension.privateBrowsingAllowed) { 224 previousTabId = undefined; 225 } 226 fire.async({ tabId, previousTabId, windowId }); 227 }, 228 }), 229 onAttached: this.tabEventRegistrar({ 230 event: "tab-attached", 231 listener: (fire, event) => { 232 fire.async(event.tabId, { 233 newWindowId: event.newWindowId, 234 newPosition: event.newPosition, 235 }); 236 }, 237 }), 238 onCreated: this.tabEventRegistrar({ 239 event: "tab-created", 240 listener: (fire, event) => { 241 let { tabManager } = this.extension; 242 fire.async(tabManager.convert(event.nativeTab, event.currentTabSize)); 243 }, 244 }), 245 onDetached: this.tabEventRegistrar({ 246 event: "tab-detached", 247 listener: (fire, event) => { 248 fire.async(event.tabId, { 249 oldWindowId: event.oldWindowId, 250 oldPosition: event.oldPosition, 251 }); 252 }, 253 }), 254 onRemoved: this.tabEventRegistrar({ 255 event: "tab-removed", 256 listener: (fire, event) => { 257 fire.async(event.tabId, { 258 windowId: event.windowId, 259 isWindowClosing: event.isWindowClosing, 260 }); 261 }, 262 }), 263 onMoved({ fire }) { 264 let { tabManager } = this.extension; 265 /** 266 * @param {CustomEvent} event 267 */ 268 let moveListener = event => { 269 let nativeTab = event.originalTarget; 270 let { previousTabState, currentTabState } = event.detail; 271 let fromIndex = previousTabState.tabIndex; 272 let toIndex = currentTabState.tabIndex; 273 // TabMove also fires if its tab group changes; we should only fire 274 // event if the position actually moved. 275 if (fromIndex !== toIndex && tabManager.canAccessTab(nativeTab)) { 276 fire.async(tabTracker.getId(nativeTab), { 277 windowId: windowTracker.getId(nativeTab.ownerGlobal), 278 fromIndex, 279 toIndex, 280 }); 281 } 282 }; 283 284 windowTracker.addListener("TabMove", moveListener); 285 return { 286 unregister() { 287 windowTracker.removeListener("TabMove", moveListener); 288 }, 289 convert(_fire) { 290 fire = _fire; 291 }, 292 }; 293 }, 294 295 onHighlighted({ fire, context }) { 296 let { windowManager } = this.extension; 297 let highlightListener = (eventName, event) => { 298 // TODO see if we can avoid "context" here 299 let window = windowTracker.getWindow(event.windowId, context, false); 300 if (!window) { 301 return; 302 } 303 let windowWrapper = windowManager.getWrapper(window); 304 if (!windowWrapper) { 305 return; 306 } 307 let tabIds = Array.from( 308 windowWrapper.getHighlightedTabs(), 309 tab => tab.id 310 ); 311 fire.async({ tabIds: tabIds, windowId: event.windowId }); 312 }; 313 314 tabTracker.on("tabs-highlighted", highlightListener); 315 return { 316 unregister() { 317 tabTracker.off("tabs-highlighted", highlightListener); 318 }, 319 convert(_fire, _context) { 320 fire = _fire; 321 context = _context; 322 }, 323 }; 324 }, 325 326 onUpdated({ fire, context }, params) { 327 let { extension } = this; 328 let { tabManager } = extension; 329 let [filterProps] = params; 330 let filter = { ...filterProps }; 331 if (filter.urls) { 332 filter.urls = new MatchPatternSet(filter.urls, { 333 restrictSchemes: false, 334 }); 335 } 336 let needsModified = true; 337 if (filter.properties) { 338 // Default is to listen for all events. 339 needsModified = filter.properties.some(p => allAttrs.has(p)); 340 filter.properties = new Set(filter.properties); 341 } else { 342 filter.properties = allProperties; 343 } 344 345 function sanitize(tab, changeInfo) { 346 let result = {}; 347 let nonempty = false; 348 for (let prop in changeInfo) { 349 // In practice, changeInfo contains at most one property from 350 // restricted. Therefore it is not necessary to cache the value 351 // of tab.hasTabPermission outside the loop. 352 // Unnecessarily accessing tab.hasTabPermission can cause bugs, see 353 // https://bugzilla.mozilla.org/show_bug.cgi?id=1694699#c21 354 if (!restricted.has(prop) || tab.hasTabPermission) { 355 nonempty = true; 356 result[prop] = changeInfo[prop]; 357 } 358 } 359 return nonempty && result; 360 } 361 362 function getWindowID(windowId) { 363 if (windowId === Window.WINDOW_ID_CURRENT) { 364 let window = windowTracker.getTopWindow(context); 365 if (!window) { 366 return undefined; 367 } 368 return windowTracker.getId(window); 369 } 370 return windowId; 371 } 372 373 function matchFilters(tab) { 374 if (!filterProps) { 375 return true; 376 } 377 if (filter.tabId != null && tab.id != filter.tabId) { 378 return false; 379 } 380 if ( 381 filter.windowId != null && 382 tab.windowId != getWindowID(filter.windowId) 383 ) { 384 return false; 385 } 386 if ( 387 filter.cookieStoreId != null && 388 filter.cookieStoreId !== tab.cookieStoreId 389 ) { 390 return false; 391 } 392 if (filter.urls) { 393 return filter.urls.matches(tab._uri) && tab.hasTabPermission; 394 } 395 return true; 396 } 397 398 let fireForTab = (tab, changed, nativeTab) => { 399 // Tab may be null if private and not_allowed. 400 if (!tab || !matchFilters(tab, changed)) { 401 return; 402 } 403 404 let changeInfo = sanitize(tab, changed); 405 if (changeInfo) { 406 tabTracker.maybeWaitForTabOpen(nativeTab).then(() => { 407 if (!nativeTab.parentNode) { 408 // If the tab is already be destroyed, do nothing. 409 return; 410 } 411 fire.async(tab.id, changeInfo, tab.convert()); 412 }); 413 } 414 }; 415 416 let listener = event => { 417 // tab grouping events are fired on the group, 418 // not the tab itself. 419 let updatedTab = event.originalTarget; 420 if (event.type == "TabGrouped" || event.type == "TabUngrouped") { 421 updatedTab = event.detail; 422 } 423 424 // Ignore any events prior to TabOpen 425 // and events that are triggered while tabs are swapped between windows. 426 if ( 427 updatedTab.initializingTab || 428 updatedTab.ownerGlobal.gBrowserInit?.isAdoptingTab() 429 ) { 430 return; 431 } 432 if (!extension.canAccessWindow(event.originalTarget.ownerGlobal)) { 433 return; 434 } 435 let needed = []; 436 437 if (event.type == "TabAttrModified") { 438 let changed = event.detail.changed; 439 if ( 440 changed.includes("image") && 441 filter.properties.has("favIconUrl") 442 ) { 443 needed.push("favIconUrl"); 444 } 445 if (changed.includes("muted") && filter.properties.has("mutedInfo")) { 446 needed.push("mutedInfo"); 447 } 448 if ( 449 changed.includes("soundplaying") && 450 filter.properties.has("audible") 451 ) { 452 needed.push("audible"); 453 } 454 if ( 455 changed.includes("undiscardable") && 456 filter.properties.has("autoDiscardable") 457 ) { 458 needed.push("autoDiscardable"); 459 } 460 if (changed.includes("label") && filter.properties.has("title")) { 461 needed.push("title"); 462 } 463 if ( 464 changed.includes("sharing") && 465 filter.properties.has("sharingState") 466 ) { 467 needed.push("sharingState"); 468 } 469 if ( 470 changed.includes("attention") && 471 filter.properties.has("attention") 472 ) { 473 needed.push("attention"); 474 } 475 } else if (event.type == "TabPinned") { 476 needed.push("pinned"); 477 } else if (event.type == "TabUnpinned") { 478 needed.push("pinned"); 479 } else if (event.type == "TabBrowserInserted") { 480 // This may be an adopted tab. Bail early to avoid asking tabManager 481 // about the tab before we run the adoption logic in ext-browser.js. 482 if (event.detail.insertedOnTabCreation) { 483 return; 484 } 485 needed.push("discarded"); 486 } else if (event.type == "TabBrowserDiscarded") { 487 needed.push("discarded"); 488 } else if (event.type === "TabGrouped") { 489 needed.push("groupId"); 490 } else if (event.type === "TabUngrouped") { 491 if (updatedTab.group) { 492 // If there is still a group, that means that the group changed, 493 // so TabGrouped will also fire. Ignore to avoid duplicate events. 494 return; 495 } 496 needed.push("groupId"); 497 } else if (event.type == "TabShow") { 498 needed.push("hidden"); 499 } else if (event.type == "TabHide") { 500 needed.push("hidden"); 501 } 502 503 let tab = tabManager.getWrapper(updatedTab); 504 505 let changeInfo = {}; 506 for (let prop of needed) { 507 changeInfo[prop] = tab[prop]; 508 } 509 510 fireForTab(tab, changeInfo, updatedTab); 511 }; 512 513 let statusListener = ({ browser, status, url }) => { 514 let { gBrowser } = browser.ownerGlobal; 515 let tabElem = gBrowser.getTabForBrowser(browser); 516 if (tabElem) { 517 if (!extension.canAccessWindow(tabElem.ownerGlobal)) { 518 return; 519 } 520 521 let changed = {}; 522 if (filter.properties.has("status")) { 523 changed.status = status; 524 } 525 if (url && filter.properties.has("url")) { 526 changed.url = url; 527 } 528 529 fireForTab(tabManager.wrapTab(tabElem), changed, tabElem); 530 } 531 }; 532 533 let isArticleChangeListener = (messageName, message) => { 534 let { gBrowser } = message.target.ownerGlobal; 535 let nativeTab = gBrowser.getTabForBrowser(message.target); 536 537 if (nativeTab && extension.canAccessWindow(nativeTab.ownerGlobal)) { 538 let tab = tabManager.getWrapper(nativeTab); 539 fireForTab(tab, { isArticle: message.data.isArticle }, nativeTab); 540 } 541 }; 542 543 let openerTabIdChangeListener = (_, { nativeTab, openerTabId }) => { 544 let tab = tabManager.getWrapper(nativeTab); 545 fireForTab(tab, { openerTabId }, nativeTab); 546 }; 547 548 let listeners = new Map(); 549 if (filter.properties.has("status") || filter.properties.has("url")) { 550 listeners.set("status", statusListener); 551 } 552 if (needsModified) { 553 listeners.set("TabAttrModified", listener); 554 } 555 if (filter.properties.has("pinned")) { 556 listeners.set("TabPinned", listener); 557 listeners.set("TabUnpinned", listener); 558 } 559 if (filter.properties.has("discarded")) { 560 listeners.set("TabBrowserInserted", listener); 561 listeners.set("TabBrowserDiscarded", listener); 562 } 563 if (filter.properties.has("groupId")) { 564 listeners.set("TabGrouped", listener); 565 listeners.set("TabUngrouped", listener); 566 } 567 if (filter.properties.has("hidden")) { 568 listeners.set("TabShow", listener); 569 listeners.set("TabHide", listener); 570 } 571 572 for (let [name, listener] of listeners) { 573 windowTracker.addListener(name, listener); 574 } 575 576 if (filter.properties.has("isArticle")) { 577 tabTracker.on("tab-isarticle", isArticleChangeListener); 578 } 579 580 if (filter.properties.has("openerTabId")) { 581 tabTracker.on("tab-openerTabId", openerTabIdChangeListener); 582 } 583 584 return { 585 unregister() { 586 for (let [name, listener] of listeners) { 587 windowTracker.removeListener(name, listener); 588 } 589 590 if (filter.properties.has("isArticle")) { 591 tabTracker.off("tab-isarticle", isArticleChangeListener); 592 } 593 594 if (filter.properties.has("openerTabId")) { 595 tabTracker.off("tab-openerTabId", openerTabIdChangeListener); 596 } 597 }, 598 convert(_fire, _context) { 599 fire = _fire; 600 context = _context; 601 }, 602 }; 603 }, 604 }; 605 606 getAPI(context) { 607 let { extension } = context; 608 let { tabManager, windowManager } = extension; 609 let extensionApi = this; 610 let module = "tabs"; 611 612 function getTabOrActive(tabId) { 613 let tab = 614 tabId !== null ? tabTracker.getTab(tabId) : tabTracker.activeTab; 615 if (!tabManager.canAccessTab(tab)) { 616 throw new ExtensionError( 617 tabId === null 618 ? "Cannot access activeTab" 619 : `Invalid tab ID: ${tabId}` 620 ); 621 } 622 return tab; 623 } 624 625 function getNativeTabsFromIDArray(tabIds) { 626 if (!Array.isArray(tabIds)) { 627 tabIds = [tabIds]; 628 } 629 return tabIds.map(tabId => { 630 let tab = tabTracker.getTab(tabId); 631 if (!tabManager.canAccessTab(tab)) { 632 throw new ExtensionError(`Invalid tab ID: ${tabId}`); 633 } 634 return tab; 635 }); 636 } 637 638 async function promiseTabWhenReady(tabId) { 639 let tab; 640 if (tabId !== null) { 641 tab = tabManager.get(tabId); 642 } else { 643 tab = tabManager.getWrapper(tabTracker.activeTab); 644 } 645 if (!tab) { 646 throw new ExtensionError( 647 tabId == null ? "Cannot access activeTab" : `Invalid tab ID: ${tabId}` 648 ); 649 } 650 651 await tabListener.awaitTabReady(tab.nativeTab); 652 653 return tab; 654 } 655 656 function setContentTriggeringPrincipal(url, browser, options) { 657 // For urls that we want to allow an extension to open in a tab, but 658 // that it may not otherwise have access to, we set the triggering 659 // principal to the url that is being opened. This is used for newtab, 660 // about: and moz-extension: protocols. 661 options.triggeringPrincipal = 662 Services.scriptSecurityManager.createContentPrincipal( 663 Services.io.newURI(url), 664 { 665 userContextId: options.userContextId, 666 privateBrowsingId: PrivateBrowsingUtils.isBrowserPrivate(browser) 667 ? 1 668 : 0, 669 } 670 ); 671 } 672 673 let tabsApi = { 674 tabs: { 675 onActivated: new EventManager({ 676 context, 677 module, 678 event: "onActivated", 679 extensionApi, 680 }).api(), 681 682 onCreated: new EventManager({ 683 context, 684 module, 685 event: "onCreated", 686 extensionApi, 687 }).api(), 688 689 onHighlighted: new EventManager({ 690 context, 691 module, 692 event: "onHighlighted", 693 extensionApi, 694 }).api(), 695 696 onAttached: new EventManager({ 697 context, 698 module, 699 event: "onAttached", 700 extensionApi, 701 }).api(), 702 703 onDetached: new EventManager({ 704 context, 705 module, 706 event: "onDetached", 707 extensionApi, 708 }).api(), 709 710 onRemoved: new EventManager({ 711 context, 712 module, 713 event: "onRemoved", 714 extensionApi, 715 }).api(), 716 717 onReplaced: new EventManager({ 718 context, 719 name: "tabs.onReplaced", 720 register: () => { 721 return () => {}; 722 }, 723 }).api(), 724 725 onMoved: new EventManager({ 726 context, 727 module, 728 event: "onMoved", 729 extensionApi, 730 }).api(), 731 732 onUpdated: new EventManager({ 733 context, 734 module, 735 event: "onUpdated", 736 extensionApi, 737 }).api(), 738 739 create(createProperties) { 740 return new Promise(resolve => { 741 let window = 742 createProperties.windowId !== null 743 ? windowTracker.getWindow(createProperties.windowId, context) 744 : windowTracker.getTopNormalWindow(context); 745 if (!window || !context.canAccessWindow(window)) { 746 throw new Error( 747 "Not allowed to create tabs on the target window" 748 ); 749 } 750 let { gBrowserInit } = window; 751 if (!gBrowserInit || !gBrowserInit.delayedStartupFinished) { 752 let obs = finishedWindow => { 753 if (finishedWindow != window) { 754 return; 755 } 756 Services.obs.removeObserver( 757 obs, 758 "browser-delayed-startup-finished" 759 ); 760 resolve(window); 761 }; 762 Services.obs.addObserver(obs, "browser-delayed-startup-finished"); 763 } else { 764 resolve(window); 765 } 766 }).then(window => { 767 let url; 768 769 let options = { triggeringPrincipal: context.principal }; 770 if (createProperties.cookieStoreId) { 771 // May throw if validation fails. 772 options.userContextId = getUserContextIdForCookieStoreId( 773 extension, 774 createProperties.cookieStoreId, 775 PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser) 776 ); 777 } 778 779 if (createProperties.url !== null) { 780 url = context.uri.resolve(createProperties.url); 781 782 if ( 783 !ExtensionUtils.isExtensionUrl(url) && 784 !context.checkLoadURL(url, { dontReportErrors: true }) 785 ) { 786 return Promise.reject({ message: `Illegal URL: ${url}` }); 787 } 788 789 if (createProperties.openInReaderMode) { 790 url = `about:reader?url=${encodeURIComponent(url)}`; 791 } 792 } else { 793 url = window.BROWSER_NEW_TAB_URL; 794 } 795 let discardable = url && !url.startsWith("about:"); 796 // Handle moz-ext separately from the discardable flag to retain prior behavior. 797 if (!discardable || ExtensionUtils.isExtensionUrl(url)) { 798 setContentTriggeringPrincipal(url, window.gBrowser, options); 799 } 800 801 tabListener.initTabReady(); 802 const currentTab = window.gBrowser.selectedTab; 803 const { frameLoader } = currentTab.linkedBrowser; 804 const currentTabSize = { 805 width: frameLoader.lazyWidth, 806 height: frameLoader.lazyHeight, 807 }; 808 809 if (createProperties.openerTabId !== null) { 810 options.ownerTab = tabTracker.getTab( 811 createProperties.openerTabId 812 ); 813 options.openerBrowser = options.ownerTab.linkedBrowser; 814 if (options.ownerTab.ownerGlobal !== window) { 815 return Promise.reject({ 816 message: 817 "Opener tab must be in the same window as the tab being created", 818 }); 819 } 820 } 821 822 if (createProperties.index != null) { 823 options.tabIndex = createProperties.index; 824 } 825 826 if (createProperties.pinned != null) { 827 options.pinned = createProperties.pinned; 828 } 829 830 let active = 831 createProperties.active !== null 832 ? createProperties.active 833 : !createProperties.discarded; 834 if (createProperties.discarded) { 835 if (active) { 836 return Promise.reject({ 837 message: `Active tabs cannot be created and discarded.`, 838 }); 839 } 840 if (createProperties.pinned) { 841 return Promise.reject({ 842 message: `Pinned tabs cannot be created and discarded.`, 843 }); 844 } 845 if (!discardable) { 846 return Promise.reject({ 847 message: `Cannot create a discarded new tab or "about" urls.`, 848 }); 849 } 850 options.createLazyBrowser = true; 851 options.lazyTabTitle = createProperties.title; 852 } else if (createProperties.title) { 853 return Promise.reject({ 854 message: `Title may only be set for discarded tabs.`, 855 }); 856 } 857 858 let nativeTab = window.gBrowser.addTab(url, options); 859 860 if (active) { 861 window.gBrowser.selectedTab = nativeTab; 862 if (!createProperties.url) { 863 window.gURLBar.select(); 864 } 865 } 866 867 if ( 868 createProperties.url && 869 createProperties.url !== window.BROWSER_NEW_TAB_URL && 870 !createProperties.url.startsWith("about:blank") 871 ) { 872 // We can't wait for a location change event for about:newtab, 873 // since it may be pre-rendered, in which case its initial 874 // location change event has already fired. 875 // The same goes for about:blank, since the initial blank document 876 // is loaded synchronously. 877 878 // Mark the tab as initializing, so that operations like 879 // `executeScript` wait until the requested URL is loaded in 880 // the tab before dispatching messages to the inner window 881 // that contains the URL we're attempting to load. 882 tabListener.initializingTabs.add(nativeTab); 883 } 884 885 if (createProperties.muted) { 886 nativeTab.toggleMuteAudio(extension.id); 887 } 888 889 return tabManager.convert(nativeTab, currentTabSize); 890 }); 891 }, 892 893 async remove(tabIds) { 894 let nativeTabs = getNativeTabsFromIDArray(tabIds); 895 896 if (nativeTabs.length === 1) { 897 nativeTabs[0].ownerGlobal.gBrowser.removeTab(nativeTabs[0]); 898 return; 899 } 900 901 // Or for multiple tabs, first group them by window 902 let windowTabMap = new DefaultMap(() => []); 903 for (let nativeTab of nativeTabs) { 904 windowTabMap.get(nativeTab.ownerGlobal).push(nativeTab); 905 } 906 907 // Then make one call to removeTabs() for each window, to keep the 908 // count accurate for SessionStore.getLastClosedTabCount(). 909 // Note: always pass options to disable animation and the warning 910 // dialogue box, so that way all tabs are actually closed when the 911 // browser.tabs.remove() promise resolves 912 for (let [eachWindow, tabsToClose] of windowTabMap.entries()) { 913 eachWindow.gBrowser.removeTabs(tabsToClose, { 914 animate: false, 915 suppressWarnAboutClosingWindow: true, 916 }); 917 } 918 }, 919 920 async discard(tabIds) { 921 let nativeTabs = getNativeTabsFromIDArray(tabIds); 922 await Promise.all( 923 nativeTabs.map(nativeTab => 924 nativeTab.ownerGlobal.gBrowser.prepareDiscardBrowser(nativeTab) 925 ) 926 ); 927 for (let nativeTab of nativeTabs) { 928 nativeTab.ownerGlobal.gBrowser.discardBrowser(nativeTab); 929 } 930 }, 931 932 async update(tabId, updateProperties) { 933 let nativeTab = getTabOrActive(tabId); 934 935 let tabbrowser = nativeTab.ownerGlobal.gBrowser; 936 937 if (updateProperties.url !== null) { 938 let url = context.uri.resolve(updateProperties.url); 939 940 let options = { 941 flags: updateProperties.loadReplace 942 ? Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY 943 : Ci.nsIWebNavigation.LOAD_FLAGS_NONE, 944 triggeringPrincipal: context.principal, 945 }; 946 947 if (!context.checkLoadURL(url, { dontReportErrors: true })) { 948 // We allow loading top level tabs for "other" extensions. 949 if (ExtensionUtils.isExtensionUrl(url)) { 950 setContentTriggeringPrincipal(url, tabbrowser, options); 951 } else { 952 return Promise.reject({ message: `Illegal URL: ${url}` }); 953 } 954 } 955 956 let browser = nativeTab.linkedBrowser; 957 if (nativeTab.linkedPanel) { 958 browser.fixupAndLoadURIString(url, options); 959 } else { 960 // Shift to fully loaded browser and make 961 // sure load handler is instantiated. 962 nativeTab.addEventListener( 963 "SSTabRestoring", 964 () => browser.fixupAndLoadURIString(url, options), 965 { once: true } 966 ); 967 tabbrowser._insertBrowser(nativeTab); 968 } 969 } 970 971 if (updateProperties.active) { 972 tabbrowser.selectedTab = nativeTab; 973 } 974 if (updateProperties.autoDiscardable !== null) { 975 nativeTab.undiscardable = !updateProperties.autoDiscardable; 976 } 977 if (updateProperties.highlighted !== null) { 978 if (updateProperties.highlighted) { 979 if (!nativeTab.selected && !nativeTab.multiselected) { 980 tabbrowser.addToMultiSelectedTabs(nativeTab); 981 // Select the highlighted tab unless active:false is provided. 982 // Note that Chrome selects it even in that case. 983 if (updateProperties.active !== false) { 984 tabbrowser.lockClearMultiSelectionOnce(); 985 tabbrowser.selectedTab = nativeTab; 986 } 987 } 988 } else { 989 tabbrowser.removeFromMultiSelectedTabs(nativeTab); 990 } 991 } 992 if (updateProperties.muted !== null) { 993 if (nativeTab.muted != updateProperties.muted) { 994 nativeTab.toggleMuteAudio(extension.id); 995 } 996 } 997 if (updateProperties.pinned !== null) { 998 if (updateProperties.pinned) { 999 tabbrowser.pinTab(nativeTab); 1000 } else { 1001 tabbrowser.unpinTab(nativeTab); 1002 } 1003 } 1004 if (updateProperties.openerTabId !== null) { 1005 tabTracker.setOpener(nativeTab, updateProperties.openerTabId); 1006 } 1007 if (updateProperties.successorTabId !== null) { 1008 let successor = null; 1009 if (updateProperties.successorTabId !== TAB_ID_NONE) { 1010 successor = tabTracker.getTab( 1011 updateProperties.successorTabId, 1012 null 1013 ); 1014 if (!successor) { 1015 throw new ExtensionError("Invalid successorTabId"); 1016 } 1017 // This also ensures "privateness" matches. 1018 if (successor.ownerDocument !== nativeTab.ownerDocument) { 1019 throw new ExtensionError( 1020 "Successor tab must be in the same window as the tab being updated" 1021 ); 1022 } 1023 } 1024 tabbrowser.setSuccessor(nativeTab, successor); 1025 } 1026 1027 return tabManager.convert(nativeTab); 1028 }, 1029 1030 async reload(tabId, reloadProperties) { 1031 let nativeTab = getTabOrActive(tabId); 1032 1033 let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; 1034 if (reloadProperties && reloadProperties.bypassCache) { 1035 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; 1036 } 1037 nativeTab.linkedBrowser.reloadWithFlags(flags); 1038 }, 1039 1040 async warmup(tabId) { 1041 let nativeTab = tabTracker.getTab(tabId); 1042 if (!tabManager.canAccessTab(nativeTab)) { 1043 throw new ExtensionError(`Invalid tab ID: ${tabId}`); 1044 } 1045 let tabbrowser = nativeTab.ownerGlobal.gBrowser; 1046 tabbrowser.warmupTab(nativeTab); 1047 }, 1048 1049 async get(tabId) { 1050 return tabManager.get(tabId).convert(); 1051 }, 1052 1053 getCurrent() { 1054 let tabData; 1055 if (context.tabId) { 1056 tabData = tabManager.get(context.tabId).convert(); 1057 } 1058 return Promise.resolve(tabData); 1059 }, 1060 1061 async query(queryInfo) { 1062 return Array.from(tabManager.query(queryInfo, context), tab => 1063 tab.convert() 1064 ); 1065 }, 1066 1067 async captureTab(tabId, options) { 1068 let nativeTab = getTabOrActive(tabId); 1069 await tabListener.awaitTabReady(nativeTab); 1070 1071 let browser = nativeTab.linkedBrowser; 1072 let window = browser.ownerGlobal; 1073 let zoom = window.ZoomManager.getZoomForBrowser(browser); 1074 1075 let tab = tabManager.wrapTab(nativeTab); 1076 return tab.capture(context, zoom, options); 1077 }, 1078 1079 async captureVisibleTab(windowId, options) { 1080 let window = 1081 windowId == null 1082 ? windowTracker.getTopWindow(context) 1083 : windowTracker.getWindow(windowId, context); 1084 1085 let tab = tabManager.getWrapper(window.gBrowser.selectedTab); 1086 if ( 1087 !extension.hasPermission("<all_urls>") && 1088 !tab.hasActiveTabPermission 1089 ) { 1090 throw new ExtensionError("Missing activeTab permission"); 1091 } 1092 await tabListener.awaitTabReady(tab.nativeTab); 1093 1094 let zoom = window.ZoomManager.getZoomForBrowser( 1095 tab.nativeTab.linkedBrowser 1096 ); 1097 return tab.capture(context, zoom, options); 1098 }, 1099 1100 async detectLanguage(tabId) { 1101 let tab = await promiseTabWhenReady(tabId); 1102 let results = await tab.queryContent("DetectLanguage", {}); 1103 return results[0]; 1104 }, 1105 1106 async executeScript(tabId, details) { 1107 let tab = await promiseTabWhenReady(tabId); 1108 return tab.executeScript(context, details); 1109 }, 1110 1111 async insertCSS(tabId, details) { 1112 let tab = await promiseTabWhenReady(tabId); 1113 return tab.insertCSS(context, details); 1114 }, 1115 1116 async removeCSS(tabId, details) { 1117 let tab = await promiseTabWhenReady(tabId); 1118 return tab.removeCSS(context, details); 1119 }, 1120 1121 async move(tabIds, moveProperties) { 1122 let tabsMoved = []; 1123 if (!Array.isArray(tabIds)) { 1124 tabIds = [tabIds]; 1125 } 1126 1127 let destinationWindow = null; 1128 if (moveProperties.windowId !== null) { 1129 destinationWindow = windowTracker.getWindow( 1130 moveProperties.windowId, 1131 context 1132 ); 1133 // Fail on an invalid window. 1134 if (!destinationWindow) { 1135 return Promise.reject({ 1136 message: `Invalid window ID: ${moveProperties.windowId}`, 1137 }); 1138 } 1139 } 1140 1141 /* 1142 Indexes are maintained on a per window basis so that a call to 1143 move([tabA, tabB], {index: 0}) 1144 -> tabA to 0, tabB to 1 if tabA and tabB are in the same window 1145 move([tabA, tabB], {index: 0}) 1146 -> tabA to 0, tabB to 0 if tabA and tabB are in different windows 1147 */ 1148 let lastInsertionMap = new Map(); 1149 1150 for (let nativeTab of getNativeTabsFromIDArray(tabIds)) { 1151 // If the window is not specified, use the window from the tab. 1152 let window = destinationWindow || nativeTab.ownerGlobal; 1153 let isSameWindow = nativeTab.ownerGlobal == window; 1154 let gBrowser = window.gBrowser; 1155 1156 // If we are not moving the tab to a different window, and the window 1157 // only has one tab, do nothing. 1158 if (isSameWindow && gBrowser.tabs.length === 1) { 1159 lastInsertionMap.set(window, 0); 1160 continue; 1161 } 1162 // If moving between windows, be sure privacy matches. While gBrowser 1163 // prevents this, we want to silently ignore it. 1164 if ( 1165 !isSameWindow && 1166 PrivateBrowsingUtils.isBrowserPrivate(gBrowser) != 1167 PrivateBrowsingUtils.isBrowserPrivate( 1168 nativeTab.ownerGlobal.gBrowser 1169 ) 1170 ) { 1171 continue; 1172 } 1173 1174 let insertionPoint; 1175 let lastInsertion = lastInsertionMap.get(window); 1176 if (lastInsertion == null) { 1177 insertionPoint = moveProperties.index; 1178 let maxIndex = gBrowser.tabs.length - (isSameWindow ? 1 : 0); 1179 if (insertionPoint == -1) { 1180 // If the index is -1 it should go to the end of the tabs. 1181 insertionPoint = maxIndex; 1182 } else { 1183 insertionPoint = Math.min(insertionPoint, maxIndex); 1184 } 1185 } else if (isSameWindow && nativeTab._tPos <= lastInsertion) { 1186 // lastInsertion is the current index of the last inserted tab. 1187 // insertionPoint is the desired index of the current tab *after* moving it. 1188 // When the tab is moved, the last inserted tab will no longer be at index 1189 // lastInsertion, but (lastInsertion - 1). To position the tabs adjacent to 1190 // each other, the tab should therefore be at index (lastInsertion - 1 + 1). 1191 insertionPoint = lastInsertion; 1192 } else { 1193 // In this case the last inserted tab will stay at index lastInsertion, 1194 // so we should move the current tab to index (lastInsertion + 1). 1195 insertionPoint = lastInsertion + 1; 1196 } 1197 1198 // We can only move pinned tabs to a point within, or just after, 1199 // the current set of pinned tabs. Unpinned tabs, likewise, can only 1200 // be moved to a position after the current set of pinned tabs. 1201 // Attempts to move a tab to an illegal position are ignored. 1202 let numPinned = gBrowser.pinnedTabCount; 1203 let ok = nativeTab.pinned 1204 ? insertionPoint <= numPinned 1205 : insertionPoint >= numPinned; 1206 if (!ok) { 1207 continue; 1208 } 1209 1210 if (isSameWindow) { 1211 // If the window we are moving is the same, just move the tab. 1212 gBrowser.moveTabTo(nativeTab, { tabIndex: insertionPoint }); 1213 } else { 1214 // If the window we are moving the tab in is different, then move the tab 1215 // to the new window. 1216 nativeTab = gBrowser.adoptTab(nativeTab, { 1217 tabIndex: insertionPoint, 1218 }); 1219 } 1220 lastInsertionMap.set(window, nativeTab._tPos); 1221 tabsMoved.push(nativeTab); 1222 } 1223 1224 return tabsMoved.map(nativeTab => tabManager.convert(nativeTab)); 1225 }, 1226 1227 duplicate(tabId, duplicateProperties) { 1228 const { active, index: tabIndex } = duplicateProperties || {}; 1229 const inBackground = active === undefined ? false : !active; 1230 1231 // Schema requires tab id. 1232 let nativeTab = getTabOrActive(tabId); 1233 1234 let gBrowser = nativeTab.ownerGlobal.gBrowser; 1235 let newTab = gBrowser.duplicateTab(nativeTab, true, { 1236 inBackground, 1237 tabIndex, 1238 }); 1239 1240 tabListener.blockTabUntilRestored(newTab); 1241 return new Promise(resolve => { 1242 // Use SSTabRestoring to ensure that the tab's URL is ready before 1243 // resolving the promise. 1244 newTab.addEventListener( 1245 "SSTabRestoring", 1246 () => resolve(tabManager.convert(newTab)), 1247 { once: true } 1248 ); 1249 }); 1250 }, 1251 1252 getZoom(tabId) { 1253 let nativeTab = getTabOrActive(tabId); 1254 1255 let { ZoomManager } = nativeTab.ownerGlobal; 1256 let zoom = ZoomManager.getZoomForBrowser(nativeTab.linkedBrowser); 1257 1258 return Promise.resolve(zoom); 1259 }, 1260 1261 setZoom(tabId, zoom) { 1262 let nativeTab = getTabOrActive(tabId); 1263 1264 let { FullZoom, ZoomManager } = nativeTab.ownerGlobal; 1265 1266 if (zoom === 0) { 1267 // A value of zero means use the default zoom factor. 1268 return FullZoom.reset(nativeTab.linkedBrowser); 1269 } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) { 1270 FullZoom.setZoom(zoom, nativeTab.linkedBrowser); 1271 } else { 1272 return Promise.reject({ 1273 message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`, 1274 }); 1275 } 1276 1277 return Promise.resolve(); 1278 }, 1279 1280 async getZoomSettings(tabId) { 1281 let nativeTab = getTabOrActive(tabId); 1282 1283 let { FullZoom, ZoomUI } = nativeTab.ownerGlobal; 1284 1285 return { 1286 mode: "automatic", 1287 scope: FullZoom.siteSpecific ? "per-origin" : "per-tab", 1288 defaultZoomFactor: await ZoomUI.getGlobalValue(), 1289 }; 1290 }, 1291 1292 async setZoomSettings(tabId, settings) { 1293 let nativeTab = getTabOrActive(tabId); 1294 1295 let currentSettings = await this.getZoomSettings( 1296 tabTracker.getId(nativeTab) 1297 ); 1298 1299 if ( 1300 !Object.keys(settings).every( 1301 key => settings[key] === currentSettings[key] 1302 ) 1303 ) { 1304 throw new ExtensionError( 1305 `Unsupported zoom settings: ${JSON.stringify(settings)}` 1306 ); 1307 } 1308 }, 1309 1310 onZoomChange: new EventManager({ 1311 context, 1312 name: "tabs.onZoomChange", 1313 register: fire => { 1314 let getZoomLevel = browser => { 1315 let { ZoomManager } = browser.ownerGlobal; 1316 1317 return ZoomManager.getZoomForBrowser(browser); 1318 }; 1319 1320 // Stores the last known zoom level for each tab's browser. 1321 // WeakMap[<browser> -> number] 1322 let zoomLevels = new WeakMap(); 1323 1324 // Store the zoom level for all existing tabs. 1325 for (let window of windowTracker.browserWindows()) { 1326 if (!context.canAccessWindow(window)) { 1327 continue; 1328 } 1329 for (let nativeTab of window.gBrowser.tabs) { 1330 let browser = nativeTab.linkedBrowser; 1331 zoomLevels.set(browser, getZoomLevel(browser)); 1332 } 1333 } 1334 1335 let tabCreated = (eventName, event) => { 1336 let browser = event.nativeTab.linkedBrowser; 1337 if (!event.isPrivate || context.privateBrowsingAllowed) { 1338 zoomLevels.set(browser, getZoomLevel(browser)); 1339 } 1340 }; 1341 1342 let zoomListener = async event => { 1343 let browser = event.originalTarget; 1344 1345 // For non-remote browsers, this event is dispatched on the document 1346 // rather than on the <browser>. But either way we have a node here. 1347 if (browser.nodeType == browser.DOCUMENT_NODE) { 1348 browser = browser.docShell.chromeEventHandler; 1349 } 1350 1351 if (!context.canAccessWindow(browser.ownerGlobal)) { 1352 return; 1353 } 1354 1355 let { gBrowser } = browser.ownerGlobal; 1356 let nativeTab = gBrowser.getTabForBrowser(browser); 1357 if (!nativeTab) { 1358 // We only care about zoom events in the top-level browser of a tab. 1359 return; 1360 } 1361 1362 let oldZoomFactor = zoomLevels.get(browser); 1363 let newZoomFactor = getZoomLevel(browser); 1364 1365 if (oldZoomFactor != newZoomFactor) { 1366 zoomLevels.set(browser, newZoomFactor); 1367 1368 let tabId = tabTracker.getId(nativeTab); 1369 fire.async({ 1370 tabId, 1371 oldZoomFactor, 1372 newZoomFactor, 1373 zoomSettings: await tabsApi.tabs.getZoomSettings(tabId), 1374 }); 1375 } 1376 }; 1377 1378 tabTracker.on("tab-attached", tabCreated); 1379 tabTracker.on("tab-created", tabCreated); 1380 1381 windowTracker.addListener("FullZoomChange", zoomListener); 1382 windowTracker.addListener("TextZoomChange", zoomListener); 1383 return () => { 1384 tabTracker.off("tab-attached", tabCreated); 1385 tabTracker.off("tab-created", tabCreated); 1386 1387 windowTracker.removeListener("FullZoomChange", zoomListener); 1388 windowTracker.removeListener("TextZoomChange", zoomListener); 1389 }; 1390 }, 1391 }).api(), 1392 1393 print() { 1394 let activeTab = getTabOrActive(null); 1395 let { PrintUtils } = activeTab.ownerGlobal; 1396 PrintUtils.startPrintWindow(activeTab.linkedBrowser.browsingContext); 1397 }, 1398 1399 // Legacy API 1400 printPreview() { 1401 return Promise.resolve(this.print()); 1402 }, 1403 1404 saveAsPDF(pageSettings) { 1405 let activeTab = getTabOrActive(null); 1406 let picker = Cc["@mozilla.org/filepicker;1"].createInstance( 1407 Ci.nsIFilePicker 1408 ); 1409 let title = strBundle.GetStringFromName( 1410 "saveaspdf.saveasdialog.title" 1411 ); 1412 let filename; 1413 if ( 1414 pageSettings.toFileName !== null && 1415 pageSettings.toFileName != "" 1416 ) { 1417 filename = pageSettings.toFileName; 1418 } else if (activeTab.linkedBrowser.contentTitle != "") { 1419 filename = activeTab.linkedBrowser.contentTitle; 1420 } else { 1421 let url = new URL(activeTab.linkedBrowser.currentURI.spec); 1422 let path = decodeURIComponent(url.pathname); 1423 path = path.replace(/\/$/, ""); 1424 filename = path.split("/").pop(); 1425 if (filename == "") { 1426 filename = url.hostname; 1427 } 1428 } 1429 filename = DownloadPaths.sanitize(filename); 1430 1431 picker.init( 1432 activeTab.ownerGlobal.browsingContext, 1433 title, 1434 Ci.nsIFilePicker.modeSave 1435 ); 1436 picker.appendFilter("PDF", "*.pdf"); 1437 picker.defaultExtension = "pdf"; 1438 picker.defaultString = filename; 1439 1440 return new Promise(resolve => { 1441 picker.open(function (retval) { 1442 if (retval == 0 || retval == 2) { 1443 // OK clicked (retval == 0) or replace confirmed (retval == 2) 1444 1445 // Workaround: When trying to replace an existing file that is open in another application (i.e. a locked file), 1446 // the print progress listener is never called. This workaround ensures that a correct status is always returned. 1447 try { 1448 let fstream = Cc[ 1449 "@mozilla.org/network/file-output-stream;1" 1450 ].createInstance(Ci.nsIFileOutputStream); 1451 fstream.init(picker.file, 0x2a, 0o666, 0); // ioflags = write|create|truncate, file permissions = rw-rw-rw- 1452 fstream.close(); 1453 } catch (e) { 1454 resolve(retval == 0 ? "not_saved" : "not_replaced"); 1455 return; 1456 } 1457 1458 let psService = Cc[ 1459 "@mozilla.org/gfx/printsettings-service;1" 1460 ].getService(Ci.nsIPrintSettingsService); 1461 let printSettings = psService.createNewPrintSettings(); 1462 1463 printSettings.printerName = ""; 1464 printSettings.isInitializedFromPrinter = true; 1465 printSettings.isInitializedFromPrefs = true; 1466 1467 printSettings.outputDestination = 1468 Ci.nsIPrintSettings.kOutputDestinationFile; 1469 printSettings.toFileName = picker.file.path; 1470 1471 printSettings.printSilent = true; 1472 1473 printSettings.outputFormat = 1474 Ci.nsIPrintSettings.kOutputFormatPDF; 1475 1476 if (pageSettings.paperSizeUnit !== null) { 1477 printSettings.paperSizeUnit = pageSettings.paperSizeUnit; 1478 } 1479 if (pageSettings.paperWidth !== null) { 1480 printSettings.paperWidth = pageSettings.paperWidth; 1481 } 1482 if (pageSettings.paperHeight !== null) { 1483 printSettings.paperHeight = pageSettings.paperHeight; 1484 } 1485 if (pageSettings.orientation !== null) { 1486 printSettings.orientation = pageSettings.orientation; 1487 } 1488 if (pageSettings.scaling !== null) { 1489 printSettings.scaling = pageSettings.scaling; 1490 } 1491 if (pageSettings.shrinkToFit !== null) { 1492 printSettings.shrinkToFit = pageSettings.shrinkToFit; 1493 } 1494 if (pageSettings.showBackgroundColors !== null) { 1495 printSettings.printBGColors = 1496 pageSettings.showBackgroundColors; 1497 } 1498 if (pageSettings.showBackgroundImages !== null) { 1499 printSettings.printBGImages = 1500 pageSettings.showBackgroundImages; 1501 } 1502 if (pageSettings.edgeLeft !== null) { 1503 printSettings.edgeLeft = pageSettings.edgeLeft; 1504 } 1505 if (pageSettings.edgeRight !== null) { 1506 printSettings.edgeRight = pageSettings.edgeRight; 1507 } 1508 if (pageSettings.edgeTop !== null) { 1509 printSettings.edgeTop = pageSettings.edgeTop; 1510 } 1511 if (pageSettings.edgeBottom !== null) { 1512 printSettings.edgeBottom = pageSettings.edgeBottom; 1513 } 1514 if (pageSettings.marginLeft !== null) { 1515 printSettings.marginLeft = pageSettings.marginLeft; 1516 } 1517 if (pageSettings.marginRight !== null) { 1518 printSettings.marginRight = pageSettings.marginRight; 1519 } 1520 if (pageSettings.marginTop !== null) { 1521 printSettings.marginTop = pageSettings.marginTop; 1522 } 1523 if (pageSettings.marginBottom !== null) { 1524 printSettings.marginBottom = pageSettings.marginBottom; 1525 } 1526 if (pageSettings.headerLeft !== null) { 1527 printSettings.headerStrLeft = pageSettings.headerLeft; 1528 } 1529 if (pageSettings.headerCenter !== null) { 1530 printSettings.headerStrCenter = pageSettings.headerCenter; 1531 } 1532 if (pageSettings.headerRight !== null) { 1533 printSettings.headerStrRight = pageSettings.headerRight; 1534 } 1535 if (pageSettings.footerLeft !== null) { 1536 printSettings.footerStrLeft = pageSettings.footerLeft; 1537 } 1538 if (pageSettings.footerCenter !== null) { 1539 printSettings.footerStrCenter = pageSettings.footerCenter; 1540 } 1541 if (pageSettings.footerRight !== null) { 1542 printSettings.footerStrRight = pageSettings.footerRight; 1543 } 1544 1545 activeTab.linkedBrowser.browsingContext 1546 .print(printSettings) 1547 .then(() => resolve(retval == 0 ? "saved" : "replaced")) 1548 .catch(() => 1549 resolve(retval == 0 ? "not_saved" : "not_replaced") 1550 ); 1551 } else { 1552 // Cancel clicked (retval == 1) 1553 resolve("canceled"); 1554 } 1555 }); 1556 }); 1557 }, 1558 1559 async toggleReaderMode(tabId) { 1560 let tab = await promiseTabWhenReady(tabId); 1561 if (!tab.isInReaderMode && !tab.isArticle) { 1562 throw new ExtensionError( 1563 "The specified tab cannot be placed into reader mode." 1564 ); 1565 } 1566 let nativeTab = getTabOrActive(tabId); 1567 1568 nativeTab.linkedBrowser.sendMessageToActor( 1569 "Reader:ToggleReaderMode", 1570 {}, 1571 "AboutReader" 1572 ); 1573 }, 1574 1575 moveInSuccession(tabIds, tabId, options) { 1576 const { insert, append } = options || {}; 1577 const tabIdSet = new Set(tabIds); 1578 if (tabIdSet.size !== tabIds.length) { 1579 throw new ExtensionError( 1580 "IDs must not occur more than once in tabIds" 1581 ); 1582 } 1583 if ((append || insert) && tabIdSet.has(tabId)) { 1584 throw new ExtensionError( 1585 "Value of tabId must not occur in tabIds if append or insert is true" 1586 ); 1587 } 1588 1589 const referenceTab = tabTracker.getTab(tabId, null); 1590 let referenceWindow = referenceTab && referenceTab.ownerGlobal; 1591 if (referenceWindow && !context.canAccessWindow(referenceWindow)) { 1592 throw new ExtensionError(`Invalid tab ID: ${tabId}`); 1593 } 1594 let previousTab, lastSuccessor; 1595 if (append) { 1596 previousTab = referenceTab; 1597 lastSuccessor = 1598 (insert && referenceTab && referenceTab.successor) || null; 1599 } else { 1600 lastSuccessor = referenceTab; 1601 } 1602 1603 let firstTab; 1604 for (const tabId of tabIds) { 1605 const tab = tabTracker.getTab(tabId, null); 1606 if (tab === null) { 1607 continue; 1608 } 1609 if (!tabManager.canAccessTab(tab)) { 1610 throw new ExtensionError(`Invalid tab ID: ${tabId}`); 1611 } 1612 if (referenceWindow === null) { 1613 referenceWindow = tab.ownerGlobal; 1614 } else if (tab.ownerGlobal !== referenceWindow) { 1615 continue; 1616 } 1617 referenceWindow.gBrowser.replaceInSuccession(tab, tab.successor); 1618 if (append && tab === lastSuccessor) { 1619 lastSuccessor = tab.successor; 1620 } 1621 if (previousTab) { 1622 referenceWindow.gBrowser.setSuccessor(previousTab, tab); 1623 } else { 1624 firstTab = tab; 1625 } 1626 previousTab = tab; 1627 } 1628 1629 if (previousTab) { 1630 if (!append && insert && lastSuccessor !== null) { 1631 referenceWindow.gBrowser.replaceInSuccession( 1632 lastSuccessor, 1633 firstTab 1634 ); 1635 } 1636 referenceWindow.gBrowser.setSuccessor(previousTab, lastSuccessor); 1637 } 1638 }, 1639 1640 show(tabIds) { 1641 for (let tab of getNativeTabsFromIDArray(tabIds)) { 1642 if (tab.ownerGlobal) { 1643 tab.ownerGlobal.gBrowser.showTab(tab); 1644 } 1645 } 1646 }, 1647 1648 hide(tabIds) { 1649 let hidden = []; 1650 for (let tab of getNativeTabsFromIDArray(tabIds)) { 1651 if (tab.ownerGlobal && !tab.hidden) { 1652 tab.ownerGlobal.gBrowser.hideTab(tab, extension.id); 1653 if (tab.hidden) { 1654 hidden.push(tabTracker.getId(tab)); 1655 } 1656 } 1657 } 1658 if (hidden.length) { 1659 let win = Services.wm.getMostRecentWindow("navigator:browser"); 1660 1661 // Before showing the hidden tabs warning, 1662 // move alltabs-button to somewhere visible if it isn't already. 1663 if (!CustomizableUI.widgetIsLikelyVisible("alltabs-button", win)) { 1664 CustomizableUI.addWidgetToArea( 1665 "alltabs-button", 1666 CustomizableUI.verticalTabsEnabled 1667 ? CustomizableUI.AREA_NAVBAR 1668 : CustomizableUI.AREA_TABSTRIP 1669 ); 1670 } 1671 tabHidePopup.open(win, extension.id); 1672 } 1673 return hidden; 1674 }, 1675 1676 highlight(highlightInfo) { 1677 let { windowId, tabs, populate } = highlightInfo; 1678 if (windowId == null) { 1679 windowId = Window.WINDOW_ID_CURRENT; 1680 } 1681 let window = windowTracker.getWindow(windowId, context); 1682 if (!context.canAccessWindow(window)) { 1683 throw new ExtensionError(`Invalid window ID: ${windowId}`); 1684 } 1685 1686 if (!Array.isArray(tabs)) { 1687 tabs = [tabs]; 1688 } else if (!tabs.length) { 1689 throw new ExtensionError("No highlighted tab."); 1690 } 1691 window.gBrowser.selectedTabs = tabs.map(tabIndex => { 1692 let tab = window.gBrowser.tabs[tabIndex]; 1693 if (!tab || !tabManager.canAccessTab(tab)) { 1694 throw new ExtensionError("No tab at index: " + tabIndex); 1695 } 1696 return tab; 1697 }); 1698 return windowManager.convert(window, { populate }); 1699 }, 1700 1701 goForward(tabId) { 1702 let nativeTab = getTabOrActive(tabId); 1703 nativeTab.linkedBrowser.goForward(false); 1704 }, 1705 1706 goBack(tabId) { 1707 let nativeTab = getTabOrActive(tabId); 1708 nativeTab.linkedBrowser.goBack(false); 1709 }, 1710 1711 group(options) { 1712 let nativeTabs = getNativeTabsFromIDArray(options.tabIds); 1713 let window = windowTracker.getWindow( 1714 options.createProperties?.windowId ?? Window.WINDOW_ID_CURRENT, 1715 context 1716 ); 1717 const windowIsPrivate = PrivateBrowsingUtils.isWindowPrivate(window); 1718 for (const nativeTab of nativeTabs) { 1719 if ( 1720 PrivateBrowsingUtils.isWindowPrivate(nativeTab.ownerGlobal) !== 1721 windowIsPrivate 1722 ) { 1723 if (windowIsPrivate) { 1724 throw new ExtensionError( 1725 "Cannot move non-private tabs to private window" 1726 ); 1727 } 1728 throw new ExtensionError( 1729 "Cannot move private tabs to non-private window" 1730 ); 1731 } 1732 } 1733 function unpinTabsBeforeGrouping() { 1734 for (const nativeTab of nativeTabs) { 1735 nativeTab.ownerGlobal.gBrowser.unpinTab(nativeTab); 1736 } 1737 } 1738 let group; 1739 if (options.groupId == null) { 1740 // By default, tabs are appended after all other tabs in the 1741 // window. But if we are grouping tabs within a window, ideally the 1742 // tabs should just be grouped without moving positions. 1743 // TODO bug 1939214: when addTabGroup inserts tabs at the front as 1744 // needed (instead of always appending), simplify this logic. 1745 const tabInWin = nativeTabs.find(t => t.ownerGlobal === window); 1746 let insertBefore = tabInWin; 1747 if (tabInWin?.group) { 1748 if (tabInWin.group.tabs[0] === tabInWin) { 1749 // When tabInWin is at the front of a tab group, insert before 1750 // the tab group (instead of after it). 1751 insertBefore = tabInWin.group; 1752 } else { 1753 insertBefore = insertBefore.group.nextElementSibling; 1754 } 1755 } 1756 unpinTabsBeforeGrouping(); 1757 group = window.gBrowser.addTabGroup(nativeTabs, { insertBefore }); 1758 // Note: group is never null, because the only condition for which 1759 // it could be null is when all tabs are pinned, and we are already 1760 // explicitly unpinning them before moving. 1761 } else { 1762 group = window.gBrowser.getTabGroupById( 1763 getInternalTabGroupIdForExtTabGroupId(options.groupId) 1764 ); 1765 if (!group) { 1766 throw new ExtensionError(`No group with id: ${options.groupId}`); 1767 } 1768 unpinTabsBeforeGrouping(); 1769 // When moving tabs within the same window, try to maintain their 1770 // relative positions. 1771 const tabsBefore = []; 1772 const tabsAfter = []; 1773 const firstTabInGroup = group.tabs[0]; 1774 for (const nativeTab of nativeTabs) { 1775 if ( 1776 nativeTab.ownerGlobal === window && 1777 nativeTab._tPos < firstTabInGroup._tPos 1778 ) { 1779 tabsBefore.push(nativeTab); 1780 } else { 1781 tabsAfter.push(nativeTab); 1782 } 1783 } 1784 if (tabsBefore.length) { 1785 window.gBrowser.moveTabsBefore(tabsBefore, firstTabInGroup); 1786 } 1787 if (tabsAfter.length) { 1788 group.addTabs(tabsAfter); 1789 } 1790 } 1791 return getExtTabGroupIdForInternalTabGroupId(group.id); 1792 }, 1793 1794 ungroup(tabIds) { 1795 const nativeTabs = getNativeTabsFromIDArray(tabIds); 1796 // Ungroup tabs while trying to preserve the relative order of tabs 1797 // within the tab strip as much as possible. This is not always 1798 // possible, e.g. when a tab group is only partially ungrouped. 1799 const ungroupOrder = new DefaultMap(() => []); 1800 for (const nativeTab of nativeTabs) { 1801 if (nativeTab.group) { 1802 ungroupOrder.get(nativeTab.group).push(nativeTab); 1803 } 1804 } 1805 for (const [group, tabs] of ungroupOrder) { 1806 // Preserve original order of ungrouped tabs. 1807 tabs.sort((a, b) => a._tPos - b._tPos); 1808 if (tabs[0] === tabs[0].group.tabs[0]) { 1809 // The tab is the front of the tab group, so insert before 1810 // current tab group to preserve order. 1811 tabs[0].ownerGlobal.gBrowser.moveTabsBefore(tabs, group); 1812 } else { 1813 tabs[0].ownerGlobal.gBrowser.moveTabsAfter(tabs, group); 1814 } 1815 } 1816 }, 1817 }, 1818 }; 1819 return tabsApi; 1820 } 1821 };