One module-level weak map replaces the per-collector stores, so a trigger resolves the tab's bounce whichever input started it. Triggering is a module function taking the browser, which the chrome call sites reach directly rather than through a collector and, on the message path, the actor. The direct path collects its snapshot before awaiting a prior bounce's trigger, where the engagement record that follows had already closed the view. URILoadingHelper's trigger fires again too, having passed a browser element to a browser-id parameter since bug 1624579, and the load flag it checks now covers every urlbar input rather than the address bar alone. Differential Revision: https://phabricator.services.mozilla.com/D323123
1129 lines
39 KiB
JavaScript
1129 lines
39 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",
|
|
handleBounceEventTrigger:
|
|
"moz-src:///browser/components/urlbar/UrlbarParentController.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,
|
|
policyContainer,
|
|
resolveOnContentBrowserCreated,
|
|
chromeless,
|
|
width,
|
|
height,
|
|
} = params;
|
|
const chromelessDimensions =
|
|
chromeless && width && height ? `,width=${width},height=${height}` : "";
|
|
const CHROMELESS_FEATURES = `resizable,minimizable,titlebar,close${chromelessDimensions}`;
|
|
let features = `chrome,dialog=no,${chromeless ? CHROMELESS_FEATURES : "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 (params.textDirectiveUserActivation !== undefined) {
|
|
extraOptions.setPropertyAsBool(
|
|
"textDirectiveUserActivation",
|
|
params.textDirectiveUserActivation
|
|
);
|
|
}
|
|
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 (globalHistoryOptions.triggeringSource) {
|
|
extraOptions.setPropertyAsACString(
|
|
"triggeringSource",
|
|
globalHistoryOptions.triggeringSource
|
|
);
|
|
}
|
|
}
|
|
if (params.schemelessInput !== undefined) {
|
|
extraOptions.setPropertyAsUint32("schemelessInput", params.schemelessInput);
|
|
}
|
|
if (params.aiWindow) {
|
|
extraOptions.setPropertyAsBool("ai-window", true);
|
|
}
|
|
if (chromeless) {
|
|
extraOptions.setPropertyAsBool("chromeless-window", true);
|
|
}
|
|
if (params.aswebauth) {
|
|
extraOptions.setPropertyAsBool("aswebauth", true);
|
|
}
|
|
|
|
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(policyContainer);
|
|
|
|
let win;
|
|
|
|
// Returns a promise that will be resolved when the new window's startup is finished.
|
|
function waitForWindowStartup() {
|
|
return new Promise(resolve => {
|
|
const removeObservers = () => {
|
|
Services.obs.removeObserver(
|
|
delayedStartupObserver,
|
|
"browser-delayed-startup-finished"
|
|
);
|
|
Services.obs.removeObserver(closedObserver, "domwindowclosed");
|
|
};
|
|
const delayedStartupObserver = aSubject => {
|
|
if (aSubject == win) {
|
|
removeObservers();
|
|
resolve();
|
|
}
|
|
};
|
|
// Delayed startup never fires if the window closes first.
|
|
const closedObserver = aSubject => {
|
|
if (aSubject == win) {
|
|
removeObservers();
|
|
}
|
|
};
|
|
Services.obs.addObserver(
|
|
delayedStartupObserver,
|
|
"browser-delayed-startup-finished"
|
|
);
|
|
Services.obs.addObserver(closedObserver, "domwindowclosed");
|
|
});
|
|
}
|
|
|
|
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 loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
|
|
if (params.allowThirdPartyFixup) {
|
|
loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
|
|
loadFlags |= 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) {
|
|
loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
|
|
}
|
|
|
|
if (params.allowPopups) {
|
|
loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS;
|
|
}
|
|
if (params.indicateErrorPageLoad) {
|
|
loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV;
|
|
}
|
|
if (params.forceAllowDataURI) {
|
|
loadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
|
|
}
|
|
if (params.fromExternal) {
|
|
loadFlags |= 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,
|
|
policyContainer,
|
|
referrerInfo,
|
|
postData,
|
|
userContextId,
|
|
hasValidUserGestureActivation,
|
|
textDirectiveUserActivation,
|
|
globalHistoryOptions,
|
|
triggeringRemoteType,
|
|
schemelessInput,
|
|
} = params;
|
|
|
|
targetBrowser.fixupAndLoadURIString(url, {
|
|
triggeringPrincipal,
|
|
policyContainer,
|
|
loadFlags,
|
|
referrerInfo,
|
|
postData,
|
|
userContextId,
|
|
hasValidUserGestureActivation,
|
|
textDirectiveUserActivation,
|
|
globalHistoryOptions,
|
|
triggeringRemoteType,
|
|
schemelessInput,
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
/* Creates a null principal using the userContextId
|
|
from the current selected tab or a passed in tab argument */
|
|
function _createNullPrincipalFromTabUserContextId(tab = null) {
|
|
const window = lazy.BrowserWindowTracker.getTopWindow();
|
|
if (!tab) {
|
|
tab = window.gBrowser.selectedTab;
|
|
}
|
|
|
|
let userContextId;
|
|
if (tab.hasAttribute("usercontextid")) {
|
|
userContextId = tab.getAttribute("usercontextid");
|
|
}
|
|
return Services.scriptSecurityManager.createNullPrincipal({
|
|
userContextId,
|
|
});
|
|
}
|
|
|
|
export const URILoadingHelper = {
|
|
/**
|
|
* Opens a URL in the place given by ``where``, which can be:
|
|
*
|
|
* - ``"current"``: the current tab, or a new window if there are no browser
|
|
* windows
|
|
* - ``"tab"``: a new tab, or a new window if there are no browser windows
|
|
* - ``"tabshifted"``: as ``"tab"``, but with the foreground decision inverted
|
|
* - ``"window"``: a new window
|
|
* - ``"chromeless"``: a new minimal window, with no browser navigation UI
|
|
* - ``"save"``: save to disk, with no filename hint
|
|
*
|
|
* The keys of ``params`` fall into five groups, in the order they appear
|
|
* below:
|
|
*
|
|
* 1. which tab or window to use, and how to open it
|
|
* 2. the load itself
|
|
* 3. security, whether the load is allowed, and which cookie container to use
|
|
* 4. tracking the load elsewhere
|
|
* 5. ``where="save"`` only
|
|
*
|
|
* @param {Window} window
|
|
* The window the load is initiated from.
|
|
* @param {string} url
|
|
* The URL to load.
|
|
* @param {string} where
|
|
* Where to open the URL, from the list above.
|
|
* @param {object} params
|
|
* Options for the load, in the five groups above.
|
|
* @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.
|
|
* @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 {SchemelessInputType} params.schemelessInput
|
|
* Whether the search/URL term was without an explicit scheme.
|
|
* @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.
|
|
* If where is "current" and the specified userContextId differs, a new tab is opened instead.
|
|
* @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 {nsIPolicyContainer} params.policyContainer
|
|
* The policyContainer 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.
|
|
* @param {boolean} params.aswebauth
|
|
* Marks a new window as an ASWebAuthenticationSession auth
|
|
* window so that it is not tracked by session restore. Only
|
|
* used when where == "window" or "chromeless".
|
|
* @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.
|
|
* @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,
|
|
allowInheritPrincipal,
|
|
forceAllowDataURI,
|
|
forceNonPrivate,
|
|
skipTabAnimation,
|
|
allowPinnedTabHostChange,
|
|
userContextId,
|
|
triggeringPrincipal,
|
|
originPrincipal,
|
|
originStoragePrincipal,
|
|
triggeringRemoteType,
|
|
policyContainer,
|
|
resolveOnNewTabCreated,
|
|
resolveOnContentBrowserCreated,
|
|
globalHistoryOptions,
|
|
hasValidUserGestureActivation,
|
|
textDirectiveUserActivation,
|
|
} = 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 = this._resolveInitialTargetWindow(
|
|
where,
|
|
params,
|
|
window,
|
|
forceNonPrivate
|
|
);
|
|
|
|
updatePrincipals(w, params);
|
|
|
|
if (where == "chromeless") {
|
|
params.chromeless = true;
|
|
where = "window";
|
|
}
|
|
|
|
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 uriObj;
|
|
let loadInBackground = BrowserUtils.willLoadInBackground(where, params);
|
|
|
|
if (where == "current") {
|
|
targetBrowser = params.targetBrowser || w.gBrowser.selectedBrowser;
|
|
uriObj = URL.parse(url)?.URI;
|
|
|
|
// 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 (
|
|
params.userContextId != null &&
|
|
params.userContextId !==
|
|
targetBrowser.browsingContext.originAttributes.userContextId
|
|
) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 "tab":
|
|
case "tabshifted": {
|
|
focusUrlBar =
|
|
!loadInBackground &&
|
|
w.isBlankPageURL(url) &&
|
|
!lazy.AboutNewTab.willNotifyUser;
|
|
|
|
let tabUsedForLoad = w.gBrowser.addTab(url, {
|
|
referrerInfo: params.referrerInfo,
|
|
charset,
|
|
postData,
|
|
inBackground: loadInBackground,
|
|
allowThirdPartyFixup,
|
|
relatedToCurrent: params.relatedToCurrent,
|
|
skipAnimation: skipTabAnimation,
|
|
userContextId,
|
|
originPrincipal,
|
|
originStoragePrincipal,
|
|
triggeringPrincipal,
|
|
allowInheritPrincipal,
|
|
triggeringRemoteType,
|
|
policyContainer,
|
|
forceAllowDataURI,
|
|
focusUrlBar,
|
|
openerBrowser: params.openerBrowser,
|
|
fromExternal: params.fromExternal,
|
|
eventDetail: params.eventDetail,
|
|
globalHistoryOptions,
|
|
schemelessInput: params.schemelessInput,
|
|
hasValidUserGestureActivation,
|
|
textDirectiveUserActivation,
|
|
});
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Potentially trigger a URL bar telemetry bounce event when navigating
|
|
// away from a page using the browser chrome.
|
|
// We avoid triggering for URL bar initiated loads since this gets called
|
|
// right after a result is picked and the bounce event tracking is started.
|
|
// We instead check for potential URL bar initiated bounce events directly
|
|
// in the collector's startTrackingBounceEvent().
|
|
if (!params.initiatedByURLBar && targetBrowser) {
|
|
lazy.handleBounceEventTrigger(targetBrowser);
|
|
}
|
|
|
|
if (
|
|
!params.avoidBrowserFocus &&
|
|
!focusUrlBar &&
|
|
targetBrowser == w.gBrowser.selectedBrowser
|
|
) {
|
|
// Focus the content, but only if the browser used for the load is selected.
|
|
targetBrowser.focus();
|
|
}
|
|
},
|
|
/**
|
|
* Resolve the initial browser window to use for a load, based on ``where``.
|
|
*
|
|
* @param {string} where
|
|
* The target location for the load (e.g. "current", "tab", "window").
|
|
* @param {object} params
|
|
* The full params object passed to openLinkIn.
|
|
* @param {Window} win
|
|
* The reference window used as a fallback for getTargetWindow.
|
|
* @param {boolean} forceNonPrivate
|
|
* Whether to force choosing a non-private target window.
|
|
* @returns {Window}
|
|
* The browser window that should be used as the initial target.
|
|
*/
|
|
_resolveInitialTargetWindow(where, params, win, forceNonPrivate) {
|
|
if (where === "current" && params.targetBrowser) {
|
|
return params.targetBrowser.documentGlobal;
|
|
}
|
|
|
|
if (where === "tab" || where === "tabshifted") {
|
|
const target = this.getTargetWindow(win, {
|
|
skipPopups: true,
|
|
skipTaskbarTabs: true,
|
|
forceNonPrivate,
|
|
});
|
|
if (win.top !== target) {
|
|
params.relatedToCurrent = false;
|
|
}
|
|
return target;
|
|
}
|
|
return this.getTargetWindow(win, { forceNonPrivate });
|
|
},
|
|
/**
|
|
* 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.skipTaskbarTabs - Require a non-taskbartab 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, skipTaskbarTabs, 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) &&
|
|
(!skipTaskbarTabs ||
|
|
!top.document.documentElement.hasAttribute("taskbartab")) &&
|
|
(!forceNonPrivate || !PrivateBrowsingUtils.isWindowPrivate(top))
|
|
) {
|
|
return top;
|
|
}
|
|
|
|
return lazy.BrowserWindowTracker.getTopWindow({
|
|
private: !forceNonPrivate && PrivateBrowsingUtils.isWindowPrivate(window),
|
|
allowPopups: !skipPopups,
|
|
allowTaskbarTabs: !skipTaskbarTabs,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* openUILink handles clicks on UI elements that cause URLs to load.
|
|
*
|
|
* @param {Window} window
|
|
* The window the load is initiated from.
|
|
* @param {string} url
|
|
* The URL to load.
|
|
* @param {Event | object} event
|
|
* The event that asked for the load, or a JSON object
|
|
* representing one. Its modifiers decide where the URL opens.
|
|
* @param {boolean | object} aIgnoreButton
|
|
* Options for the load, as for openLinkIn, plus
|
|
* ``ignoreButton`` to ignore which mouse button was used and
|
|
* ``ignoreAlt`` to ignore the Alt modifier. The positional
|
|
* arguments below are the legacy form of this argument, and
|
|
* cannot carry the required triggering principal.
|
|
* @param {boolean} aIgnoreAlt
|
|
* As ``ignoreAlt`` above.
|
|
* @param {boolean} aAllowThirdPartyFixup
|
|
* As ``params.allowThirdPartyFixup`` for openLinkIn.
|
|
* @param {object} aPostData
|
|
* As ``params.postData`` for openLinkIn.
|
|
* @param {object} aReferrerInfo
|
|
* As ``params.referrerInfo`` for openLinkIn.
|
|
*/
|
|
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);
|
|
},
|
|
|
|
/**
|
|
* Opens the given URI using the SystemPrincipal as the triggeringPrincipal,
|
|
* unless a more specific principal is provided.
|
|
*
|
|
* @param {Window} window
|
|
* The window the load is initiated from.
|
|
* @param {string} url
|
|
* The URL to load.
|
|
* @param {string} where
|
|
* Where to open the URL, as for openLinkIn.
|
|
* @param {object} [params]
|
|
* Options for the load, as for openLinkIn, except that
|
|
* ``forceForeground`` defaults to true.
|
|
*/
|
|
openTrustedLinkIn(window, url, where, params = {}) {
|
|
if (!params.triggeringPrincipal) {
|
|
params.triggeringPrincipal =
|
|
Services.scriptSecurityManager.getSystemPrincipal();
|
|
}
|
|
|
|
params.forceForeground ??= true;
|
|
this.openLinkIn(window, url, where, params);
|
|
},
|
|
|
|
/**
|
|
* Opens the given URI using a NullPrincipal as the triggeringPrincipal,
|
|
* unless a more specific principal is provided. Throws if handed the system
|
|
* principal.
|
|
*
|
|
* @param {Window} window
|
|
* The window the load is initiated from.
|
|
* @param {string} url
|
|
* The URL to load.
|
|
* @param {string} where
|
|
* Where to open the URL, as for openLinkIn.
|
|
* @param {object} [params]
|
|
* Options for the load, as for openLinkIn, except that
|
|
* ``forceForeground`` defaults 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).
|
|
* 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;
|
|
},
|
|
/**
|
|
* Switch to a tab that has a given URI, and focuses its browser window.
|
|
* If a matching tab is in this window, it will be switched to. Otherwise, other
|
|
* windows will be searched.
|
|
*
|
|
* @param {Window} window
|
|
* The current window
|
|
* @param {nsIURI | string} aURI
|
|
* URI to search for
|
|
* @param {boolean} aOpenNew
|
|
* True to open a new tab and switch to it, if no existing tab is found.
|
|
* If no suitable window is found, a new one will be opened.
|
|
* @param {object} [aOpenParams]
|
|
* If switching to this URI results in us opening a tab, aOpenParams
|
|
* will be the parameter object that gets passed to openTrustedLinkIn. Please
|
|
* see the documentation for openTrustedLinkIn to see what parameters can be
|
|
* passed via this object. The four properties below are consumed here
|
|
* rather than forwarded.
|
|
* @param {string} [aOpenParams.ignoreFragment]
|
|
* ``"whenComparing"`` to exclude the fragment when comparing URIs,
|
|
* ``"whenComparingAndReplace"`` to also load the requested URI into the
|
|
* tab that was found.
|
|
* @param {boolean} [aOpenParams.ignoreQueryString]
|
|
* Exclude the query string when comparing URIs.
|
|
* @param {boolean} [aOpenParams.replaceQueryString]
|
|
* Exclude the query string when comparing URIs, and load the
|
|
* requested URI into the tab that was found.
|
|
* @param {boolean} [aOpenParams.adoptIntoActiveWindow]
|
|
* Adopt a tab found in another window into the current one.
|
|
* @param {number} [aUserContextId]
|
|
* If not null, will switch to the first found tab having the provided
|
|
* userContextId.
|
|
* @param {object} [aSplitView]
|
|
* If not null, will move the tab to the active split view instead of switching to tab
|
|
* @returns {boolean} True if an existing tab was found, false otherwise
|
|
*/
|
|
switchToTabHavingURI(
|
|
window,
|
|
aURI,
|
|
aOpenNew,
|
|
aOpenParams = {},
|
|
aUserContextId = null,
|
|
aSplitView = null
|
|
) {
|
|
// Certain URLs can be switched to irrespective of the source or destination
|
|
// window being in private browsing mode:
|
|
const kPrivateBrowsingURLs = new Set(["about:addons"]);
|
|
|
|
let ignoreFragment = aOpenParams.ignoreFragment;
|
|
let ignoreQueryString = aOpenParams.ignoreQueryString;
|
|
let replaceQueryString = aOpenParams.replaceQueryString;
|
|
let adoptIntoActiveWindow = aOpenParams.adoptIntoActiveWindow;
|
|
|
|
// These properties are only used by switchToTabHavingURI and should
|
|
// not be used as a parameter for the new load.
|
|
delete aOpenParams.ignoreFragment;
|
|
delete aOpenParams.ignoreQueryString;
|
|
delete aOpenParams.replaceQueryString;
|
|
delete aOpenParams.adoptIntoActiveWindow;
|
|
|
|
let isBrowserWindow = !!window.gBrowser;
|
|
|
|
// This will switch to the tab in aWindow having aURI, if present.
|
|
function switchIfURIInWindow(aWindow) {
|
|
// We can switch tab only if if both the source and destination windows have
|
|
// the same private-browsing status.
|
|
if (
|
|
!kPrivateBrowsingURLs.has(aURI.spec) &&
|
|
PrivateBrowsingUtils.isWindowPrivate(window) !==
|
|
PrivateBrowsingUtils.isWindowPrivate(aWindow)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Remove the query string, fragment, both, or neither from a given url.
|
|
function cleanURL(url, removeQuery, removeFragment) {
|
|
let ret = url;
|
|
if (removeFragment) {
|
|
ret = ret.split("#")[0];
|
|
if (removeQuery) {
|
|
// This removes a query, if present before the fragment.
|
|
ret = ret.split("?")[0];
|
|
}
|
|
} else if (removeQuery) {
|
|
// This is needed in case there is a fragment after the query.
|
|
let fragment = ret.split("#")[1];
|
|
ret = ret
|
|
.split("?")[0]
|
|
.concat(fragment != undefined ? "#".concat(fragment) : "");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// Need to handle nsSimpleURIs here too (e.g. about:...), which don't
|
|
// work correctly with URL objects - so treat them as strings
|
|
let ignoreFragmentWhenComparing =
|
|
typeof ignoreFragment == "string" &&
|
|
ignoreFragment.startsWith("whenComparing");
|
|
let requestedCompare = cleanURL(
|
|
aURI.displaySpec,
|
|
ignoreQueryString || replaceQueryString,
|
|
ignoreFragmentWhenComparing
|
|
);
|
|
let browsers = aWindow.gBrowser.browsers;
|
|
for (let i = 0; i < browsers.length; i++) {
|
|
let browser = browsers[i];
|
|
let browserCompare = cleanURL(
|
|
browser.currentURI.displaySpec,
|
|
ignoreQueryString || replaceQueryString,
|
|
ignoreFragmentWhenComparing
|
|
);
|
|
let browserUserContextId = browser.getAttribute("usercontextid") || "";
|
|
if (aUserContextId != null && aUserContextId != browserUserContextId) {
|
|
continue;
|
|
}
|
|
if (requestedCompare == browserCompare) {
|
|
// If adoptIntoActiveWindow is set, and this is a cross-window switch,
|
|
// adopt the tab into the current window, after the active tab.
|
|
let doAdopt =
|
|
adoptIntoActiveWindow && isBrowserWindow && aWindow != window;
|
|
|
|
if (doAdopt) {
|
|
const newTab = window.gBrowser.adoptTab(
|
|
aWindow.gBrowser.getTabForBrowser(browser),
|
|
{
|
|
tabIndex: window.gBrowser.tabContainer.selectedIndex + 1,
|
|
selectTab: true,
|
|
}
|
|
);
|
|
if (!newTab) {
|
|
doAdopt = false;
|
|
}
|
|
}
|
|
if (!doAdopt) {
|
|
aWindow.focus();
|
|
}
|
|
|
|
if (
|
|
ignoreFragment == "whenComparingAndReplace" ||
|
|
replaceQueryString
|
|
) {
|
|
browser.loadURI(aURI, {
|
|
triggeringPrincipal:
|
|
aOpenParams.triggeringPrincipal ||
|
|
_createNullPrincipalFromTabUserContextId(),
|
|
});
|
|
}
|
|
|
|
if (!doAdopt) {
|
|
if (aSplitView) {
|
|
let tabToMove = aWindow.gBrowser.tabs[i];
|
|
if (aSplitView.tabs.includes(tabToMove)) {
|
|
aWindow.gBrowser.selectedTab = tabToMove;
|
|
} else {
|
|
let tabToReplace = aSplitView.tabs.find(tab => tab.selected);
|
|
aSplitView.replaceTab(tabToReplace, tabToMove);
|
|
}
|
|
aSplitView.documentGlobal.focus();
|
|
} else {
|
|
aWindow.gBrowser.tabContainer.selectedIndex = i;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// This can be passed either nsIURI or a string.
|
|
if (!(aURI instanceof Ci.nsIURI)) {
|
|
aURI = Services.io.newURI(aURI);
|
|
}
|
|
|
|
// Prioritise this window.
|
|
if (isBrowserWindow && switchIfURIInWindow(window)) {
|
|
return true;
|
|
}
|
|
|
|
for (let browserWin of lazy.BrowserWindowTracker.orderedWindows) {
|
|
// Skip closed (but not yet destroyed) windows,
|
|
// and the current window (which was checked earlier).
|
|
if (browserWin.closed || browserWin == window) {
|
|
continue;
|
|
}
|
|
if (switchIfURIInWindow(browserWin)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// No opened tab has that url.
|
|
if (aOpenNew) {
|
|
if (aUserContextId != null) {
|
|
aOpenParams.userContextId = aUserContextId;
|
|
}
|
|
if (isBrowserWindow && window.gBrowser.selectedTab.isEmpty) {
|
|
this.openTrustedLinkIn(window, aURI.spec, "current", aOpenParams);
|
|
} else {
|
|
this.openTrustedLinkIn(window, aURI.spec, "tab", aOpenParams);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
};
|