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/. */
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";
11 ChromeUtils.defineESModuleGetters(lazy, {
12 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
13 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
16 ChromeUtils.defineLazyGetter(lazy, "ReferrerInfo", () =>
17 Components.Constructor(
18 "@mozilla.org/referrer-info;1",
24 function saveLink(window, url, params) {
25 if ("isContentWindowPrivate" in params) {
36 params.isContentWindowPrivate,
37 params.originPrincipal
40 if (!params.initiatingDoc) {
42 "openUILink/openLinkIn was called with " +
43 "where == 'save' but without initiatingDoc. See bug 814264."
61 function openInWindow(url, params, sourceWindow) {
72 originStoragePrincipal,
75 resolveOnContentBrowserCreated,
77 let features = "chrome,dialog=no,all";
79 features += ",private";
80 // To prevent regular browsing data from leaking to private browsing sites,
81 // strip the referrer when opening a new private window. (See Bug: 1409226)
82 referrerInfo = new lazy.ReferrerInfo(
83 referrerInfo.referrerPolicy,
85 referrerInfo.originalReferrer
87 } else if (forceNonPrivate) {
88 features += ",non-private";
91 // This propagates to window.arguments.
92 var sa = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
94 var wuri = Cc["@mozilla.org/supports-string;1"].createInstance(
99 let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
100 Ci.nsIWritablePropertyBag2
102 if (triggeringRemoteType) {
103 extraOptions.setPropertyAsACString(
104 "triggeringRemoteType",
108 if (params.hasValidUserGestureActivation !== undefined) {
109 extraOptions.setPropertyAsBool(
110 "hasValidUserGestureActivation",
111 params.hasValidUserGestureActivation
114 if (forceAllowDataURI) {
115 extraOptions.setPropertyAsBool("forceAllowDataURI", true);
117 if (params.fromExternal !== undefined) {
118 extraOptions.setPropertyAsBool("fromExternal", params.fromExternal);
120 if (globalHistoryOptions?.triggeringSponsoredURL) {
121 extraOptions.setPropertyAsACString(
122 "triggeringSponsoredURL",
123 globalHistoryOptions.triggeringSponsoredURL
125 if (globalHistoryOptions.triggeringSponsoredURLVisitTimeMS) {
126 extraOptions.setPropertyAsUint64(
127 "triggeringSponsoredURLVisitTimeMS",
128 globalHistoryOptions.triggeringSponsoredURLVisitTimeMS
132 if (params.wasSchemelessInput !== undefined) {
133 extraOptions.setPropertyAsBool(
134 "wasSchemelessInput",
135 params.wasSchemelessInput
139 var allowThirdPartyFixupSupports = Cc[
140 "@mozilla.org/supports-PRBool;1"
141 ].createInstance(Ci.nsISupportsPRBool);
142 allowThirdPartyFixupSupports.data = allowThirdPartyFixup;
144 var userContextIdSupports = Cc[
145 "@mozilla.org/supports-PRUint32;1"
146 ].createInstance(Ci.nsISupportsPRUint32);
147 userContextIdSupports.data = userContextId;
149 sa.appendElement(wuri);
150 sa.appendElement(extraOptions);
151 sa.appendElement(referrerInfo);
152 sa.appendElement(postData);
153 sa.appendElement(allowThirdPartyFixupSupports);
154 sa.appendElement(userContextIdSupports);
155 sa.appendElement(originPrincipal);
156 sa.appendElement(originStoragePrincipal);
157 sa.appendElement(triggeringPrincipal);
158 sa.appendElement(null); // allowInheritPrincipal
159 sa.appendElement(csp);
163 // Returns a promise that will be resolved when the new window's startup is finished.
164 function waitForWindowStartup() {
165 return new Promise(resolve => {
166 const delayedStartupObserver = aSubject => {
167 if (aSubject == win) {
168 Services.obs.removeObserver(
169 delayedStartupObserver,
170 "browser-delayed-startup-finished"
175 Services.obs.addObserver(
176 delayedStartupObserver,
177 "browser-delayed-startup-finished"
182 if (params.frameID != undefined && sourceWindow) {
183 // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
184 // event if it contains the expected frameID params.
185 // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
186 // opening a new window using the keyboard shortcut).
187 const sourceTabBrowser = sourceWindow.gBrowser.selectedBrowser;
188 waitForWindowStartup().then(() => {
189 Services.obs.notifyObservers(
193 createdTabBrowser: win.gBrowser.selectedBrowser,
195 sourceFrameID: params.frameID,
198 "webNavigation-createdNavigationTarget"
203 if (resolveOnContentBrowserCreated) {
204 waitForWindowStartup().then(() =>
205 resolveOnContentBrowserCreated(win.gBrowser.selectedBrowser)
209 win = Services.ww.openWindow(
211 AppConstants.BROWSER_CHROME_URL,
218 function openInCurrentTab(targetBrowser, url, uriObj, params) {
219 let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
221 if (params.allowThirdPartyFixup) {
222 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
223 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
225 // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL isn't supported for javascript URIs,
226 // i.e. it causes them not to load at all. Callers should strip
227 // "javascript:" from pasted strings to prevent blank tabs
228 if (!params.allowInheritPrincipal) {
229 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
232 if (params.allowPopups) {
233 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS;
235 if (params.indicateErrorPageLoad) {
236 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV;
238 if (params.forceAllowDataURI) {
239 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
241 if (params.fromExternal) {
242 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
245 let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
247 params.forceAboutBlankViewerInCurrent &&
249 Services.io.getDynamicProtocolFlags(uriObj) &
250 URI_INHERITS_SECURITY_CONTEXT)
252 // Unless we know for sure we're not inheriting principals,
253 // force the about:blank viewer to have the right principal:
254 targetBrowser.createAboutBlankDocumentViewer(
255 params.originPrincipal,
256 params.originStoragePrincipal
266 hasValidUserGestureActivation,
267 globalHistoryOptions,
268 triggeringRemoteType,
272 targetBrowser.fixupAndLoadURIString(url, {
279 hasValidUserGestureActivation,
280 globalHistoryOptions,
281 triggeringRemoteType,
284 params.resolveOnContentBrowserCreated?.(targetBrowser);
287 function updatePrincipals(window, params) {
288 let { userContextId } = params;
289 // Teach the principal about the right OA to use, e.g. in case when
290 // opening a link in a new private window, or in a new container tab.
291 // Please note we do not have to do that for SystemPrincipals and we
292 // can not do it for NullPrincipals since NullPrincipals are only
293 // identical if they actually are the same object (See Bug: 1346759)
294 function useOAForPrincipal(principal) {
295 if (principal && principal.isContentPrincipal) {
296 let privateBrowsingId =
298 (window && PrivateBrowsingUtils.isWindowPrivate(window));
302 firstPartyDomain: principal.originAttributes.firstPartyDomain,
304 return Services.scriptSecurityManager.principalWithOA(principal, attrs);
308 params.originPrincipal = useOAForPrincipal(params.originPrincipal);
309 params.originStoragePrincipal = useOAForPrincipal(
310 params.originStoragePrincipal
312 params.triggeringPrincipal = useOAForPrincipal(params.triggeringPrincipal);
315 export const URILoadingHelper = {
316 /* openLinkIn opens a URL in a place specified by the parameter |where|.
318 * The params object is the same as for `openLinkIn` and documented below.
320 * @param {String} where
322 * "current" current tab (if there aren't any browser windows, then in a new window instead)
323 * "tab" new tab (if there aren't any browser windows, then in a new window instead)
324 * "tabshifted" same as "tab" but in background if default is to select new tabs, and vice versa
325 * "window" new window
326 * "save" save to disk (with no filename hint!)
328 * @param {Object} params
330 * Options relating to what tab/window to use and how to open it:
332 * @param {boolean} params.private
333 * Load the URL in a private window.
334 * @param {boolean} params.forceNonPrivate
335 * Force the load to happen in non-private windows.
336 * @param {boolean} params.relatedToCurrent
337 * Whether new tabs should go immediately next to the current tab.
338 * @param {Element} params.targetBrowser
339 * The browser to use for the load. Only used if where == "current".
340 * @param {boolean} params.inBackground
341 * If explicitly true or false, whether to switch to the tab immediately.
342 * If null, will switch to the tab if `forceForeground` was true. If
343 * neither is passed, will defer to the user preference browser.tabs.loadInBackground.
344 * @param {boolean} params.forceForeground
345 * Ignore the user preference and load in the foreground.
346 * @param {boolean} params.allowPinnedTabHostChange
347 * Allow even a pinned tab to change hosts.
348 * @param {boolean} params.allowPopups
349 * whether the link is allowed to open in a popup window (ie one with no browser
351 * @param {boolean} params.skipTabAnimation
352 * Skip the tab opening animation.
353 * @param {Element} params.openerBrowser
354 * The browser that started the load.
355 * @param {boolean} params.avoidBrowserFocus
356 * Don't focus the browser element immediately after starting
357 * the load. Used by the URL bar to avoid leaking user input
358 * into web content, see bug 1641287.
360 * Options relating to the load itself:
362 * @param {boolean} params.allowThirdPartyFixup
363 * Allow transforming the 'url' into a search query.
364 * @param {nsIInputStream} params.postData
365 * Data to post as part of the request.
366 * @param {nsIReferrerInfo} params.referrerInfo
367 * Referrer info for the request.
368 * @param {boolean} params.indicateErrorPageLoad
369 * Whether docshell should throw an exception (i.e. return non-NS_OK)
371 * @param {string} params.charset
372 * Character set to use for the load. Only honoured for tabs.
373 * Legacy argument - do not use.
374 * @param {string} params.wasSchemelessInput
375 * Whether the search/URL term was without an explicit scheme.
377 * Options relating to security, whether the load is allowed to happen,
378 * and what cookie container to use for the load:
380 * @param {boolean} params.forceAllowDataURI
381 * Force allow a data URI to load as a toplevel load.
382 * @param {number} params.userContextId
383 * The userContextId (container identifier) to use for the load.
384 * @param {boolean} params.allowInheritPrincipal
385 * Allow the load to inherit the triggering principal.
386 * @param {boolean} params.forceAboutBlankViewerInCurrent
387 * Force load an about:blank page first. Only used if
388 * allowInheritPrincipal is passed or no URL was provided.
389 * @param {nsIPrincipal} params.triggeringPrincipal
390 * Triggering principal to pass to docshell for the load.
391 * @param {nsIPrincipal} params.originPrincipal
392 * Origin principal to pass to docshell for the load.
393 * @param {nsIPrincipal} params.originStoragePrincipal
394 * Storage principal to pass to docshell for the load.
395 * @param {string} params.triggeringRemoteType
396 * The remoteType triggering this load.
397 * @param {nsIContentSecurityPolicy} params.csp
398 * The CSP that should apply to the load.
399 * @param {boolean} params.hasValidUserGestureActivation
400 * Indicates if a valid user gesture caused this load. This
401 * informs e.g. popup blocker decisions.
402 * @param {boolean} params.fromExternal
403 * Indicates the load was started outside of the browser,
404 * e.g. passed on the commandline or through OS mechanisms.
406 * Options used to track the load elsewhere
408 * @param {function} params.resolveOnNewTabCreated
409 * This callback will be called when a new tab is created.
410 * @param {function} params.resolveOnContentBrowserCreated
411 * This callback will be called with the content browser once it's created.
412 * @param {Object} params.globalHistoryOptions
413 * Used by places to keep track of search related metadata for loads.
414 * @param {Number} params.frameID
415 * Used by webextensions for their loads.
417 * Options used for where="save" only:
419 * @param {boolean} params.isContentWindowPrivate
420 * Save content as coming from a private window.
421 * @param {Document} params.initiatingDoc
422 * Used to determine where to prompt for a filename.
424 openLinkIn(window, url, where, params) {
425 if (!where || !url) {
430 allowThirdPartyFixup,
434 allowInheritPrincipal,
438 allowPinnedTabHostChange,
442 originStoragePrincipal,
443 triggeringRemoteType,
445 resolveOnNewTabCreated,
446 resolveOnContentBrowserCreated,
447 globalHistoryOptions,
450 // We want to overwrite some things for convenience when passing it to other
451 // methods. To avoid impacting callers, copy the params.
452 params = Object.assign({}, params);
454 if (!params.referrerInfo) {
455 params.referrerInfo = new lazy.ReferrerInfo(
456 Ci.nsIReferrerInfo.EMPTY,
462 if (!triggeringPrincipal) {
463 throw new Error("Must load with a triggering Principal");
466 if (where == "save") {
467 saveLink(window, url, params);
471 // Establish which window we'll load the link in.
473 if (where == "current" && params.targetBrowser) {
474 w = params.targetBrowser.ownerGlobal;
476 w = this.getTargetWindow(window, { forceNonPrivate });
478 // We don't want to open tabs in popups, so try to find a non-popup window in
480 if ((where == "tab" || where == "tabshifted") && w && !w.toolbar.visible) {
481 w = this.getTargetWindow(window, {
485 relatedToCurrent = false;
488 updatePrincipals(w, params);
490 if (!w || where == "window") {
491 openInWindow(url, params, w || window);
495 // We're now committed to loading the link in an existing browser window.
497 // Raise the target window before loading the URI, since loading it may
498 // result in a new frontmost window (e.g. "javascript:window.open('');").
502 let loadInBackground;
505 if (where == "current") {
506 targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser;
507 loadInBackground = false;
509 uriObj = Services.io.newURI(url);
512 // In certain tabs, we restrict what if anything may replace the loaded
513 // page. If a load request bounces off for the currently selected tab,
514 // we'll open a new tab instead.
515 let tab = w.gBrowser.getTabForBrowser(targetBrowser);
516 if (tab == w.FirefoxViewHandler.tab) {
518 targetBrowser = null;
520 !allowPinnedTabHostChange &&
522 url != "about:crashcontent"
525 // nsIURI.host can throw for non-nsStandardURL nsIURIs.
528 (!uriObj.schemeIs("javascript") &&
529 targetBrowser.currentURI.host != uriObj.host)
532 targetBrowser = null;
536 targetBrowser = null;
540 // `where` is "tab" or "tabshifted", so we'll load the link in a new tab.
541 loadInBackground = params.inBackground;
542 if (loadInBackground == null) {
543 loadInBackground = params.forceForeground
545 : Services.prefs.getBoolPref("browser.tabs.loadInBackground");
549 let focusUrlBar = false;
553 openInCurrentTab(targetBrowser, url, uriObj, params);
555 // Don't focus the content area if focus is in the address bar and we're
556 // loading the New Tab page.
558 w.document.activeElement == w.gURLBar.inputField &&
559 w.isBlankPageURL(url);
562 loadInBackground = !loadInBackground;
567 w.isBlankPageURL(url) &&
568 !lazy.AboutNewTab.willNotifyUser;
570 let tabUsedForLoad = w.gBrowser.addTab(url, {
571 referrerInfo: params.referrerInfo,
574 inBackground: loadInBackground,
575 allowThirdPartyFixup,
577 skipAnimation: skipTabAnimation,
580 originStoragePrincipal,
582 allowInheritPrincipal,
583 triggeringRemoteType,
587 openerBrowser: params.openerBrowser,
588 fromExternal: params.fromExternal,
589 globalHistoryOptions,
590 wasSchemelessInput: params.wasSchemelessInput,
592 targetBrowser = tabUsedForLoad.linkedBrowser;
594 resolveOnNewTabCreated?.(targetBrowser);
595 resolveOnContentBrowserCreated?.(targetBrowser);
597 if (params.frameID != undefined && w) {
598 // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget
599 // event if it contains the expected frameID params.
600 // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is
601 // opening a new tab using the keyboard shortcut).
602 Services.obs.notifyObservers(
606 createdTabBrowser: targetBrowser,
607 sourceTabBrowser: w.gBrowser.selectedBrowser,
608 sourceFrameID: params.frameID,
611 "webNavigation-createdNavigationTarget"
618 !params.avoidBrowserFocus &&
620 targetBrowser == w.gBrowser.selectedBrowser
622 // Focus the content, but only if the browser used for the load is selected.
623 targetBrowser.focus();
628 * Finds a browser window suitable for opening a link matching the
629 * requirements given in the `params` argument. If the current window matches
630 * the requirements then it is returned otherwise the top-most window that
631 * matches will be returned.
633 * @param {Window} window - The current window.
634 * @param {Object} params - Parameters for selecting the window.
635 * @param {boolean} params.skipPopups - Require a non-popup window.
636 * @param {boolean} params.forceNonPrivate - Require a non-private window.
637 * @returns {Window | null} A matching browser window or null if none matched.
639 getTargetWindow(window, { skipPopups, forceNonPrivate } = {}) {
640 let { top } = window;
641 // If this is called in a browser window, use that window regardless of
642 // whether it's the frontmost window, since commands can be executed in
643 // background windows (bug 626148).
645 top.document.documentElement.getAttribute("windowtype") ==
646 "navigator:browser" &&
647 (!skipPopups || top.toolbar.visible) &&
648 (!forceNonPrivate || !PrivateBrowsingUtils.isWindowPrivate(top))
653 return lazy.BrowserWindowTracker.getTopWindow({
654 private: !forceNonPrivate && PrivateBrowsingUtils.isWindowPrivate(window),
655 allowPopups: !skipPopups,
660 * openUILink handles clicks on UI elements that cause URLs to load.
662 * @param {string} url
663 * @param {Event | Object} event Event or JSON object representing an Event
664 * @param {Boolean | Object} aIgnoreButton
665 * Boolean or object with the same properties as
666 * accepted by openLinkIn, plus "ignoreButton"
668 * @param {Boolean} aIgnoreAlt
669 * @param {Boolean} aAllowThirdPartyFixup
670 * @param {Object} aPostData
671 * @param {Object} aReferrerInfo
679 aAllowThirdPartyFixup,
683 event = BrowserUtils.getRootEvent(event);
686 if (aIgnoreButton && typeof aIgnoreButton == "object") {
687 params = aIgnoreButton;
689 // don't forward "ignoreButton" and "ignoreAlt" to openLinkIn
690 aIgnoreButton = params.ignoreButton;
691 aIgnoreAlt = params.ignoreAlt;
692 delete params.ignoreButton;
693 delete params.ignoreAlt;
696 allowThirdPartyFixup: aAllowThirdPartyFixup,
698 referrerInfo: aReferrerInfo,
699 initiatingDoc: event ? event.target.ownerDocument : null,
703 if (!params.triggeringPrincipal) {
705 "Required argument triggeringPrincipal missing within openUILink"
709 let where = BrowserUtils.whereToOpenLink(event, aIgnoreButton, aIgnoreAlt);
710 params.forceForeground ??= true;
711 this.openLinkIn(window, url, where, params);
714 /* openTrustedLinkIn will attempt to open the given URI using the SystemPrincipal
715 * as the trigeringPrincipal, unless a more specific Principal is provided.
717 * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground`
720 openTrustedLinkIn(window, url, where, params = {}) {
721 if (!params.triggeringPrincipal) {
722 params.triggeringPrincipal =
723 Services.scriptSecurityManager.getSystemPrincipal();
726 params.forceForeground ??= true;
727 this.openLinkIn(window, url, where, params);
730 /* openWebLinkIn will attempt to open the given URI using the NullPrincipal
731 * as the triggeringPrincipal, unless a more specific Principal is provided.
733 * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground`
736 openWebLinkIn(window, url, where, params = {}) {
737 if (!params.triggeringPrincipal) {
738 params.triggeringPrincipal =
739 Services.scriptSecurityManager.createNullPrincipal({});
741 if (params.triggeringPrincipal.isSystemPrincipal) {
743 "System principal should never be passed into openWebLinkIn()"
746 params.forceForeground ??= true;
747 this.openLinkIn(window, url, where, params);
751 * Given a URI, guess which container to use to open it. This is used for external
752 * openers as a quality of life improvement (e.g. to open a document into the container
753 * where you are logged in to the service that hosts it).
754 * matches will be returned.
755 * For now this can only use currently-open tabs, until history is tagged with the
756 * container id (https://bugzilla.mozilla.org/show_bug.cgi?id=1283320).
758 * @param {nsIURI} aURI - The URI being opened.
759 * @returns {number | null} The guessed userContextId, or null if none.
761 guessUserContextId(aURI) {
769 const containerScores = new Map();
770 let guessedUserContextId = null;
772 for (let win of lazy.BrowserWindowTracker.orderedWindows) {
773 for (let tab of win.gBrowser.visibleTabs) {
774 let { userContextId } = tab;
775 let currentURIHost = null;
777 currentURIHost = tab.linkedBrowser.currentURI.host;
780 if (currentURIHost == host) {
781 let count = (containerScores.get(userContextId) ?? 0) + 1;
782 containerScores.set(userContextId, count);
783 if (count > maxCount) {
784 guessedUserContextId = userContextId;
791 return guessedUserContextId;