diff options
Diffstat (limited to 'browser/modules/URILoadingHelper.sys.mjs')
-rw-r--r-- | browser/modules/URILoadingHelper.sys.mjs | 739 |
1 files changed, 739 insertions, 0 deletions
diff --git a/browser/modules/URILoadingHelper.sys.mjs b/browser/modules/URILoadingHelper.sys.mjs new file mode 100644 index 0000000000..d08bc68ce8 --- /dev/null +++ b/browser/modules/URILoadingHelper.sys.mjs @@ -0,0 +1,739 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs"; +import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", +}); + +XPCOMUtils.defineLazyGetter(lazy, "ReferrerInfo", () => + Components.Constructor( + "@mozilla.org/referrer-info;1", + "nsIReferrerInfo", + "init" + ) +); + +function saveLink(window, url, params) { + if ("isContentWindowPrivate" in params) { + window.saveURL( + url, + null, + null, + null, + true, + true, + params.referrerInfo, + null, + null, + params.isContentWindowPrivate, + params.originPrincipal + ); + } else { + if (!params.initiatingDoc) { + console.error( + "openUILink/openLinkIn was called with " + + "where == 'save' but without initiatingDoc. See bug 814264." + ); + return; + } + window.saveURL( + url, + null, + null, + null, + true, + true, + params.referrerInfo, + null, + params.initiatingDoc + ); + } +} + +function openInWindow(url, params, sourceWindow) { + let { + referrerInfo, + forceNonPrivate, + triggeringRemoteType, + forceAllowDataURI, + globalHistoryOptions, + allowThirdPartyFixup, + userContextId, + postData, + originPrincipal, + originStoragePrincipal, + triggeringPrincipal, + csp, + resolveOnContentBrowserCreated, + } = params; + let features = "chrome,dialog=no,all"; + if (params.private) { + features += ",private"; + // To prevent regular browsing data from leaking to private browsing sites, + // strip the referrer when opening a new private window. (See Bug: 1409226) + referrerInfo = new lazy.ReferrerInfo( + referrerInfo.referrerPolicy, + false, + referrerInfo.originalReferrer + ); + } else if (forceNonPrivate) { + features += ",non-private"; + } + + // This propagates to window.arguments. + var sa = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + + var wuri = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + wuri.data = url; + + let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag2 + ); + if (triggeringRemoteType) { + extraOptions.setPropertyAsACString( + "triggeringRemoteType", + triggeringRemoteType + ); + } + if (params.hasValidUserGestureActivation !== undefined) { + extraOptions.setPropertyAsBool( + "hasValidUserGestureActivation", + params.hasValidUserGestureActivation + ); + } + if (forceAllowDataURI) { + extraOptions.setPropertyAsBool("forceAllowDataURI", true); + } + if (params.fromExternal !== undefined) { + extraOptions.setPropertyAsBool("fromExternal", params.fromExternal); + } + if (globalHistoryOptions?.triggeringSponsoredURL) { + extraOptions.setPropertyAsACString( + "triggeringSponsoredURL", + globalHistoryOptions.triggeringSponsoredURL + ); + if (globalHistoryOptions.triggeringSponsoredURLVisitTimeMS) { + extraOptions.setPropertyAsUint64( + "triggeringSponsoredURLVisitTimeMS", + globalHistoryOptions.triggeringSponsoredURLVisitTimeMS + ); + } + } + + var allowThirdPartyFixupSupports = Cc[ + "@mozilla.org/supports-PRBool;1" + ].createInstance(Ci.nsISupportsPRBool); + allowThirdPartyFixupSupports.data = allowThirdPartyFixup; + + var userContextIdSupports = Cc[ + "@mozilla.org/supports-PRUint32;1" + ].createInstance(Ci.nsISupportsPRUint32); + userContextIdSupports.data = userContextId; + + sa.appendElement(wuri); + sa.appendElement(extraOptions); + sa.appendElement(referrerInfo); + sa.appendElement(postData); + sa.appendElement(allowThirdPartyFixupSupports); + sa.appendElement(userContextIdSupports); + sa.appendElement(originPrincipal); + sa.appendElement(originStoragePrincipal); + sa.appendElement(triggeringPrincipal); + sa.appendElement(null); // allowInheritPrincipal + sa.appendElement(csp); + + let win; + + // Returns a promise that will be resolved when the new window's startup is finished. + function waitForWindowStartup() { + return new Promise(resolve => { + const delayedStartupObserver = aSubject => { + if (aSubject == win) { + Services.obs.removeObserver( + delayedStartupObserver, + "browser-delayed-startup-finished" + ); + resolve(); + } + }; + Services.obs.addObserver( + delayedStartupObserver, + "browser-delayed-startup-finished" + ); + }); + } + + if (params.frameID != undefined && sourceWindow) { + // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget + // event if it contains the expected frameID params. + // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is + // opening a new window using the keyboard shortcut). + const sourceTabBrowser = sourceWindow.gBrowser.selectedBrowser; + waitForWindowStartup().then(() => { + Services.obs.notifyObservers( + { + wrappedJSObject: { + url, + createdTabBrowser: win.gBrowser.selectedBrowser, + sourceTabBrowser, + sourceFrameID: params.frameID, + }, + }, + "webNavigation-createdNavigationTarget" + ); + }); + } + + if (resolveOnContentBrowserCreated) { + waitForWindowStartup().then(() => + resolveOnContentBrowserCreated(win.gBrowser.selectedBrowser) + ); + } + + win = Services.ww.openWindow( + sourceWindow, + AppConstants.BROWSER_CHROME_URL, + null, + features, + sa + ); +} + +function openInCurrentTab(targetBrowser, url, uriObj, params) { + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + + if (params.allowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL isn't supported for javascript URIs, + // i.e. it causes them not to load at all. Callers should strip + // "javascript:" from pasted strings to prevent blank tabs + if (!params.allowInheritPrincipal) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + } + + if (params.allowPopups) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS; + } + if (params.indicateErrorPageLoad) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV; + } + if (params.forceAllowDataURI) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI; + } + if (params.fromExternal) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; + } + + let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler; + if ( + params.forceAboutBlankViewerInCurrent && + (!uriObj || + Services.io.getDynamicProtocolFlags(uriObj) & + URI_INHERITS_SECURITY_CONTEXT) + ) { + // Unless we know for sure we're not inheriting principals, + // force the about:blank viewer to have the right principal: + targetBrowser.createAboutBlankContentViewer( + params.originPrincipal, + params.originStoragePrincipal + ); + } + + let { + triggeringPrincipal, + csp, + referrerInfo, + postData, + userContextId, + hasValidUserGestureActivation, + globalHistoryOptions, + triggeringRemoteType, + } = params; + + targetBrowser.fixupAndLoadURIString(url, { + triggeringPrincipal, + csp, + flags, + referrerInfo, + postData, + userContextId, + hasValidUserGestureActivation, + globalHistoryOptions, + triggeringRemoteType, + }); + params.resolveOnContentBrowserCreated?.(targetBrowser); +} + +function updatePrincipals(window, params) { + let { userContextId } = params; + // Teach the principal about the right OA to use, e.g. in case when + // opening a link in a new private window, or in a new container tab. + // Please note we do not have to do that for SystemPrincipals and we + // can not do it for NullPrincipals since NullPrincipals are only + // identical if they actually are the same object (See Bug: 1346759) + function useOAForPrincipal(principal) { + if (principal && principal.isContentPrincipal) { + let privateBrowsingId = + params.private || + (window && PrivateBrowsingUtils.isWindowPrivate(window)); + let attrs = { + userContextId, + privateBrowsingId, + firstPartyDomain: principal.originAttributes.firstPartyDomain, + }; + return Services.scriptSecurityManager.principalWithOA(principal, attrs); + } + return principal; + } + params.originPrincipal = useOAForPrincipal(params.originPrincipal); + params.originStoragePrincipal = useOAForPrincipal( + params.originStoragePrincipal + ); + params.triggeringPrincipal = useOAForPrincipal(params.triggeringPrincipal); +} + +export const URILoadingHelper = { + /* openLinkIn opens a URL in a place specified by the parameter |where|. + * + * The params object is the same as for `openLinkIn` and documented below. + * + * @param {String} where + * |where| can be: + * "current" current tab (if there aren't any browser windows, then in a new window instead) + * "tab" new tab (if there aren't any browser windows, then in a new window instead) + * "tabshifted" same as "tab" but in background if default is to select new tabs, and vice versa + * "window" new window + * "save" save to disk (with no filename hint!) + * + * @param {Object} params + * + * Options relating to what tab/window to use and how to open it: + * + * @param {boolean} params.private + * Load the URL in a private window. + * @param {boolean} params.forceNonPrivate + * Force the load to happen in non-private windows. + * @param {boolean} params.relatedToCurrent + * Whether new tabs should go immediately next to the current tab. + * @param {Element} params.targetBrowser + * The browser to use for the load. Only used if where == "current". + * @param {boolean} params.inBackground + * If explicitly true or false, whether to switch to the tab immediately. + * If null, will switch to the tab if `forceForeground` was true. If + * neither is passed, will defer to the user preference browser.tabs.loadInBackground. + * @param {boolean} params.forceForeground + * Ignore the user preference and load in the foreground. + * @param {boolean} params.allowPinnedTabHostChange + * Allow even a pinned tab to change hosts. + * @param {boolean} params.allowPopups + * whether the link is allowed to open in a popup window (ie one with no browser + * chrome) + * @param {boolean} params.skipTabAnimation + * Skip the tab opening animation. + * @param {Element} params.openerBrowser + * The browser that started the load. + * @param {boolean} params.avoidBrowserFocus + * Don't focus the browser element immediately after starting + * the load. Used by the URL bar to avoid leaking user input + * into web content, see bug 1641287. + * + * Options relating to the load itself: + * + * @param {boolean} params.allowThirdPartyFixup + * Allow transforming the 'url' into a search query. + * @param {nsIInputStream} params.postData + * Data to post as part of the request. + * @param {nsIReferrerInfo} params.referrerInfo + * Referrer info for the request. + * @param {boolean} params.indicateErrorPageLoad + * Whether docshell should throw an exception (i.e. return non-NS_OK) + * if the load fails. + * @param {string} params.charset + * Character set to use for the load. Only honoured for tabs. + * Legacy argument - do not use. + * + * Options relating to security, whether the load is allowed to happen, + * and what cookie container to use for the load: + * + * @param {boolean} params.forceAllowDataURI + * Force allow a data URI to load as a toplevel load. + * @param {number} params.userContextId + * The userContextId (container identifier) to use for the load. + * @param {boolean} params.allowInheritPrincipal + * Allow the load to inherit the triggering principal. + * @param {boolean} params.forceAboutBlankViewerInCurrent + * Force load an about:blank page first. Only used if + * allowInheritPrincipal is passed or no URL was provided. + * @param {nsIPrincipal} params.triggeringPrincipal + * Triggering principal to pass to docshell for the load. + * @param {nsIPrincipal} params.originPrincipal + * Origin principal to pass to docshell for the load. + * @param {nsIPrincipal} params.originStoragePrincipal + * Storage principal to pass to docshell for the load. + * @param {string} params.triggeringRemoteType + * The remoteType triggering this load. + * @param {nsIContentSecurityPolicy} params.csp + * The CSP that should apply to the load. + * @param {boolean} params.hasValidUserGestureActivation + * Indicates if a valid user gesture caused this load. This + * informs e.g. popup blocker decisions. + * @param {boolean} params.fromExternal + * Indicates the load was started outside of the browser, + * e.g. passed on the commandline or through OS mechanisms. + * + * Options used to track the load elsewhere + * + * @param {function} params.resolveOnNewTabCreated + * This callback will be called when a new tab is created. + * @param {function} params.resolveOnContentBrowserCreated + * This callback will be called with the content browser once it's created. + * @param {Object} params.globalHistoryOptions + * Used by places to keep track of search related metadata for loads. + * @param {Number} params.frameID + * Used by webextensions for their loads. + * + * Options used for where="save" only: + * + * @param {boolean} params.isContentWindowPrivate + * Save content as coming from a private window. + * @param {Document} params.initiatingDoc + * Used to determine where to prompt for a filename. + */ + openLinkIn(window, url, where, params) { + if (!where || !url) { + return; + } + + let { + allowThirdPartyFixup, + postData, + charset, + relatedToCurrent, + allowInheritPrincipal, + forceAllowDataURI, + forceNonPrivate, + skipTabAnimation, + allowPinnedTabHostChange, + userContextId, + triggeringPrincipal, + originPrincipal, + originStoragePrincipal, + triggeringRemoteType, + csp, + resolveOnNewTabCreated, + resolveOnContentBrowserCreated, + globalHistoryOptions, + } = params; + + // We want to overwrite some things for convenience when passing it to other + // methods. To avoid impacting callers, copy the params. + params = Object.assign({}, params); + + if (!params.referrerInfo) { + params.referrerInfo = new lazy.ReferrerInfo( + Ci.nsIReferrerInfo.EMPTY, + true, + null + ); + } + + if (!triggeringPrincipal) { + throw new Error("Must load with a triggering Principal"); + } + + if (where == "save") { + saveLink(window, url, params); + return; + } + + // Establish which window we'll load the link in. + let w; + if (where == "current" && params.targetBrowser) { + w = params.targetBrowser.ownerGlobal; + } else { + w = this.getTargetWindow(window, { forceNonPrivate }); + } + // We don't want to open tabs in popups, so try to find a non-popup window in + // that case. + if ((where == "tab" || where == "tabshifted") && w && !w.toolbar.visible) { + w = this.getTargetWindow(window, { + skipPopups: true, + forceNonPrivate, + }); + relatedToCurrent = false; + } + + updatePrincipals(w, params); + + if (!w || where == "window") { + openInWindow(url, params, w || window); + return; + } + + // We're now committed to loading the link in an existing browser window. + + // Raise the target window before loading the URI, since loading it may + // result in a new frontmost window (e.g. "javascript:window.open('');"). + w.focus(); + + let targetBrowser; + let loadInBackground; + let uriObj; + + if (where == "current") { + targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser; + loadInBackground = false; + try { + uriObj = Services.io.newURI(url); + } catch (e) {} + + // In certain tabs, we restrict what if anything may replace the loaded + // page. If a load request bounces off for the currently selected tab, + // we'll open a new tab instead. + let tab = w.gBrowser.getTabForBrowser(targetBrowser); + if (tab == w.FirefoxViewHandler.tab) { + where = "tab"; + targetBrowser = null; + } else if ( + !allowPinnedTabHostChange && + tab.pinned && + url != "about:crashcontent" + ) { + try { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + if ( + !uriObj || + (!uriObj.schemeIs("javascript") && + targetBrowser.currentURI.host != uriObj.host) + ) { + where = "tab"; + targetBrowser = null; + } + } catch (err) { + where = "tab"; + targetBrowser = null; + } + } + } else { + // `where` is "tab" or "tabshifted", so we'll load the link in a new tab. + loadInBackground = params.inBackground; + if (loadInBackground == null) { + loadInBackground = params.forceForeground + ? false + : Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + } + } + + let focusUrlBar = false; + + switch (where) { + case "current": + openInCurrentTab(targetBrowser, url, uriObj, params); + + // Don't focus the content area if focus is in the address bar and we're + // loading the New Tab page. + focusUrlBar = + w.document.activeElement == w.gURLBar.inputField && + w.isBlankPageURL(url); + break; + case "tabshifted": + loadInBackground = !loadInBackground; + // fall through + case "tab": + focusUrlBar = + !loadInBackground && + w.isBlankPageURL(url) && + !lazy.AboutNewTab.willNotifyUser; + + let tabUsedForLoad = w.gBrowser.addTab(url, { + referrerInfo: params.referrerInfo, + charset, + postData, + inBackground: loadInBackground, + allowThirdPartyFixup, + relatedToCurrent, + skipAnimation: skipTabAnimation, + userContextId, + originPrincipal, + originStoragePrincipal, + triggeringPrincipal, + allowInheritPrincipal, + triggeringRemoteType, + csp, + forceAllowDataURI, + focusUrlBar, + openerBrowser: params.openerBrowser, + fromExternal: params.fromExternal, + globalHistoryOptions, + }); + targetBrowser = tabUsedForLoad.linkedBrowser; + + resolveOnNewTabCreated?.(targetBrowser); + resolveOnContentBrowserCreated?.(targetBrowser); + + if (params.frameID != undefined && w) { + // Only notify it as a WebExtensions' webNavigation.onCreatedNavigationTarget + // event if it contains the expected frameID params. + // (e.g. we should not notify it as a onCreatedNavigationTarget if the user is + // opening a new tab using the keyboard shortcut). + Services.obs.notifyObservers( + { + wrappedJSObject: { + url, + createdTabBrowser: targetBrowser, + sourceTabBrowser: w.gBrowser.selectedBrowser, + sourceFrameID: params.frameID, + }, + }, + "webNavigation-createdNavigationTarget" + ); + } + break; + } + + if ( + !params.avoidBrowserFocus && + !focusUrlBar && + targetBrowser == w.gBrowser.selectedBrowser + ) { + // Focus the content, but only if the browser used for the load is selected. + targetBrowser.focus(); + } + }, + + /** + * Finds a browser window suitable for opening a link matching the + * requirements given in the `params` argument. If the current window matches + * the requirements then it is returned otherwise the top-most window that + * matches will be returned. + * + * @param {Window} window - The current window. + * @param {Object} params - Parameters for selecting the window. + * @param {boolean} params.skipPopups - Require a non-popup window. + * @param {boolean} params.forceNonPrivate - Require a non-private window. + * @returns {Window | null} A matching browser window or null if none matched. + */ + getTargetWindow(window, { skipPopups, forceNonPrivate } = {}) { + let { top } = window; + // If this is called in a browser window, use that window regardless of + // whether it's the frontmost window, since commands can be executed in + // background windows (bug 626148). + if ( + top.document.documentElement.getAttribute("windowtype") == + "navigator:browser" && + (!skipPopups || top.toolbar.visible) && + (!forceNonPrivate || !PrivateBrowsingUtils.isWindowPrivate(top)) + ) { + return top; + } + + return lazy.BrowserWindowTracker.getTopWindow({ + private: !forceNonPrivate && PrivateBrowsingUtils.isWindowPrivate(window), + allowPopups: !skipPopups, + }); + }, + + /** + * openUILink handles clicks on UI elements that cause URLs to load. + * + * @param {string} url + * @param {Event | Object} event Event or JSON object representing an Event + * @param {Boolean | Object} aIgnoreButton + * Boolean or object with the same properties as + * accepted by openLinkIn, plus "ignoreButton" + * and "ignoreAlt". + * @param {Boolean} aIgnoreAlt + * @param {Boolean} aAllowThirdPartyFixup + * @param {Object} aPostData + * @param {Object} aReferrerInfo + */ + openUILink( + window, + url, + event, + aIgnoreButton, + aIgnoreAlt, + aAllowThirdPartyFixup, + aPostData, + aReferrerInfo + ) { + event = BrowserUtils.getRootEvent(event); + let params; + + if (aIgnoreButton && typeof aIgnoreButton == "object") { + params = aIgnoreButton; + + // don't forward "ignoreButton" and "ignoreAlt" to openLinkIn + aIgnoreButton = params.ignoreButton; + aIgnoreAlt = params.ignoreAlt; + delete params.ignoreButton; + delete params.ignoreAlt; + } else { + params = { + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostData, + referrerInfo: aReferrerInfo, + initiatingDoc: event ? event.target.ownerDocument : null, + }; + } + + if (!params.triggeringPrincipal) { + throw new Error( + "Required argument triggeringPrincipal missing within openUILink" + ); + } + + let where = BrowserUtils.whereToOpenLink(event, aIgnoreButton, aIgnoreAlt); + params.forceForeground ??= true; + this.openLinkIn(window, url, where, params); + }, + + /* openTrustedLinkIn will attempt to open the given URI using the SystemPrincipal + * as the trigeringPrincipal, unless a more specific Principal is provided. + * + * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground` + * to true. + */ + openTrustedLinkIn(window, url, where, params = {}) { + if (!params.triggeringPrincipal) { + params.triggeringPrincipal = + Services.scriptSecurityManager.getSystemPrincipal(); + } + + params.forceForeground ??= true; + this.openLinkIn(window, url, where, params); + }, + + /* openWebLinkIn will attempt to open the given URI using the NullPrincipal + * as the triggeringPrincipal, unless a more specific Principal is provided. + * + * Otherwise, parameters are the same as openLinkIn, but we will set `forceForeground` + * to true. + */ + openWebLinkIn(window, url, where, params = {}) { + if (!params.triggeringPrincipal) { + params.triggeringPrincipal = + Services.scriptSecurityManager.createNullPrincipal({}); + } + if (params.triggeringPrincipal.isSystemPrincipal) { + throw new Error( + "System principal should never be passed into openWebLinkIn()" + ); + } + params.forceForeground ??= true; + this.openLinkIn(window, url, where, params); + }, +}; |