gecko-dev/browser/modules/URILoadingHelper.sys.mjs

794 lines
26 KiB
JavaScript

/* 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";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
});
ChromeUtils.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
);
}
}
if (params.wasSchemelessInput !== undefined) {
extraOptions.setPropertyAsBool(
"wasSchemelessInput",
params.wasSchemelessInput
);
}
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.createAboutBlankDocumentViewer(
params.originPrincipal,
params.originStoragePrincipal
);
}
let {
triggeringPrincipal,
csp,
referrerInfo,
postData,
userContextId,
hasValidUserGestureActivation,
globalHistoryOptions,
triggeringRemoteType,
wasSchemelessInput,
} = params;
targetBrowser.fixupAndLoadURIString(url, {
triggeringPrincipal,
csp,
flags,
referrerInfo,
postData,
userContextId,
hasValidUserGestureActivation,
globalHistoryOptions,
triggeringRemoteType,
wasSchemelessInput,
});
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.
* @param {string} params.wasSchemelessInput
* Whether the search/URL term was without an explicit scheme.
*
* 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,
wasSchemelessInput: params.wasSchemelessInput,
});
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);
},
/**
* Given a URI, guess which container to use to open it. This is used for external
* openers as a quality of life improvement (e.g. to open a document into the container
* where you are logged in to the service that hosts it).
* matches will be returned.
* For now this can only use currently-open tabs, until history is tagged with the
* container id (https://bugzilla.mozilla.org/show_bug.cgi?id=1283320).
*
* @param {nsIURI} aURI - The URI being opened.
* @returns {number | null} The guessed userContextId, or null if none.
*/
guessUserContextId(aURI) {
let host;
try {
host = aURI.host;
} catch (e) {}
if (!host) {
return null;
}
const containerScores = new Map();
let guessedUserContextId = null;
let maxCount = 0;
for (let win of lazy.BrowserWindowTracker.orderedWindows) {
for (let tab of win.gBrowser.visibleTabs) {
let { userContextId } = tab;
let currentURIHost = null;
try {
currentURIHost = tab.linkedBrowser.currentURI.host;
} catch (e) {}
if (currentURIHost == host) {
let count = (containerScores.get(userContextId) ?? 0) + 1;
containerScores.set(userContextId, count);
if (count > maxCount) {
guessedUserContextId = userContextId;
maxCount = count;
}
}
}
}
return guessedUserContextId;
},
};