diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/BrowserContentHandler.sys.mjs | 1309 |
1 files changed, 1309 insertions, 0 deletions
diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs new file mode 100644 index 0000000000..60ba0a7d32 --- /dev/null +++ b/browser/components/BrowserContentHandler.sys.mjs @@ -0,0 +1,1309 @@ +/* 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, { + FirstStartup: "resource://gre/modules/FirstStartup.sys.mjs", + HeadlessShell: "resource:///modules/HeadlessShell.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", + UpdatePing: "resource://gre/modules/UpdatePing.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", + HomePage: "resource:///modules/HomePage.jsm", + LaterRun: "resource:///modules/LaterRun.jsm", +}); +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"], +}); + +XPCOMUtils.defineLazyGetter(lazy, "gSystemPrincipal", () => + Services.scriptSecurityManager.getSystemPrincipal() +); + +XPCOMUtils.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) { + 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)) { + 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 + ); + if (privateWindowParam) { + 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 { + 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)) { + lazy.ShellService.setDefaultBrowser(true, true); + } + + 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 += + " --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.enabled = true; + 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); + } + + 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); + } + 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 isDefault = false; + try { + var url = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "win10-default-browser"; + if (urlParam == url) { + isDefault = lazy.ShellService.isDefaultBrowser(false, false); + } + } catch (ex) {} + if (isDefault) { + // Firefox is already the default HTTP handler. + // We don't have to show the instruction page. + 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 (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; + } + + let alertService = lazy.gWindowsAlertsService; + if (!alertService) { + console.error("Windows alert service not available."); + break; + } + + async function handleNotification() { + const { launchUrl, privilegedName } = + await alertService.handleWindowsTag(tag); + + // If `launchUrl` or `privilegedName` are provided, then the + // notification was from a prior instance of the application and we + // need to handled fallback behavior. + if (launchUrl || privilegedName) { + console.info( + `Completing Windows notification (tag=${JSON.stringify( + tag + )}, launchUrl=${JSON.stringify( + launchUrl + )}, privilegedName=${JSON.stringify(privilegedName)}))` + ); + } + + if (privilegedName) { + Services.telemetry.setEventRecordingEnabled( + "browser.launched_to_handle", + true + ); + Glean.browserLaunchedToHandle.systemNotification.record({ + name: privilegedName, + }); + } + + if (launchUrl) { + let uri = resolveURIInternal(cmdLine, 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. + openBrowserWindow(cmdLine, lazy.gSystemPrincipal); + } + } + + // 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") { + // If we don't have a profile selected yet (e.g. the Profile Manager is + // displayed) we will crash if we open an url and then select a profile. 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 + "\n" + ); + // 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 + + "\n" + ); + } + } + } + + 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.isPlatformAndVersionAtLeast("win", "10") && + 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: "", +}; |