URILoadingHelper.sys.mjs (38080B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs"; 7 import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; 8 9 const lazy = {}; 10 11 ChromeUtils.defineESModuleGetters(lazy, { 12 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs", 13 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 14 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 15 TorConnect: "resource://gre/modules/TorConnect.sys.mjs", 16 TorConnectParent: "resource://gre/actors/TorConnectParent.sys.mjs", 17 }); 18 19 ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () => 20 Components.Constructor( 21 "@mozilla.org/referrer-info;1", 22 "nsIReferrerInfo", 23 "init" 24 ) 25 ); 26 27 function saveLink(window, url, params) { 28 if ("isContentWindowPrivate" in params) { 29 window.saveURL( 30 url, 31 null, 32 null, 33 null, 34 true, 35 true, 36 params.referrerInfo, 37 null, 38 null, 39 params.isContentWindowPrivate, 40 params.originPrincipal 41 ); 42 } else { 43 if (!params.initiatingDoc) { 44 console.error( 45 "openUILink/openLinkIn was called with " + 46 "where == 'save' but without initiatingDoc. See bug 814264." 47 ); 48 return; 49 } 50 window.saveURL( 51 url, 52 null, 53 null, 54 null, 55 true, 56 true, 57 params.referrerInfo, 58 null, 59 params.initiatingDoc 60 ); 61 } 62 } 63 64 function openInWindow(url, params, sourceWindow) { 65 let { 66 referrerInfo, 67 forceNonPrivate, 68 triggeringRemoteType, 69 forceAllowDataURI, 70 globalHistoryOptions, 71 allowThirdPartyFixup, 72 userContextId, 73 postData, 74 originPrincipal, 75 originStoragePrincipal, 76 triggeringPrincipal, 77 policyContainer, 78 resolveOnContentBrowserCreated, 79 chromeless, 80 } = params; 81 const CHROMELESS_FEATURES = `resizable,minimizable,titlebar,close`; 82 let features = `chrome,dialog=no,${chromeless ? CHROMELESS_FEATURES : "all"}`; 83 if (params.private) { 84 features += ",private"; 85 // To prevent regular browsing data from leaking to private browsing sites, 86 // strip the referrer when opening a new private window. (See Bug: 1409226) 87 referrerInfo = new lazy.ReferrerInfo( 88 referrerInfo.referrerPolicy, 89 false, 90 referrerInfo.originalReferrer 91 ); 92 } else if (forceNonPrivate) { 93 features += ",non-private"; 94 } 95 96 // This propagates to window.arguments. 97 var sa = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 98 99 var wuri = Cc["@mozilla.org/supports-string;1"].createInstance( 100 Ci.nsISupportsString 101 ); 102 wuri.data = url; 103 104 let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( 105 Ci.nsIWritablePropertyBag2 106 ); 107 if (triggeringRemoteType) { 108 extraOptions.setPropertyAsACString( 109 "triggeringRemoteType", 110 triggeringRemoteType 111 ); 112 } 113 if (params.hasValidUserGestureActivation !== undefined) { 114 extraOptions.setPropertyAsBool( 115 "hasValidUserGestureActivation", 116 params.hasValidUserGestureActivation 117 ); 118 } 119 if (params.textDirectiveUserActivation !== undefined) { 120 extraOptions.setPropertyAsBool( 121 "textDirectiveUserActivation", 122 params.textDirectiveUserActivation 123 ); 124 } 125 if (forceAllowDataURI) { 126 extraOptions.setPropertyAsBool("forceAllowDataURI", true); 127 } 128 if (params.fromExternal !== undefined) { 129 extraOptions.setPropertyAsBool("fromExternal", params.fromExternal); 130 } 131 if (globalHistoryOptions?.triggeringSponsoredURL) { 132 extraOptions.setPropertyAsACString( 133 "triggeringSponsoredURL", 134 globalHistoryOptions.triggeringSponsoredURL 135 ); 136 if (globalHistoryOptions.triggeringSponsoredURLVisitTimeMS) { 137 extraOptions.setPropertyAsUint64( 138 "triggeringSponsoredURLVisitTimeMS", 139 globalHistoryOptions.triggeringSponsoredURLVisitTimeMS 140 ); 141 } 142 if (globalHistoryOptions.triggeringSource) { 143 extraOptions.setPropertyAsACString( 144 "triggeringSource", 145 globalHistoryOptions.triggeringSource 146 ); 147 } 148 } 149 if (params.schemelessInput !== undefined) { 150 extraOptions.setPropertyAsUint32("schemelessInput", params.schemelessInput); 151 } 152 153 var allowThirdPartyFixupSupports = Cc[ 154 "@mozilla.org/supports-PRBool;1" 155 ].createInstance(Ci.nsISupportsPRBool); 156 allowThirdPartyFixupSupports.data = allowThirdPartyFixup; 157 158 var userContextIdSupports = Cc[ 159 "@mozilla.org/supports-PRUint32;1" 160 ].createInstance(Ci.nsISupportsPRUint32); 161 userContextIdSupports.data = userContextId; 162 163 sa.appendElement(wuri); 164 sa.appendElement(extraOptions); 165 sa.appendElement(referrerInfo); 166 sa.appendElement(postData); 167 sa.appendElement(allowThirdPartyFixupSupports); 168 sa.appendElement(userContextIdSupports); 169 sa.appendElement(originPrincipal); 170 sa.appendElement(originStoragePrincipal); 171 sa.appendElement(triggeringPrincipal); 172 sa.appendElement(null); // allowInheritPrincipal 173 sa.appendElement(policyContainer); 174 175 let win; 176 177 // Returns a promise that will be resolved when the new window's startup is finished. 178 function waitForWindowStartup() { 179 return new Promise(resolve => { 180 const delayedStartupObserver = aSubject => { 181 if (aSubject == win) { 182 Services.obs.removeObserver( 183 delayedStartupObserver, 184 "browser-delayed-startup-finished" 185 ); 186 resolve(); 187 } 188 }; 189 Services.obs.addObserver( 190 delayedStartupObserver, 191 "browser-delayed-startup-finished" 192 ); 193 }); 194 } 195 196 if (params.frameID != undefined && sourceWindow) { 197 // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget 198 // event if it contains the expected frameID params. 199 // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is 200 // opening a new window using the keyboard shortcut). 201 const sourceTabBrowser = sourceWindow.gBrowser.selectedBrowser; 202 waitForWindowStartup().then(() => { 203 Services.obs.notifyObservers( 204 { 205 wrappedJSObject: { 206 url, 207 createdTabBrowser: win.gBrowser.selectedBrowser, 208 sourceTabBrowser, 209 sourceFrameID: params.frameID, 210 }, 211 }, 212 "webNavigation-createdNavigationTarget" 213 ); 214 }); 215 } 216 217 if (resolveOnContentBrowserCreated) { 218 waitForWindowStartup().then(() => 219 resolveOnContentBrowserCreated(win.gBrowser.selectedBrowser) 220 ); 221 } 222 223 win = Services.ww.openWindow( 224 sourceWindow, 225 AppConstants.BROWSER_CHROME_URL, 226 null, 227 features, 228 sa 229 ); 230 } 231 232 function openInCurrentTab(targetBrowser, url, uriObj, params) { 233 let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; 234 235 if (params.allowThirdPartyFixup) { 236 loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; 237 loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; 238 } 239 // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL isn't supported for javascript URIs, 240 // i.e. it causes them not to load at all. Callers should strip 241 // "javascript:" from pasted strings to prevent blank tabs 242 if (!params.allowInheritPrincipal) { 243 loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; 244 } 245 246 if (params.allowPopups) { 247 loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS; 248 } 249 if (params.indicateErrorPageLoad) { 250 loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV; 251 } 252 if (params.forceAllowDataURI) { 253 loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI; 254 } 255 if (params.fromExternal) { 256 loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; 257 } 258 259 let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler; 260 if ( 261 params.forceAboutBlankViewerInCurrent && 262 (!uriObj || 263 Services.io.getDynamicProtocolFlags(uriObj) & 264 URI_INHERITS_SECURITY_CONTEXT) 265 ) { 266 // Unless we know for sure we're not inheriting principals, 267 // force the about:blank viewer to have the right principal: 268 targetBrowser.createAboutBlankDocumentViewer( 269 params.originPrincipal, 270 params.originStoragePrincipal 271 ); 272 } 273 274 let { 275 triggeringPrincipal, 276 policyContainer, 277 referrerInfo, 278 postData, 279 userContextId, 280 hasValidUserGestureActivation, 281 textDirectiveUserActivation, 282 globalHistoryOptions, 283 triggeringRemoteType, 284 schemelessInput, 285 } = params; 286 287 targetBrowser.fixupAndLoadURIString(url, { 288 triggeringPrincipal, 289 policyContainer, 290 loadFlags, 291 referrerInfo, 292 postData, 293 userContextId, 294 hasValidUserGestureActivation, 295 textDirectiveUserActivation, 296 globalHistoryOptions, 297 triggeringRemoteType, 298 schemelessInput, 299 }); 300 301 params.resolveOnContentBrowserCreated?.(targetBrowser); 302 } 303 304 function updatePrincipals(window, params) { 305 let { userContextId } = params; 306 // Teach the principal about the right OA to use, e.g. in case when 307 // opening a link in a new private window, or in a new container tab. 308 // Please note we do not have to do that for SystemPrincipals and we 309 // can not do it for NullPrincipals since NullPrincipals are only 310 // identical if they actually are the same object (See Bug: 1346759) 311 function useOAForPrincipal(principal) { 312 if (principal && principal.isContentPrincipal) { 313 let privateBrowsingId = 314 params.private || 315 (window && PrivateBrowsingUtils.isWindowPrivate(window)); 316 let attrs = { 317 userContextId, 318 privateBrowsingId, 319 firstPartyDomain: principal.originAttributes.firstPartyDomain, 320 }; 321 return Services.scriptSecurityManager.principalWithOA(principal, attrs); 322 } 323 return principal; 324 } 325 params.originPrincipal = useOAForPrincipal(params.originPrincipal); 326 params.originStoragePrincipal = useOAForPrincipal( 327 params.originStoragePrincipal 328 ); 329 params.triggeringPrincipal = useOAForPrincipal(params.triggeringPrincipal); 330 } 331 332 /* Creates a null principal using the userContextId 333 from the current selected tab or a passed in tab argument */ 334 function _createNullPrincipalFromTabUserContextId(tab = null) { 335 const window = lazy.BrowserWindowTracker.getTopWindow(); 336 if (!tab) { 337 tab = window.gBrowser.selectedTab; 338 } 339 340 let userContextId; 341 if (tab.hasAttribute("usercontextid")) { 342 userContextId = tab.getAttribute("usercontextid"); 343 } 344 return Services.scriptSecurityManager.createNullPrincipal({ 345 userContextId, 346 }); 347 } 348 349 export const URILoadingHelper = { 350 /** 351 * openLinkIn opens a URL in a place specified by the parameter |where|. 352 * 353 * The params object is the same as for `openLinkIn` and documented below. 354 * 355 * @param {string} where 356 * |where| can be: 357 * "current" current tab (if there aren't any browser windows, then in a new window instead) 358 * "tab" new tab (if there aren't any browser windows, then in a new window instead) 359 * "tabshifted" same as "tab" but in background if default is to select new tabs, and vice versa 360 * "window" new window 361 * "chromeless" new minimal window (no browser navigation UI) 362 * "save" save to disk (with no filename hint!) 363 * 364 * @param {object} params 365 * 366 * Options relating to what tab/window to use and how to open it: 367 * 368 * @param {boolean} params.private 369 * Load the URL in a private window. 370 * @param {boolean} params.forceNonPrivate 371 * Force the load to happen in non-private windows. 372 * @param {boolean} params.relatedToCurrent 373 * Whether new tabs should go immediately next to the current tab. 374 * @param {Element} params.targetBrowser 375 * The browser to use for the load. Only used if where == "current". 376 * @param {boolean} params.inBackground 377 * If explicitly true or false, whether to switch to the tab immediately. 378 * If null, will switch to the tab if `forceForeground` was true. If 379 * neither is passed, will defer to the user preference browser.tabs.loadInBackground. 380 * @param {boolean} params.forceForeground 381 * Ignore the user preference and load in the foreground. 382 * @param {boolean} params.allowPinnedTabHostChange 383 * Allow even a pinned tab to change hosts. 384 * @param {boolean} params.allowPopups 385 * whether the link is allowed to open in a popup window (ie one with no browser 386 * chrome) 387 * @param {boolean} params.skipTabAnimation 388 * Skip the tab opening animation. 389 * @param {Element} params.openerBrowser 390 * The browser that started the load. 391 * @param {boolean} params.avoidBrowserFocus 392 * Don't focus the browser element immediately after starting 393 * the load. Used by the URL bar to avoid leaking user input 394 * into web content, see bug 1641287. 395 * 396 * Options relating to the load itself: 397 * 398 * @param {boolean} params.allowThirdPartyFixup 399 * Allow transforming the 'url' into a search query. 400 * @param {nsIInputStream} params.postData 401 * Data to post as part of the request. 402 * @param {nsIReferrerInfo} params.referrerInfo 403 * Referrer info for the request. 404 * @param {boolean} params.indicateErrorPageLoad 405 * Whether docshell should throw an exception (i.e. return non-NS_OK) 406 * if the load fails. 407 * @param {string} params.charset 408 * Character set to use for the load. Only honoured for tabs. 409 * Legacy argument - do not use. 410 * @param {SchemelessInputType} params.schemelessInput 411 * Whether the search/URL term was without an explicit scheme. 412 * 413 * Options relating to security, whether the load is allowed to happen, 414 * and what cookie container to use for the load: 415 * 416 * @param {boolean} params.forceAllowDataURI 417 * Force allow a data URI to load as a toplevel load. 418 * @param {number} params.userContextId 419 * The userContextId (container identifier) to use for the load. 420 * @param {boolean} params.allowInheritPrincipal 421 * Allow the load to inherit the triggering principal. 422 * @param {boolean} params.forceAboutBlankViewerInCurrent 423 * Force load an about:blank page first. Only used if 424 * allowInheritPrincipal is passed or no URL was provided. 425 * @param {nsIPrincipal} params.triggeringPrincipal 426 * Triggering principal to pass to docshell for the load. 427 * @param {nsIPrincipal} params.originPrincipal 428 * Origin principal to pass to docshell for the load. 429 * @param {nsIPrincipal} params.originStoragePrincipal 430 * Storage principal to pass to docshell for the load. 431 * @param {string} params.triggeringRemoteType 432 * The remoteType triggering this load. 433 * @param {nsIPolicyContainer} params.policyContainer 434 * The policyContainer that should apply to the load. 435 * @param {boolean} params.hasValidUserGestureActivation 436 * Indicates if a valid user gesture caused this load. This 437 * informs e.g. popup blocker decisions. 438 * @param {boolean} params.fromExternal 439 * Indicates the load was started outside of the browser, 440 * e.g. passed on the commandline or through OS mechanisms. 441 * 442 * Options used to track the load elsewhere 443 * 444 * @param {function} params.resolveOnNewTabCreated 445 * This callback will be called when a new tab is created. 446 * @param {function} params.resolveOnContentBrowserCreated 447 * This callback will be called with the content browser once it's created. 448 * @param {object} params.globalHistoryOptions 449 * Used by places to keep track of search related metadata for loads. 450 * @param {number} params.frameID 451 * Used by webextensions for their loads. 452 * 453 * Options used for where="save" only: 454 * 455 * @param {boolean} params.isContentWindowPrivate 456 * Save content as coming from a private window. 457 * @param {Document} params.initiatingDoc 458 * Used to determine where to prompt for a filename. 459 */ 460 openLinkIn(window, url, where, params) { 461 if (!where || !url) { 462 return; 463 } 464 465 // make sure users are not faced with the scary red 'tor isn't working' screen 466 // if they navigate to about:tor before bootstrapped 467 // 468 // fixes tor-browser#40752 469 // new tabs also redirect to about:tor if browser.newtabpage.enabled is true 470 // otherwise they go to about:blank 471 if (lazy.TorConnect.shouldShowTorConnect) { 472 const homeURLs = [ 473 "about:home", 474 "about:privatebrowsing", 475 "about:tor", 476 "about:welcome", 477 ]; 478 if ( 479 homeURLs.includes(url) || 480 (url === "about:newtab" && 481 Services.prefs.getBoolPref("browser.newtabpage.enabled", false)) 482 ) { 483 url = lazy.TorConnectParent.getRedirectURL(url); 484 } 485 } 486 487 let { 488 allowThirdPartyFixup, 489 postData, 490 charset, 491 allowInheritPrincipal, 492 forceAllowDataURI, 493 forceNonPrivate, 494 skipTabAnimation, 495 allowPinnedTabHostChange, 496 userContextId, 497 triggeringPrincipal, 498 originPrincipal, 499 originStoragePrincipal, 500 triggeringRemoteType, 501 policyContainer, 502 resolveOnNewTabCreated, 503 resolveOnContentBrowserCreated, 504 globalHistoryOptions, 505 hasValidUserGestureActivation, 506 textDirectiveUserActivation, 507 } = params; 508 509 // We want to overwrite some things for convenience when passing it to other 510 // methods. To avoid impacting callers, copy the params. 511 params = Object.assign({}, params); 512 513 if (!params.referrerInfo) { 514 params.referrerInfo = new lazy.ReferrerInfo( 515 Ci.nsIReferrerInfo.EMPTY, 516 true, 517 null 518 ); 519 } 520 521 if (!triggeringPrincipal) { 522 throw new Error("Must load with a triggering Principal"); 523 } 524 525 if (where == "save") { 526 saveLink(window, url, params); 527 return; 528 } 529 530 // Establish which window we'll load the link in. 531 let w = this._resolveInitialTargetWindow( 532 where, 533 params, 534 window, 535 forceNonPrivate 536 ); 537 538 updatePrincipals(w, params); 539 540 if (where == "chromeless") { 541 params.chromeless = true; 542 where = "window"; 543 } 544 545 if (!w || where == "window") { 546 openInWindow(url, params, w || window); 547 return; 548 } 549 550 // We're now committed to loading the link in an existing browser window. 551 552 // Raise the target window before loading the URI, since loading it may 553 // result in a new frontmost window (e.g. "javascript:window.open('');"). 554 w.focus(); 555 556 let targetBrowser; 557 let loadInBackground; 558 let uriObj; 559 560 if (where == "current") { 561 targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser; 562 loadInBackground = false; 563 uriObj = URL.parse(url)?.URI; 564 565 // In certain tabs, we restrict what if anything may replace the loaded 566 // page. If a load request bounces off for the currently selected tab, 567 // we'll open a new tab instead. 568 let tab = w.gBrowser.getTabForBrowser(targetBrowser); 569 if (tab == w.FirefoxViewHandler.tab) { 570 where = "tab"; 571 targetBrowser = null; 572 } else if ( 573 !allowPinnedTabHostChange && 574 tab.pinned && 575 url != "about:crashcontent" 576 ) { 577 try { 578 // nsIURI.host can throw for non-nsStandardURL nsIURIs. 579 if ( 580 !uriObj || 581 (!uriObj.schemeIs("javascript") && 582 targetBrowser.currentURI.host != uriObj.host) 583 ) { 584 where = "tab"; 585 targetBrowser = null; 586 } 587 } catch (err) { 588 where = "tab"; 589 targetBrowser = null; 590 } 591 } 592 } else { 593 // `where` is "tab" or "tabshifted", so we'll load the link in a new tab. 594 loadInBackground = params.inBackground; 595 if (loadInBackground == null) { 596 loadInBackground = params.forceForeground 597 ? false 598 : Services.prefs.getBoolPref("browser.tabs.loadInBackground"); 599 } 600 } 601 602 let focusUrlBar = false; 603 604 switch (where) { 605 case "current": 606 openInCurrentTab(targetBrowser, url, uriObj, params); 607 608 // Don't focus the content area if focus is in the address bar and we're 609 // loading the New Tab page. 610 focusUrlBar = 611 w.document.activeElement == w.gURLBar.inputField && 612 w.isBlankPageURL(url); 613 break; 614 case "tabshifted": 615 loadInBackground = !loadInBackground; 616 // fall through 617 case "tab": { 618 focusUrlBar = 619 !loadInBackground && 620 w.isBlankPageURL(url) && 621 !lazy.AboutNewTab.willNotifyUser; 622 623 let tabUsedForLoad = w.gBrowser.addTab(url, { 624 referrerInfo: params.referrerInfo, 625 charset, 626 postData, 627 inBackground: loadInBackground, 628 allowThirdPartyFixup, 629 relatedToCurrent: params.relatedToCurrent, 630 skipAnimation: skipTabAnimation, 631 userContextId, 632 originPrincipal, 633 originStoragePrincipal, 634 triggeringPrincipal, 635 allowInheritPrincipal, 636 triggeringRemoteType, 637 policyContainer, 638 forceAllowDataURI, 639 focusUrlBar, 640 openerBrowser: params.openerBrowser, 641 fromExternal: params.fromExternal, 642 globalHistoryOptions, 643 schemelessInput: params.schemelessInput, 644 hasValidUserGestureActivation, 645 textDirectiveUserActivation, 646 }); 647 targetBrowser = tabUsedForLoad.linkedBrowser; 648 649 resolveOnNewTabCreated?.(targetBrowser); 650 resolveOnContentBrowserCreated?.(targetBrowser); 651 652 if (params.frameID != undefined && w) { 653 // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget 654 // event if it contains the expected frameID params. 655 // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is 656 // opening a new tab using the keyboard shortcut). 657 Services.obs.notifyObservers( 658 { 659 wrappedJSObject: { 660 url, 661 createdTabBrowser: targetBrowser, 662 sourceTabBrowser: w.gBrowser.selectedBrowser, 663 sourceFrameID: params.frameID, 664 }, 665 }, 666 "webNavigation-createdNavigationTarget" 667 ); 668 } 669 break; 670 } 671 } 672 673 // Potentially trigger a URL bar telemetry bounce event when navigating 674 // away from a page using the browser chrome. 675 // We avoid triggering for URL bar initiated loads since this gets called 676 // right after a result is picked and the bounce event tracking is started. 677 // We instead check for potential URL bar initiated bounce events directly 678 // in gURLBar.controller.engagementEvent.startTrackingBounceEvent(). 679 if (!params.initiatedByURLBar && targetBrowser) { 680 w.gURLBar.controller.engagementEvent.handleBounceEventTrigger( 681 targetBrowser 682 ); 683 } 684 685 if ( 686 !params.avoidBrowserFocus && 687 !focusUrlBar && 688 targetBrowser == w.gBrowser.selectedBrowser 689 ) { 690 // Focus the content, but only if the browser used for the load is selected. 691 targetBrowser.focus(); 692 } 693 }, 694 /** 695 * Resolve the initial browser window to use for a load, based on `where`. 696 * 697 * @param {string} where 698 * The target location for the load (e.g. "current", "tab", "window"). 699 * @param {object} params 700 * The full params object passed to openLinkIn. 701 * @param {Window} win 702 * The reference window used as a fallback for getTargetWindow. 703 * @param {boolean} forceNonPrivate 704 * Whether to force choosing a non-private target window. 705 * @returns {Window} 706 * The browser window that should be used as the initial target. 707 */ 708 _resolveInitialTargetWindow(where, params, win, forceNonPrivate) { 709 if (where === "current" && params.targetBrowser) { 710 return params.targetBrowser.ownerGlobal; 711 } 712 713 if (where === "tab" || where === "tabshifted") { 714 const target = this.getTargetWindow(win, { 715 skipPopups: true, 716 skipTaskbarTabs: true, 717 forceNonPrivate, 718 }); 719 if (win.top !== target) { 720 params.relatedToCurrent = false; 721 } 722 return target; 723 } 724 return this.getTargetWindow(win, { forceNonPrivate }); 725 }, 726 /** 727 * Finds a browser window suitable for opening a link matching the 728 * requirements given in the `params` argument. If the current window matches 729 * the requirements then it is returned otherwise the top-most window that 730 * matches will be returned. 731 * 732 * @param {Window} window - The current window. 733 * @param {object} params - Parameters for selecting the window. 734 * @param {boolean} params.skipPopups - Require a non-popup window. 735 * @param {boolean} params.skipTaskbarTabs - Require a non-taskbartab window. 736 * @param {boolean} params.forceNonPrivate - Require a non-private window. 737 * @returns {Window | null} A matching browser window or null if none matched. 738 */ 739 getTargetWindow( 740 window, 741 { skipPopups, skipTaskbarTabs, forceNonPrivate } = {} 742 ) { 743 let { top } = window; 744 // If this is called in a browser window, use that window regardless of 745 // whether it's the frontmost window, since commands can be executed in 746 // background windows (bug 626148). 747 if ( 748 top.document.documentElement.getAttribute("windowtype") == 749 "navigator:browser" && 750 (!skipPopups || top.toolbar.visible) && 751 (!skipTaskbarTabs || 752 !top.document.documentElement.hasAttribute("taskbartab")) && 753 (!forceNonPrivate || !PrivateBrowsingUtils.isWindowPrivate(top)) 754 ) { 755 return top; 756 } 757 758 return lazy.BrowserWindowTracker.getTopWindow({ 759 private: !forceNonPrivate && PrivateBrowsingUtils.isWindowPrivate(window), 760 allowPopups: !skipPopups, 761 allowTaskbarTabs: !skipTaskbarTabs, 762 }); 763 }, 764 765 /** 766 * openUILink handles clicks on UI elements that cause URLs to load. 767 * 768 * @param {string} url 769 * @param {Event | object} event Event or JSON object representing an Event 770 * @param {boolean | object} aIgnoreButton 771 * Boolean or object with the same properties as 772 * accepted by openLinkIn, plus "ignoreButton" 773 * and "ignoreAlt". 774 * @param {boolean} aIgnoreAlt 775 * @param {boolean} aAllowThirdPartyFixup 776 * @param {object} aPostData 777 * @param {object} aReferrerInfo 778 */ 779 openUILink( 780 window, 781 url, 782 event, 783 aIgnoreButton, 784 aIgnoreAlt, 785 aAllowThirdPartyFixup, 786 aPostData, 787 aReferrerInfo 788 ) { 789 event = BrowserUtils.getRootEvent(event); 790 let params; 791 792 if (aIgnoreButton && typeof aIgnoreButton == "object") { 793 params = aIgnoreButton; 794 795 // don't forward "ignoreButton" and "ignoreAlt" to openLinkIn 796 aIgnoreButton = params.ignoreButton; 797 aIgnoreAlt = params.ignoreAlt; 798 delete params.ignoreButton; 799 delete params.ignoreAlt; 800 } else { 801 params = { 802 allowThirdPartyFixup: aAllowThirdPartyFixup, 803 postData: aPostData, 804 referrerInfo: aReferrerInfo, 805 initiatingDoc: event ? event.target.ownerDocument : null, 806 }; 807 } 808 809 if (!params.triggeringPrincipal) { 810 throw new Error( 811 "Required argument triggeringPrincipal missing within openUILink" 812 ); 813 } 814 815 let where = BrowserUtils.whereToOpenLink(event, aIgnoreButton, aIgnoreAlt); 816 params.forceForeground ??= true; 817 this.openLinkIn(window, url, where, params); 818 }, 819 820 /* openTrustedLinkIn will attempt to open the given URI using the SystemPrincipal 821 * as the trigeringPrincipal, unless a more specific Principal is provided. 822 * 823 * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground` 824 * to true. 825 */ 826 openTrustedLinkIn(window, url, where, params = {}) { 827 if (!params.triggeringPrincipal) { 828 params.triggeringPrincipal = 829 Services.scriptSecurityManager.getSystemPrincipal(); 830 } 831 832 params.forceForeground ??= true; 833 this.openLinkIn(window, url, where, params); 834 }, 835 836 /* openWebLinkIn will attempt to open the given URI using the NullPrincipal 837 * as the triggeringPrincipal, unless a more specific Principal is provided. 838 * 839 * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground` 840 * to true. 841 */ 842 openWebLinkIn(window, url, where, params = {}) { 843 if (!params.triggeringPrincipal) { 844 params.triggeringPrincipal = 845 Services.scriptSecurityManager.createNullPrincipal({}); 846 } 847 if (params.triggeringPrincipal.isSystemPrincipal) { 848 throw new Error( 849 "System principal should never be passed into openWebLinkIn()" 850 ); 851 } 852 params.forceForeground ??= true; 853 this.openLinkIn(window, url, where, params); 854 }, 855 856 /** 857 * Given a URI, guess which container to use to open it. This is used for external 858 * openers as a quality of life improvement (e.g. to open a document into the container 859 * where you are logged in to the service that hosts it). 860 * matches will be returned. 861 * For now this can only use currently-open tabs, until history is tagged with the 862 * container id (https://bugzilla.mozilla.org/show_bug.cgi?id=1283320). 863 * 864 * @param {nsIURI} aURI - The URI being opened. 865 * @returns {number | null} The guessed userContextId, or null if none. 866 */ 867 guessUserContextId(aURI) { 868 let host; 869 try { 870 host = aURI.host; 871 } catch (e) {} 872 if (!host) { 873 return null; 874 } 875 const containerScores = new Map(); 876 let guessedUserContextId = null; 877 let maxCount = 0; 878 for (let win of lazy.BrowserWindowTracker.orderedWindows) { 879 for (let tab of win.gBrowser.visibleTabs) { 880 let { userContextId } = tab; 881 let currentURIHost = null; 882 try { 883 currentURIHost = tab.linkedBrowser.currentURI.host; 884 } catch (e) {} 885 886 if (currentURIHost == host) { 887 let count = (containerScores.get(userContextId) ?? 0) + 1; 888 containerScores.set(userContextId, count); 889 if (count > maxCount) { 890 guessedUserContextId = userContextId; 891 maxCount = count; 892 } 893 } 894 } 895 } 896 897 return guessedUserContextId; 898 }, 899 /** 900 * Switch to a tab that has a given URI, and focuses its browser window. 901 * If a matching tab is in this window, it will be switched to. Otherwise, other 902 * windows will be searched. 903 * 904 * @param window 905 * The current window 906 * @param aURI 907 * URI to search for 908 * @param aOpenNew 909 * True to open a new tab and switch to it, if no existing tab is found. 910 * If no suitable window is found, a new one will be opened. 911 * @param aOpenParams 912 * If switching to this URI results in us opening a tab, aOpenParams 913 * will be the parameter object that gets passed to openTrustedLinkIn. Please 914 * see the documentation for openTrustedLinkIn to see what parameters can be 915 * passed via this object. 916 * This object also allows: 917 * - 'ignoreFragment' property to be set to true to exclude fragment-portion 918 * matching when comparing URIs. 919 * If set to "whenComparing", the fragment will be unmodified. 920 * If set to "whenComparingAndReplace", the fragment will be replaced. 921 * - 'ignoreQueryString' boolean property to be set to true to exclude query string 922 * matching when comparing URIs. 923 * - 'replaceQueryString' boolean property to be set to true to exclude query string 924 * matching when comparing URIs and overwrite the initial query string with 925 * the one from the new URI. 926 * - 'adoptIntoActiveWindow' boolean property to be set to true to adopt the tab 927 * into the current window. 928 * @param aUserContextId 929 * If not null, will switch to the first found tab having the provided 930 * userContextId. 931 * @return True if an existing tab was found, false otherwise 932 */ 933 switchToTabHavingURI( 934 window, 935 aURI, 936 aOpenNew, 937 aOpenParams = {}, 938 aUserContextId = null 939 ) { 940 // Certain URLs can be switched to irrespective of the source or destination 941 // window being in private browsing mode: 942 const kPrivateBrowsingURLs = new Set(["about:addons"]); 943 944 let ignoreFragment = aOpenParams.ignoreFragment; 945 let ignoreQueryString = aOpenParams.ignoreQueryString; 946 let replaceQueryString = aOpenParams.replaceQueryString; 947 let adoptIntoActiveWindow = aOpenParams.adoptIntoActiveWindow; 948 949 // These properties are only used by switchToTabHavingURI and should 950 // not be used as a parameter for the new load. 951 delete aOpenParams.ignoreFragment; 952 delete aOpenParams.ignoreQueryString; 953 delete aOpenParams.replaceQueryString; 954 delete aOpenParams.adoptIntoActiveWindow; 955 956 let isBrowserWindow = !!window.gBrowser; 957 958 // This will switch to the tab in aWindow having aURI, if present. 959 function switchIfURIInWindow(aWindow) { 960 // We can switch tab only if if both the source and destination windows have 961 // the same private-browsing status. 962 if ( 963 !kPrivateBrowsingURLs.has(aURI.spec) && 964 PrivateBrowsingUtils.isWindowPrivate(window) !== 965 PrivateBrowsingUtils.isWindowPrivate(aWindow) 966 ) { 967 return false; 968 } 969 970 // Remove the query string, fragment, both, or neither from a given url. 971 function cleanURL(url, removeQuery, removeFragment) { 972 let ret = url; 973 if (removeFragment) { 974 ret = ret.split("#")[0]; 975 if (removeQuery) { 976 // This removes a query, if present before the fragment. 977 ret = ret.split("?")[0]; 978 } 979 } else if (removeQuery) { 980 // This is needed in case there is a fragment after the query. 981 let fragment = ret.split("#")[1]; 982 ret = ret 983 .split("?")[0] 984 .concat(fragment != undefined ? "#".concat(fragment) : ""); 985 } 986 return ret; 987 } 988 989 // Need to handle nsSimpleURIs here too (e.g. about:...), which don't 990 // work correctly with URL objects - so treat them as strings 991 let ignoreFragmentWhenComparing = 992 typeof ignoreFragment == "string" && 993 ignoreFragment.startsWith("whenComparing"); 994 let requestedCompare = cleanURL( 995 aURI.displaySpec, 996 ignoreQueryString || replaceQueryString, 997 ignoreFragmentWhenComparing 998 ); 999 let browsers = aWindow.gBrowser.browsers; 1000 for (let i = 0; i < browsers.length; i++) { 1001 let browser = browsers[i]; 1002 let browserCompare = cleanURL( 1003 browser.currentURI.displaySpec, 1004 ignoreQueryString || replaceQueryString, 1005 ignoreFragmentWhenComparing 1006 ); 1007 let browserUserContextId = browser.getAttribute("usercontextid") || ""; 1008 if (aUserContextId != null && aUserContextId != browserUserContextId) { 1009 continue; 1010 } 1011 if (requestedCompare == browserCompare) { 1012 // If adoptIntoActiveWindow is set, and this is a cross-window switch, 1013 // adopt the tab into the current window, after the active tab. 1014 let doAdopt = 1015 adoptIntoActiveWindow && isBrowserWindow && aWindow != window; 1016 1017 if (doAdopt) { 1018 const newTab = window.gBrowser.adoptTab( 1019 aWindow.gBrowser.getTabForBrowser(browser), 1020 { 1021 tabIndex: window.gBrowser.tabContainer.selectedIndex + 1, 1022 selectTab: true, 1023 } 1024 ); 1025 if (!newTab) { 1026 doAdopt = false; 1027 } 1028 } 1029 if (!doAdopt) { 1030 aWindow.focus(); 1031 } 1032 1033 if ( 1034 ignoreFragment == "whenComparingAndReplace" || 1035 replaceQueryString 1036 ) { 1037 browser.loadURI(aURI, { 1038 triggeringPrincipal: 1039 aOpenParams.triggeringPrincipal || 1040 _createNullPrincipalFromTabUserContextId(), 1041 }); 1042 } 1043 1044 if (!doAdopt) { 1045 aWindow.gBrowser.tabContainer.selectedIndex = i; 1046 } 1047 1048 return true; 1049 } 1050 } 1051 return false; 1052 } 1053 1054 // This can be passed either nsIURI or a string. 1055 if (!(aURI instanceof Ci.nsIURI)) { 1056 aURI = Services.io.newURI(aURI); 1057 } 1058 1059 // Prioritise this window. 1060 if (isBrowserWindow && switchIfURIInWindow(window)) { 1061 return true; 1062 } 1063 1064 for (let browserWin of lazy.BrowserWindowTracker.orderedWindows) { 1065 // Skip closed (but not yet destroyed) windows, 1066 // and the current window (which was checked earlier). 1067 if (browserWin.closed || browserWin == window) { 1068 continue; 1069 } 1070 if (switchIfURIInWindow(browserWin)) { 1071 return true; 1072 } 1073 } 1074 1075 // No opened tab has that url. 1076 if (aOpenNew) { 1077 if ( 1078 lazy.UrlbarPrefs.get("switchTabs.searchAllContainers") && 1079 aUserContextId != null 1080 ) { 1081 aOpenParams.userContextId = aUserContextId; 1082 } 1083 if (isBrowserWindow && window.gBrowser.selectedTab.isEmpty) { 1084 this.openTrustedLinkIn(window, aURI.spec, "current", aOpenParams); 1085 } else { 1086 this.openTrustedLinkIn(window, aURI.spec, "tab", aOpenParams); 1087 } 1088 } 1089 1090 return false; 1091 }, 1092 };