fune/browser/components/BrowserContentHandler.sys.mjs
Michael Hughes be184998f5 Bug 1863980 - [msix] Make ShellService.setDefaultBrowser async. r=nalexander,mconley,dmose,omc-reviewers
This commit includes the changes to call into the new asynchronous code to set the browser as default, using powershell.

While here, we add more logging to this interaction to allow easier
debugging with QA.

Differential Revision: https://phabricator.services.mozilla.com/D194949
2023-12-01 20:07:08 +00:00

1419 lines
48 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
FirstStartup: "resource://gre/modules/FirstStartup.sys.mjs",
HeadlessShell: "resource:///modules/HeadlessShell.sys.mjs",
HomePage: "resource:///modules/HomePage.sys.mjs",
LaterRun: "resource:///modules/LaterRun.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
ShellService: "resource:///modules/ShellService.sys.mjs",
SpecialMessageActions:
"resource://messaging-system/lib/SpecialMessageActions.sys.mjs",
UpdatePing: "resource://gre/modules/UpdatePing.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetters(lazy, {
UpdateManager: ["@mozilla.org/updates/update-manager;1", "nsIUpdateManager"],
WinTaskbar: ["@mozilla.org/windows-taskbar;1", "nsIWinTaskbar"],
WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils"],
});
ChromeUtils.defineLazyGetter(lazy, "gSystemPrincipal", () =>
Services.scriptSecurityManager.getSystemPrincipal()
);
ChromeUtils.defineLazyGetter(lazy, "gWindowsAlertsService", () => {
// We might not have the Windows alerts service: e.g., on Windows 7 and Windows 8.
if (!("nsIWindowsAlertsService" in Ci)) {
return null;
}
return Cc["@mozilla.org/system-alerts-service;1"]
?.getService(Ci.nsIAlertsService)
?.QueryInterface(Ci.nsIWindowsAlertsService);
});
// One-time startup homepage override configurations
const ONCE_DOMAINS = ["mozilla.org", "firefox.com"];
const ONCE_PREF = "browser.startup.homepage_override.once";
// Index of Private Browsing icon in firefox.exe
// Must line up with the one in nsNativeAppSupportWin.h.
const PRIVATE_BROWSING_ICON_INDEX = 5;
function shouldLoadURI(aURI) {
if (aURI && !aURI.schemeIs("chrome")) {
return true;
}
dump("*** Preventing external load of chrome: URI into browser window\n");
dump(" Use --chrome <uri> instead\n");
return false;
}
function resolveURIInternal(aCmdLine, aArgument) {
// If using Firefox protocol handler remove it from URI
// at this stage. This is before we would otherwise
// record telemetry so do that here.
if (aArgument.startsWith("firefox:")) {
aArgument = aArgument.substring("firefox:".length);
Services.telemetry.keyedScalarAdd(
"os.environment.launched_to_handle",
"firefox",
1
);
}
if (aArgument.startsWith("firefox-private:")) {
aArgument = aArgument.substring("firefox-private:".length);
Services.telemetry.keyedScalarAdd(
"os.environment.launched_to_handle",
"firefox-private",
1
);
}
var uri = aCmdLine.resolveURI(aArgument);
var uriFixup = Services.uriFixup;
if (!(uri instanceof Ci.nsIFileURL)) {
return Services.uriFixup.getFixupURIInfo(
aArgument,
uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS
).preferredURI;
}
try {
if (uri.file.exists()) {
return uri;
}
} catch (e) {
console.error(e);
}
// We have interpreted the argument as a relative file URI, but the file
// doesn't exist. Try URI fixup heuristics: see bug 290782.
try {
uri = Services.uriFixup.getFixupURIInfo(aArgument).preferredURI;
} catch (e) {
console.error(e);
}
return uri;
}
let gKiosk = false;
let gMajorUpgrade = false;
let gFirstRunProfile = false;
var gFirstWindow = false;
const OVERRIDE_NONE = 0;
const OVERRIDE_NEW_PROFILE = 1;
const OVERRIDE_NEW_MSTONE = 2;
const OVERRIDE_NEW_BUILD_ID = 3;
/**
* Determines whether a home page override is needed.
* Returns:
* OVERRIDE_NEW_PROFILE if this is the first run with a new profile.
* OVERRIDE_NEW_MSTONE if this is the first run with a build with a different
* Gecko milestone (i.e. right after an upgrade).
* OVERRIDE_NEW_BUILD_ID if this is the first run with a new build ID of the
* same Gecko milestone (i.e. after a nightly upgrade).
* OVERRIDE_NONE otherwise.
*/
function needHomepageOverride(prefb) {
var savedmstone = prefb.getCharPref(
"browser.startup.homepage_override.mstone",
""
);
if (savedmstone == "ignore") {
return OVERRIDE_NONE;
}
var mstone = Services.appinfo.platformVersion;
var savedBuildID = prefb.getCharPref(
"browser.startup.homepage_override.buildID",
""
);
var buildID = Services.appinfo.platformBuildID;
if (mstone != savedmstone) {
// Bug 462254. Previous releases had a default pref to suppress the EULA
// agreement if the platform's installer had already shown one. Now with
// about:rights we've removed the EULA stuff and default pref, but we need
// a way to make existing profiles retain the default that we removed.
if (savedmstone) {
prefb.setBoolPref("browser.rights.3.shown", true);
// Remember that we saw a major version change.
gMajorUpgrade = true;
}
prefb.setCharPref("browser.startup.homepage_override.mstone", mstone);
prefb.setCharPref("browser.startup.homepage_override.buildID", buildID);
return savedmstone ? OVERRIDE_NEW_MSTONE : OVERRIDE_NEW_PROFILE;
}
if (buildID != savedBuildID) {
prefb.setCharPref("browser.startup.homepage_override.buildID", buildID);
return OVERRIDE_NEW_BUILD_ID;
}
return OVERRIDE_NONE;
}
/**
* Gets the override page for the first run after the application has been
* updated.
* @param update
* The nsIUpdate for the update that has been applied.
* @param defaultOverridePage
* The default override page.
* @return The override page.
*/
function getPostUpdateOverridePage(update, defaultOverridePage) {
update = update.QueryInterface(Ci.nsIWritablePropertyBag);
let actions = update.getProperty("actions");
// When the update doesn't specify actions fallback to the original behavior
// of displaying the default override page.
if (!actions) {
return defaultOverridePage;
}
// The existence of silent or the non-existence of showURL in the actions both
// mean that an override page should not be displayed.
if (actions.includes("silent") || !actions.includes("showURL")) {
return "";
}
// If a policy was set to not allow the update.xml-provided
// URL to be used, use the default fallback (which will also
// be provided by the policy).
if (!Services.policies.isAllowed("postUpdateCustomPage")) {
return defaultOverridePage;
}
return update.getProperty("openURL") || defaultOverridePage;
}
/**
* Open a browser window. If this is the initial launch, this function will
* attempt to use the navigator:blank window opened by BrowserGlue.sys.mjs during
* early startup.
*
* @param cmdLine
* The nsICommandLine object given to nsICommandLineHandler's handle
* method.
* Used to check if we are processing the command line for the initial launch.
* @param triggeringPrincipal
* The nsIPrincipal to use as triggering principal for the page load(s).
* @param urlOrUrlList (optional)
* When omitted, the browser window will be opened with the default
* arguments, which will usually load the homepage.
* This can be a JS array of urls provided as strings, each url will be
* loaded in a tab. postData will be ignored in this case.
* This can be a single url to load in the new window, provided as a string.
* postData will be used in this case if provided.
* @param postData (optional)
* An nsIInputStream object to use as POST data when loading the provided
* url, or null.
* @param forcePrivate (optional)
* Boolean. If set to true, the new window will be a private browsing one.
*
* @returns {ChromeWindow}
* Returns the top level window opened.
*/
function openBrowserWindow(
cmdLine,
triggeringPrincipal,
urlOrUrlList,
postData = null,
forcePrivate = false
) {
const isStartup =
cmdLine && cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH;
let args;
if (!urlOrUrlList) {
// Just pass in the defaultArgs directly. We'll use system principal on the other end.
args = [gBrowserContentHandler.getArgs(isStartup)];
} else if (Array.isArray(urlOrUrlList)) {
// There isn't an explicit way to pass a principal here, so we load multiple URLs
// with system principal when we get to actually loading them.
if (
!triggeringPrincipal ||
!triggeringPrincipal.equals(lazy.gSystemPrincipal)
) {
throw new Error(
"Can't open multiple URLs with something other than system principal."
);
}
// Passing an nsIArray for the url disables the "|"-splitting behavior.
let uriArray = Cc["@mozilla.org/array;1"].createInstance(
Ci.nsIMutableArray
);
urlOrUrlList.forEach(function (uri) {
var sstring = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
sstring.data = uri;
uriArray.appendElement(sstring);
});
args = [uriArray];
} else {
let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
Ci.nsIWritablePropertyBag2
);
extraOptions.setPropertyAsBool("fromExternal", true);
// Always pass at least 3 arguments to avoid the "|"-splitting behavior,
// ie. avoid the loadOneOrMoreURIs function.
// Also, we need to pass the triggering principal.
args = [
urlOrUrlList,
extraOptions,
null, // refererInfo
postData,
undefined, // allowThirdPartyFixup; this would be `false` but that
// needs a conversion. Hopefully bug 1485961 will fix.
undefined, // user context id
null, // origin principal
null, // origin storage principal
triggeringPrincipal,
];
}
if (isStartup) {
let win = Services.wm.getMostRecentWindow("navigator:blank");
if (win) {
// Remove the windowtype of our blank window so that we don't close it
// later on when seeing cmdLine.preventDefault is true.
win.document.documentElement.removeAttribute("windowtype");
if (forcePrivate) {
win.docShell.QueryInterface(
Ci.nsILoadContext
).usePrivateBrowsing = true;
if (
AppConstants.platform == "win" &&
lazy.NimbusFeatures.majorRelease2022.getVariable(
"feltPrivacyWindowSeparation"
)
) {
lazy.WinTaskbar.setGroupIdForWindow(
win,
lazy.WinTaskbar.defaultPrivateGroupId
);
lazy.WindowsUIUtils.setWindowIconFromExe(
win,
Services.dirsvc.get("XREExeF", Ci.nsIFile).path,
// This corresponds to the definitions in
// nsNativeAppSupportWin.h
PRIVATE_BROWSING_ICON_INDEX
);
}
}
let openTime = win.openTime;
win.location = AppConstants.BROWSER_CHROME_URL;
win.arguments = args; // <-- needs to be a plain JS array here.
ChromeUtils.addProfilerMarker("earlyBlankWindowVisible", openTime);
lazy.BrowserWindowTracker.registerOpeningWindow(win, forcePrivate);
return win;
}
}
// We can't provide arguments to openWindow as a JS array.
if (!urlOrUrlList) {
// If we have a single string guaranteed to not contain '|' we can simply
// wrap it in an nsISupportsString object.
let [url] = args;
args = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
args.data = url;
} else {
// Otherwise, pass an nsIArray.
if (args.length > 1) {
let string = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
string.data = args[0];
args[0] = string;
}
let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
args.forEach(a => {
array.appendElement(a);
});
args = array;
}
return lazy.BrowserWindowTracker.openWindow({
args,
features: gBrowserContentHandler.getFeatures(cmdLine),
private: forcePrivate,
});
}
function openPreferences(cmdLine, extraArgs) {
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, "about:preferences");
}
async function doSearch(searchTerm, cmdLine) {
// XXXbsmedberg: use handURIToExistingBrowser to obey tabbed-browsing
// preferences, but need nsIBrowserDOMWindow extensions
// Open the window immediately as BrowserContentHandler needs to
// be handled synchronously. Then load the search URI when the
// SearchService has loaded.
let win = openBrowserWindow(cmdLine, lazy.gSystemPrincipal, "about:blank");
await new Promise(resolve => {
Services.obs.addObserver(function observe(subject) {
if (subject == win) {
Services.obs.removeObserver(
observe,
"browser-delayed-startup-finished"
);
resolve();
}
}, "browser-delayed-startup-finished");
});
win.BrowserSearch.loadSearchFromCommandLine(
searchTerm,
lazy.PrivateBrowsingUtils.isInTemporaryAutoStartMode ||
lazy.PrivateBrowsingUtils.isWindowPrivate(win),
lazy.gSystemPrincipal,
win.gBrowser.selectedBrowser.csp
).catch(console.error);
}
export function nsBrowserContentHandler() {
if (!gBrowserContentHandler) {
gBrowserContentHandler = this;
}
return gBrowserContentHandler;
}
nsBrowserContentHandler.prototype = {
/* nsISupports */
QueryInterface: ChromeUtils.generateQI([
"nsICommandLineHandler",
"nsIBrowserHandler",
"nsIContentHandler",
"nsICommandLineValidator",
]),
/* nsICommandLineHandler */
handle: function bch_handle(cmdLine) {
if (
cmdLine.handleFlag("kiosk", false) ||
cmdLine.handleFlagWithParam("kiosk-monitor", false)
) {
gKiosk = true;
}
if (cmdLine.handleFlag("disable-pinch", false)) {
let defaults = Services.prefs.getDefaultBranch(null);
defaults.setBoolPref("apz.allow_zooming", false);
Services.prefs.lockPref("apz.allow_zooming");
defaults.setCharPref("browser.gesture.pinch.in", "");
Services.prefs.lockPref("browser.gesture.pinch.in");
defaults.setCharPref("browser.gesture.pinch.in.shift", "");
Services.prefs.lockPref("browser.gesture.pinch.in.shift");
defaults.setCharPref("browser.gesture.pinch.out", "");
Services.prefs.lockPref("browser.gesture.pinch.out");
defaults.setCharPref("browser.gesture.pinch.out.shift", "");
Services.prefs.lockPref("browser.gesture.pinch.out.shift");
}
if (cmdLine.handleFlag("browser", false)) {
openBrowserWindow(cmdLine, lazy.gSystemPrincipal);
cmdLine.preventDefault = true;
}
// In the past, when an instance was not already running, the -remote
// option returned an error code. Any script or application invoking the
// -remote option is expected to be handling this case, otherwise they
// wouldn't be doing anything when there is no Firefox already running.
// Making the -remote option always return an error code makes those
// scripts or applications handle the situation as if Firefox was not
// already running.
if (cmdLine.handleFlag("remote", true)) {
throw Components.Exception("", Cr.NS_ERROR_ABORT);
}
var uriparam;
try {
while ((uriparam = cmdLine.handleFlagWithParam("new-window", false))) {
let uri = resolveURIInternal(cmdLine, uriparam);
if (!shouldLoadURI(uri)) {
continue;
}
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, uri.spec);
cmdLine.preventDefault = true;
}
} catch (e) {
console.error(e);
}
try {
while ((uriparam = cmdLine.handleFlagWithParam("new-tab", false))) {
let uri = resolveURIInternal(cmdLine, uriparam);
handURIToExistingBrowser(
uri,
Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
cmdLine,
false,
lazy.gSystemPrincipal
);
cmdLine.preventDefault = true;
}
} catch (e) {
console.error(e);
}
var chromeParam = cmdLine.handleFlagWithParam("chrome", false);
if (chromeParam) {
// Handle old preference dialog URLs.
if (
chromeParam == "chrome://browser/content/pref/pref.xul" ||
chromeParam == "chrome://browser/content/preferences/preferences.xul"
) {
openPreferences(cmdLine);
cmdLine.preventDefault = true;
} else {
try {
let resolvedURI = resolveURIInternal(cmdLine, chromeParam);
let isLocal = uri => {
let localSchemes = new Set(["chrome", "file", "resource"]);
if (uri instanceof Ci.nsINestedURI) {
uri = uri.QueryInterface(Ci.nsINestedURI).innerMostURI;
}
return localSchemes.has(uri.scheme);
};
if (isLocal(resolvedURI)) {
// If the URI is local, we are sure it won't wrongly inherit chrome privs
let features = "chrome,dialog=no,all" + this.getFeatures(cmdLine);
// Provide 1 null argument, as openWindow has a different behavior
// when the arg count is 0.
let argArray = Cc["@mozilla.org/array;1"].createInstance(
Ci.nsIMutableArray
);
argArray.appendElement(null);
Services.ww.openWindow(
null,
resolvedURI.spec,
"_blank",
features,
argArray
);
cmdLine.preventDefault = true;
} else {
dump("*** Preventing load of web URI as chrome\n");
dump(
" If you're trying to load a webpage, do not pass --chrome.\n"
);
}
} catch (e) {
console.error(e);
}
}
}
if (cmdLine.handleFlag("preferences", false)) {
openPreferences(cmdLine);
cmdLine.preventDefault = true;
}
if (cmdLine.handleFlag("silent", false)) {
cmdLine.preventDefault = true;
}
try {
var privateWindowParam = cmdLine.handleFlagWithParam(
"private-window",
false
);
// Check for Firefox private browsing protocol handler here.
let url = null;
let urlFlagIdx = cmdLine.findFlag("url", false);
if (urlFlagIdx > -1 && cmdLine.length > 1) {
url = cmdLine.getArgument(urlFlagIdx + 1);
}
if (privateWindowParam || url?.startsWith("firefox-private:")) {
let forcePrivate = true;
let resolvedURI;
if (!lazy.PrivateBrowsingUtils.enabled) {
// Load about:privatebrowsing in a normal tab, which will display an error indicating
// access to private browsing has been disabled.
forcePrivate = false;
resolvedURI = Services.io.newURI("about:privatebrowsing");
} else if (url?.startsWith("firefox-private:")) {
// We can safely remove the flag and parameter now.
cmdLine.removeArguments(urlFlagIdx, urlFlagIdx + 1);
resolvedURI = resolveURIInternal(cmdLine, url);
} else {
resolvedURI = resolveURIInternal(cmdLine, privateWindowParam);
}
handURIToExistingBrowser(
resolvedURI,
Ci.nsIBrowserDOMWindow.OPEN_NEWTAB,
cmdLine,
forcePrivate,
lazy.gSystemPrincipal
);
cmdLine.preventDefault = true;
}
} catch (e) {
if (e.result != Cr.NS_ERROR_INVALID_ARG) {
throw e;
}
// NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param.
if (cmdLine.handleFlag("private-window", false)) {
openBrowserWindow(
cmdLine,
lazy.gSystemPrincipal,
"about:privatebrowsing",
null,
lazy.PrivateBrowsingUtils.enabled
);
cmdLine.preventDefault = true;
}
}
var searchParam = cmdLine.handleFlagWithParam("search", false);
if (searchParam) {
doSearch(searchParam, cmdLine);
cmdLine.preventDefault = true;
}
// The global PB Service consumes this flag, so only eat it in per-window
// PB builds.
if (
cmdLine.handleFlag("private", false) &&
lazy.PrivateBrowsingUtils.enabled
) {
lazy.PrivateBrowsingUtils.enterTemporaryAutoStartMode();
if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
let win = Services.wm.getMostRecentWindow("navigator:blank");
if (win) {
win.docShell.QueryInterface(
Ci.nsILoadContext
).usePrivateBrowsing = true;
}
}
}
if (cmdLine.handleFlag("setDefaultBrowser", false)) {
// Note that setDefaultBrowser is an async function, but "handle" (the method being executed)
// is an implementation of an interface method and changing it to be async would be complicated
// and ultimately nothing here needs the result of setDefaultBrowser, so we do not bother doing
// an await.
lazy.ShellService.setDefaultBrowser(true).catch(e => {
console.error("setDefaultBrowser failed:", e);
});
}
if (cmdLine.handleFlag("first-startup", false)) {
lazy.FirstStartup.init();
}
var fileParam = cmdLine.handleFlagWithParam("file", false);
if (fileParam) {
var file = cmdLine.resolveFile(fileParam);
var fileURI = Services.io.newFileURI(file);
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, fileURI.spec);
cmdLine.preventDefault = true;
}
if (AppConstants.platform == "win") {
// Handle "? searchterm" for Windows Vista start menu integration
for (var i = cmdLine.length - 1; i >= 0; --i) {
var param = cmdLine.getArgument(i);
if (param.match(/^\? /)) {
cmdLine.removeArguments(i, i);
cmdLine.preventDefault = true;
searchParam = param.substr(2);
doSearch(searchParam, cmdLine);
}
}
}
},
get helpInfo() {
let info =
" --browser Open a browser window.\n" +
" --new-window <url> Open <url> in a new window.\n" +
" --new-tab <url> Open <url> in a new tab.\n" +
" --private-window <url> Open <url> in a new private window.\n";
if (AppConstants.platform == "win") {
info += " --preferences Open Options dialog.\n";
} else {
info += " --preferences Open Preferences dialog.\n";
}
info +=
" --screenshot [<path>] Save screenshot to <path> or in working directory.\n";
info +=
" --window-size width[,height] Width and optionally height of screenshot.\n";
info +=
" --search <term> Search <term> with your default search engine.\n";
info += " --setDefaultBrowser Set this app as the default browser.\n";
info +=
" --first-startup Run post-install actions before opening a new window.\n";
info += " --kiosk Start the browser in kiosk mode.\n";
info +=
" --kiosk-monitor <num> Place kiosk browser window on given monitor.\n";
info +=
" --disable-pinch Disable touch-screen and touch-pad pinch gestures.\n";
return info;
},
/* nsIBrowserHandler */
get defaultArgs() {
return this.getArgs();
},
getArgs(isStartup = false) {
var prefb = Services.prefs;
if (!gFirstWindow) {
gFirstWindow = true;
if (lazy.PrivateBrowsingUtils.isInTemporaryAutoStartMode) {
return "about:privatebrowsing";
}
}
var override;
var overridePage = "";
var additionalPage = "";
var willRestoreSession = false;
try {
// Read the old value of homepage_override.mstone before
// needHomepageOverride updates it, so that we can later add it to the
// URL if we do end up showing an overridePage. This makes it possible
// to have the overridePage's content vary depending on the version we're
// upgrading from.
let old_mstone = Services.prefs.getCharPref(
"browser.startup.homepage_override.mstone",
"unknown"
);
let old_buildId = Services.prefs.getCharPref(
"browser.startup.homepage_override.buildID",
"unknown"
);
override = needHomepageOverride(prefb);
if (override != OVERRIDE_NONE) {
switch (override) {
case OVERRIDE_NEW_PROFILE:
// New profile.
gFirstRunProfile = true;
if (lazy.NimbusFeatures.aboutwelcome.getVariable("showModal")) {
break;
}
overridePage = Services.urlFormatter.formatURLPref(
"startup.homepage_welcome_url"
);
additionalPage = Services.urlFormatter.formatURLPref(
"startup.homepage_welcome_url.additional"
);
// Turn on 'later run' pages for new profiles.
lazy.LaterRun.enable(lazy.LaterRun.ENABLE_REASON_NEW_PROFILE);
break;
case OVERRIDE_NEW_MSTONE:
// Check whether we will restore a session. If we will, we assume
// that this is an "update" session. This does not take crashes
// into account because that requires waiting for the session file
// to be read. If a crash occurs after updating, before restarting,
// we may open the startPage in addition to restoring the session.
willRestoreSession =
lazy.SessionStartup.isAutomaticRestoreEnabled();
overridePage = Services.urlFormatter.formatURLPref(
"startup.homepage_override_url"
);
let update = lazy.UpdateManager.readyUpdate;
if (
update &&
Services.vc.compare(update.appVersion, old_mstone) > 0
) {
overridePage = getPostUpdateOverridePage(update, overridePage);
// Send the update ping to signal that the update was successful.
lazy.UpdatePing.handleUpdateSuccess(old_mstone, old_buildId);
lazy.LaterRun.enable(lazy.LaterRun.ENABLE_REASON_UPDATE_APPLIED);
}
overridePage = overridePage.replace("%OLD_VERSION%", old_mstone);
break;
case OVERRIDE_NEW_BUILD_ID:
if (lazy.UpdateManager.readyUpdate) {
// Send the update ping to signal that the update was successful.
lazy.UpdatePing.handleUpdateSuccess(old_mstone, old_buildId);
lazy.LaterRun.enable(lazy.LaterRun.ENABLE_REASON_UPDATE_APPLIED);
}
break;
}
}
} catch (ex) {}
// formatURLPref might return "about:blank" if getting the pref fails
if (overridePage == "about:blank") {
overridePage = "";
}
// Allow showing a one-time startup override if we're not showing one
if (isStartup && overridePage == "" && prefb.prefHasUserValue(ONCE_PREF)) {
try {
// Show if we haven't passed the expiration or there's no expiration
const { expire, url } = JSON.parse(
Services.urlFormatter.formatURLPref(ONCE_PREF)
);
if (!(Date.now() > expire)) {
// Only set allowed urls as override pages
overridePage = url
.split("|")
.map(val => {
try {
return new URL(val);
} catch (ex) {
// Invalid URL, so filter out below
console.error("Invalid once url:", ex);
return null;
}
})
.filter(
parsed =>
parsed &&
parsed.protocol == "https:" &&
// Only accept exact hostname or subdomain; without port
ONCE_DOMAINS.includes(
Services.eTLD.getBaseDomainFromHost(parsed.host)
)
)
.join("|");
// Be noisy as properly configured urls should be unchanged
if (overridePage != url) {
console.error(`Mismatched once urls: ${url}`);
}
}
} catch (ex) {
// Invalid json pref, so ignore (and clear below)
console.error("Invalid once pref:", ex);
} finally {
prefb.clearUserPref(ONCE_PREF);
}
}
if (!additionalPage) {
additionalPage = lazy.LaterRun.getURL() || "";
}
if (additionalPage && additionalPage != "about:blank") {
if (overridePage) {
overridePage += "|" + additionalPage;
} else {
overridePage = additionalPage;
}
}
var startPage = "";
try {
var choice = prefb.getIntPref("browser.startup.page");
if (choice == 1 || choice == 3) {
startPage = lazy.HomePage.get();
}
} catch (e) {
console.error(e);
}
if (startPage == "about:blank") {
startPage = "";
}
let skipStartPage =
override == OVERRIDE_NEW_PROFILE &&
prefb.getBoolPref("browser.startup.firstrunSkipsHomepage");
// Only show the startPage if we're not restoring an update session and are
// not set to skip the start page on this profile
if (overridePage && startPage && !willRestoreSession && !skipStartPage) {
return overridePage + "|" + startPage;
}
return overridePage || startPage || "about:blank";
},
mFeatures: null,
getFeatures: function bch_features(cmdLine) {
if (this.mFeatures === null) {
this.mFeatures = "";
if (cmdLine) {
try {
var width = cmdLine.handleFlagWithParam("width", false);
var height = cmdLine.handleFlagWithParam("height", false);
var left = cmdLine.handleFlagWithParam("left", false);
var top = cmdLine.handleFlagWithParam("top", false);
if (width) {
this.mFeatures += ",width=" + width;
}
if (height) {
this.mFeatures += ",height=" + height;
}
if (left) {
this.mFeatures += ",left=" + left;
}
if (top) {
this.mFeatures += ",top=" + top;
}
} catch (e) {}
}
// The global PB Service consumes this flag, so only eat it in per-window
// PB builds.
if (lazy.PrivateBrowsingUtils.isInTemporaryAutoStartMode) {
this.mFeatures += ",private";
}
if (
Services.prefs.getBoolPref("browser.suppress_first_window_animation") &&
!Services.wm.getMostRecentWindow("navigator:browser")
) {
this.mFeatures += ",suppressanimation";
}
}
return this.mFeatures;
},
get kiosk() {
return gKiosk;
},
get majorUpgrade() {
return gMajorUpgrade;
},
set majorUpgrade(val) {
gMajorUpgrade = val;
},
get firstRunProfile() {
return gFirstRunProfile;
},
set firstRunProfile(val) {
gFirstRunProfile = val;
},
/* nsIContentHandler */
handleContent: function bch_handleContent(contentType, context, request) {
const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001;
try {
var webNavInfo = Cc["@mozilla.org/webnavigation-info;1"].getService(
Ci.nsIWebNavigationInfo
);
if (!webNavInfo.isTypeSupported(contentType)) {
throw NS_ERROR_WONT_HANDLE_CONTENT;
}
} catch (e) {
throw NS_ERROR_WONT_HANDLE_CONTENT;
}
request.QueryInterface(Ci.nsIChannel);
handURIToExistingBrowser(
request.URI,
Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
null,
false,
request.loadInfo.triggeringPrincipal
);
request.cancel(Cr.NS_BINDING_ABORTED);
},
/* nsICommandLineValidator */
validate: function bch_validate(cmdLine) {
var urlFlagIdx = cmdLine.findFlag("url", false);
if (
urlFlagIdx > -1 &&
cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
) {
var urlParam = cmdLine.getArgument(urlFlagIdx + 1);
if (
cmdLine.length != urlFlagIdx + 2 ||
/firefoxurl(-[a-f0-9]+)?:/i.test(urlParam)
) {
throw Components.Exception("", Cr.NS_ERROR_ABORT);
}
}
},
};
var gBrowserContentHandler = new nsBrowserContentHandler();
function handURIToExistingBrowser(
uri,
location,
cmdLine,
forcePrivate,
triggeringPrincipal
) {
if (!shouldLoadURI(uri)) {
return;
}
let openInWindow = ({ browserDOMWindow }) => {
browserDOMWindow.openURI(
uri,
null,
location,
Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL,
triggeringPrincipal
);
};
// Unless using a private window is forced, open external links in private
// windows only if we're in perma-private mode.
let allowPrivate =
forcePrivate || lazy.PrivateBrowsingUtils.permanentPrivateBrowsing;
let navWin = lazy.BrowserWindowTracker.getTopWindow({
private: allowPrivate,
});
if (navWin) {
openInWindow(navWin);
return;
}
let pending = lazy.BrowserWindowTracker.getPendingWindow({
private: allowPrivate,
});
if (pending) {
// Note that we cannot make this function async as some callers rely on
// catching exceptions it can throw in some cases and some of those callers
// cannot be made async.
pending.then(openInWindow);
return;
}
// if we couldn't load it in an existing window, open a new one
openBrowserWindow(cmdLine, triggeringPrincipal, uri.spec, null, forcePrivate);
}
/**
* If given URI is a file type or a protocol, record telemetry that
* Firefox was invoked or launched (if `isLaunch` is truth-y). If the
* file type or protocol is not registered by default, record it as
* ".<other extension>" or "<other protocol>".
*
* @param uri
* The URI Firefox was asked to handle.
* @param isLaunch
* truth-y if Firefox was launched/started rather than running and invoked.
*/
function maybeRecordToHandleTelemetry(uri, isLaunch) {
let scalar = isLaunch
? "os.environment.launched_to_handle"
: "os.environment.invoked_to_handle";
if (uri instanceof Ci.nsIFileURL) {
let extension = "." + uri.fileExtension.toLowerCase();
// Keep synchronized with https://searchfox.org/mozilla-central/source/browser/installer/windows/nsis/shared.nsh
// and https://searchfox.org/mozilla-central/source/browser/installer/windows/msix/AppxManifest.xml.in.
let registeredExtensions = new Set([
".avif",
".htm",
".html",
".pdf",
".shtml",
".xht",
".xhtml",
".svg",
".webp",
]);
if (registeredExtensions.has(extension)) {
Services.telemetry.keyedScalarAdd(scalar, extension, 1);
} else {
Services.telemetry.keyedScalarAdd(scalar, ".<other extension>", 1);
}
} else if (uri) {
let scheme = uri.scheme.toLowerCase();
let registeredSchemes = new Set(["about", "http", "https", "mailto"]);
if (registeredSchemes.has(scheme)) {
Services.telemetry.keyedScalarAdd(scalar, scheme, 1);
} else {
Services.telemetry.keyedScalarAdd(scalar, "<other protocol>", 1);
}
}
}
export function nsDefaultCommandLineHandler() {}
nsDefaultCommandLineHandler.prototype = {
/* nsISupports */
QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]),
_haveProfile: false,
/* nsICommandLineHandler */
handle: function dch_handle(cmdLine) {
var urilist = [];
if (cmdLine && cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
// Since the purpose of this is to record early in startup,
// only record on launches, not already-running invocations.
Services.telemetry.setEventRecordingEnabled("telemetry", true);
Glean.fogValidation.validateEarlyEvent.record();
}
if (AppConstants.platform == "win") {
// Windows itself does disk I/O when the notification service is
// initialized, so make sure that is lazy.
while (true) {
let tag = cmdLine.handleFlagWithParam("notification-windowsTag", false);
if (!tag) {
break;
}
// All notifications will invoke Firefox with an action. Prior to Bug 1805514,
// this data was extracted from the Windows toast object directly (keyed by the
// notification ID) and not passed over the command line. This is acceptable
// because the data passed is chrome-controlled, but if we implement the `actions`
// part of the DOM Web Notifications API, this will no longer be true:
// content-controlled data might transit over the command line. This could lead
// to escaping bugs and overflows. In the future, we intend to avoid any such
// issue by once again extracting all such data from the Windows toast object.
let notificationData = cmdLine.handleFlagWithParam(
"notification-windowsAction",
false
);
if (!notificationData) {
break;
}
let alertService = lazy.gWindowsAlertsService;
if (!alertService) {
console.error("Windows alert service not available.");
break;
}
async function handleNotification() {
let { tagWasHandled } = await alertService.handleWindowsTag(tag);
// If the tag was not handled via callback, then the notification was
// from a prior instance of the application and we need to handle
// fallback behavior.
if (!tagWasHandled) {
console.info(
`Completing Windows notification (tag=${JSON.stringify(
tag
)}, notificationData=${notificationData})`
);
try {
notificationData = JSON.parse(notificationData);
} catch (e) {
console.error(
`Completing Windows notification (tag=${JSON.stringify(
tag
)}, failed to parse (notificationData=${notificationData})`
);
}
}
// This is awkward: the relaunch data set by the caller is _wrapped_
// into a compound object that includes additional notification data,
// and everything is exchanged as strings. Unwrap and parse here.
let opaqueRelaunchData = null;
if (notificationData?.opaqueRelaunchData) {
try {
opaqueRelaunchData = JSON.parse(
notificationData.opaqueRelaunchData
);
} catch (e) {
console.error(
`Completing Windows notification (tag=${JSON.stringify(
tag
)}, failed to parse (opaqueRelaunchData=${
notificationData.opaqueRelaunchData
})`
);
}
}
if (notificationData?.privilegedName) {
Services.telemetry.setEventRecordingEnabled(
"browser.launched_to_handle",
true
);
Glean.browserLaunchedToHandle.systemNotification.record({
name: notificationData.privilegedName,
});
}
// If we have an action in the notification data, this will be the
// window to perform the action in.
let winForAction;
if (notificationData?.launchUrl && !opaqueRelaunchData) {
// Unprivileged Web Notifications contain a launch URL and are handled
// slightly differently than privileged notifications with actions.
let uri = resolveURIInternal(cmdLine, notificationData.launchUrl);
if (cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
// Try to find an existing window and load our URI into the current
// tab, new tab, or new window as prefs determine.
try {
handURIToExistingBrowser(
uri,
Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
cmdLine,
false,
lazy.gSystemPrincipal
);
return;
} catch (e) {}
}
if (shouldLoadURI(uri)) {
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, [uri.spec]);
}
} else if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
// No URL provided, but notification was interacted with while the
// application was closed. Fall back to opening the browser without url.
winForAction = openBrowserWindow(cmdLine, lazy.gSystemPrincipal);
await new Promise(resolve => {
Services.obs.addObserver(function observe(subject) {
if (subject == winForAction) {
Services.obs.removeObserver(
observe,
"browser-delayed-startup-finished"
);
resolve();
}
}, "browser-delayed-startup-finished");
});
} else {
// Relaunch in private windows only if we're in perma-private mode.
let allowPrivate =
lazy.PrivateBrowsingUtils.permanentPrivateBrowsing;
winForAction = lazy.BrowserWindowTracker.getTopWindow({
private: allowPrivate,
});
}
if (opaqueRelaunchData && winForAction) {
// Without dispatch, `OPEN_URL` with `where: "tab"` does not work on relaunch.
Services.tm.dispatchToMainThread(() => {
lazy.SpecialMessageActions.handleAction(
opaqueRelaunchData,
winForAction.gBrowser
);
});
}
}
// Notification handling occurs asynchronously to prevent blocking the
// main thread. As a result we won't have the information we need to open
// a new tab in the case of notification fallback handling before
// returning. We call `enterLastWindowClosingSurvivalArea` to prevent
// the browser from exiting in case early blank window is pref'd off.
if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
Services.startup.enterLastWindowClosingSurvivalArea();
}
handleNotification()
.catch(e => {
console.error(
`Error handling Windows notification with tag '${tag}':`,
e
);
})
.finally(() => {
if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
Services.startup.exitLastWindowClosingSurvivalArea();
}
});
return;
}
}
if (
cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH &&
Services.startup.wasSilentlyStarted
) {
// If we are starting up in silent mode, don't open a window. We also need
// to make sure that the application doesn't immediately exit, so stay in
// a LastWindowClosingSurvivalArea until a window opens.
Services.startup.enterLastWindowClosingSurvivalArea();
Services.obs.addObserver(function windowOpenObserver() {
Services.startup.exitLastWindowClosingSurvivalArea();
Services.obs.removeObserver(windowOpenObserver, "domwindowopened");
}, "domwindowopened");
return;
}
if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
// Handle the case where we don't have a profile selected yet (e.g. the
// Profile Manager is displayed).
// On Windows, we will crash if we open an url and then select a profile.
// On macOS, if we open an url we don't experience a crash but a broken
// window is opened.
// To prevent this handle all url command line flags and set the
// command line's preventDefault to true to prevent the display of the ui.
// The initial command line will be retained when nsAppRunner calls
// LaunchChild though urls launched after the initial launch will be lost.
if (!this._haveProfile) {
try {
// This will throw when a profile has not been selected.
Services.dirsvc.get("ProfD", Ci.nsIFile);
this._haveProfile = true;
} catch (e) {
// eslint-disable-next-line no-empty
while ((ar = cmdLine.handleFlagWithParam("url", false))) {}
cmdLine.preventDefault = true;
}
}
}
// `-osint` and handling registered file types and protocols is Windows-only.
let launchedWithArg_osint =
AppConstants.platform == "win" && cmdLine.findFlag("osint", false) == 0;
if (launchedWithArg_osint) {
cmdLine.handleFlag("osint", false);
}
try {
var ar;
while ((ar = cmdLine.handleFlagWithParam("url", false))) {
var uri = resolveURIInternal(cmdLine, ar);
urilist.push(uri);
if (launchedWithArg_osint) {
launchedWithArg_osint = false;
// We use the resolved URI here, even though it can produce
// surprising results where-by `-osint -url test.pdf` resolves to
// a query with search parameter "test.pdf". But that shouldn't
// happen when Firefox is launched by Windows itself: files should
// exist and be resolved to file URLs.
const isLaunch =
cmdLine && cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH;
maybeRecordToHandleTelemetry(uri, isLaunch);
}
}
} catch (e) {
console.error(e);
}
if (
AppConstants.platform == "win" &&
cmdLine.handleFlag("to-handle-default-browser-agent", false)
) {
// The Default Browser Agent launches Firefox in response to a Windows
// native notification, but it does so in a non-standard manner.
Services.telemetry.setEventRecordingEnabled(
"browser.launched_to_handle",
true
);
Glean.browserLaunchedToHandle.systemNotification.record({
name: "default-browser-agent",
});
let thanksURI = Services.io.newURI(
Services.urlFormatter.formatURLPref(
"browser.shell.defaultBrowserAgent.thanksURL"
)
);
urilist.push(thanksURI);
}
if (cmdLine.findFlag("screenshot", true) != -1) {
lazy.HeadlessShell.handleCmdLineArgs(
cmdLine,
urilist.filter(shouldLoadURI).map(u => u.spec)
);
return;
}
for (let i = 0; i < cmdLine.length; ++i) {
var curarg = cmdLine.getArgument(i);
if (curarg.match(/^-/)) {
console.error("Warning: unrecognized command line flag", curarg);
// To emulate the pre-nsICommandLine behavior, we ignore
// the argument after an unrecognized flag.
++i;
} else {
try {
urilist.push(resolveURIInternal(cmdLine, curarg));
} catch (e) {
console.error(
`Error opening URI ${curarg} from the command line:`,
e
);
}
}
}
if (urilist.length) {
if (
cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH &&
urilist.length == 1
) {
// Try to find an existing window and load our URI into the
// current tab, new tab, or new window as prefs determine.
try {
handURIToExistingBrowser(
urilist[0],
Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW,
cmdLine,
false,
lazy.gSystemPrincipal
);
return;
} catch (e) {}
}
var URLlist = urilist.filter(shouldLoadURI).map(u => u.spec);
if (URLlist.length) {
openBrowserWindow(cmdLine, lazy.gSystemPrincipal, URLlist);
}
} else if (!cmdLine.preventDefault) {
if (
AppConstants.platform == "win" &&
cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH &&
lazy.WindowsUIUtils.inTabletMode
) {
// In windows 10 tablet mode, do not create a new window, but reuse the existing one.
let win = lazy.BrowserWindowTracker.getTopWindow();
if (win) {
win.focus();
return;
}
}
openBrowserWindow(cmdLine, lazy.gSystemPrincipal);
} else {
// Need a better solution in the future to avoid opening the blank window
// when command line parameters say we are not going to show a browser
// window, but for now the blank window getting closed quickly (and
// causing only a slight flicker) is better than leaving it open.
let win = Services.wm.getMostRecentWindow("navigator:blank");
if (win) {
win.close();
}
}
},
helpInfo: "",
};