BrowserContentHandler.sys.mjs (59369B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const lazy = {}; 9 10 ChromeUtils.defineESModuleGetters(lazy, { 11 AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", 12 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 13 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", 14 FirstStartup: "resource://gre/modules/FirstStartup.sys.mjs", 15 HeadlessShell: "moz-src:///browser/components/shell/HeadlessShell.sys.mjs", 16 HomePage: "resource:///modules/HomePage.sys.mjs", 17 LaterRun: "resource:///modules/LaterRun.sys.mjs", 18 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 19 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 20 SearchUIUtils: "moz-src:///browser/components/search/SearchUIUtils.sys.mjs", 21 SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs", 22 ShellService: "moz-src:///browser/components/shell/ShellService.sys.mjs", 23 SpecialMessageActions: 24 "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", 25 UpdatePing: "resource://gre/modules/UpdatePing.sys.mjs", 26 }); 27 28 XPCOMUtils.defineLazyServiceGetters(lazy, { 29 UpdateManager: ["@mozilla.org/updates/update-manager;1", Ci.nsIUpdateManager], 30 WinTaskbar: ["@mozilla.org/windows-taskbar;1", Ci.nsIWinTaskbar], 31 WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", Ci.nsIWindowsUIUtils], 32 }); 33 34 ChromeUtils.defineLazyGetter(lazy, "gSystemPrincipal", () => 35 Services.scriptSecurityManager.getSystemPrincipal() 36 ); 37 38 ChromeUtils.defineLazyGetter(lazy, "gWindowsAlertsService", () => { 39 // We might not have the Windows alerts service: e.g., on Windows 7 and Windows 8. 40 if (!("nsIWindowsAlertsService" in Ci)) { 41 return null; 42 } 43 return Cc["@mozilla.org/system-alerts-service;1"] 44 ?.getService(Ci.nsIAlertsService) 45 ?.QueryInterface(Ci.nsIWindowsAlertsService); 46 }); 47 48 const FORK_VERSION_PREF = 49 "browser.startup.homepage_override.torbrowser.version"; 50 51 // One-time startup homepage override configurations 52 const ONCE_DOMAINS = new Set(["mozilla.org", "firefox.com"]); 53 const ONCE_PREF = "browser.startup.homepage_override.once"; 54 55 // Index of Private Browsing icon in firefox.exe 56 // Must line up with the one in nsNativeAppSupportWin.h. 57 const PRIVATE_BROWSING_ICON_INDEX = 5; 58 59 function shouldLoadURI(aURI) { 60 if (aURI && !aURI.schemeIs("chrome")) { 61 return true; 62 } 63 64 dump("*** Preventing external load of chrome: URI into browser window\n"); 65 dump(" Use --chrome <uri> instead\n"); 66 return false; 67 } 68 69 function resolveURIInternal(aCmdLine, aArgument) { 70 let principal = lazy.gSystemPrincipal; 71 var uri = aCmdLine.resolveURI(aArgument); 72 var uriFixup = Services.uriFixup; 73 74 if (!(uri instanceof Ci.nsIFileURL)) { 75 let prefURI = Services.uriFixup.getFixupURIInfo( 76 aArgument, 77 uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS 78 ).preferredURI; 79 return { uri: prefURI, principal }; 80 } 81 82 try { 83 if (uri.file.exists()) { 84 return { uri, principal }; 85 } 86 } catch (e) { 87 console.error(e); 88 } 89 90 // We have interpreted the argument as a relative file URI, but the file 91 // doesn't exist. Try URI fixup heuristics: see bug 290782. 92 93 try { 94 uri = Services.uriFixup.getFixupURIInfo(aArgument).preferredURI; 95 } catch (e) { 96 console.error(e); 97 } 98 99 return { uri, principal }; 100 } 101 102 let gKiosk = false; 103 let gMajorUpgrade = false; 104 let gFirstRunProfile = false; 105 var gFirstWindow = false; 106 107 const OVERRIDE_NONE = 0; 108 const OVERRIDE_NEW_PROFILE = 1; 109 const OVERRIDE_NEW_MSTONE = 2; 110 const OVERRIDE_NEW_BUILD_ID = 3; 111 /** 112 * Determines whether a home page override is needed. 113 * 114 * @param {boolean} [updateMilestones=true] 115 * True if we should update the milestone prefs after comparing those prefs 116 * with the current platform version and build ID. 117 * 118 * If updateMilestones is false, then this function has no side-effects. 119 * 120 * @returns {number} 121 * One of the following constants: 122 * OVERRIDE_NEW_PROFILE 123 * if this is the first run with a new profile. 124 * OVERRIDE_NEW_MSTONE 125 * if this is the first run with a build with a different Gecko milestone 126 * or fork version (i.e. right after an upgrade). 127 * OVERRIDE_NEW_BUILD_ID 128 * if this is the first run with a new build ID of the same Gecko 129 * milestone (i.e. after a nightly upgrade). 130 * OVERRIDE_NONE 131 * otherwise. 132 */ 133 function needHomepageOverride(updateMilestones = true) { 134 var savedmstone = Services.prefs.getCharPref( 135 "browser.startup.homepage_override.mstone", 136 "" 137 ); 138 139 if (savedmstone == "ignore") { 140 return OVERRIDE_NONE; 141 } 142 143 var mstone = Services.appinfo.platformVersion; 144 145 var savedForkVersion = Services.prefs.getCharPref(FORK_VERSION_PREF, null); 146 147 var savedBuildID = Services.prefs.getCharPref( 148 "browser.startup.homepage_override.buildID", 149 "" 150 ); 151 152 var buildID = Services.appinfo.platformBuildID; 153 154 if (mstone != savedmstone) { 155 // Bug 462254. Previous releases had a default pref to suppress the EULA 156 // agreement if the platform's installer had already shown one. Now with 157 // about:rights we've removed the EULA stuff and default pref, but we need 158 // a way to make existing profiles retain the default that we removed. 159 if (savedmstone) { 160 Services.prefs.setBoolPref("browser.rights.3.shown", true); 161 162 // Remember that we saw a major version change. 163 gMajorUpgrade = true; 164 } 165 166 if (updateMilestones) { 167 Services.prefs.setCharPref( 168 "browser.startup.homepage_override.mstone", 169 mstone 170 ); 171 Services.prefs.setCharPref( 172 "browser.startup.homepage_override.buildID", 173 buildID 174 ); 175 Services.prefs.setCharPref( 176 FORK_VERSION_PREF, 177 AppConstants.BASE_BROWSER_VERSION 178 ); 179 } 180 return savedmstone ? OVERRIDE_NEW_MSTONE : OVERRIDE_NEW_PROFILE; 181 } 182 183 if (AppConstants.BASE_BROWSER_VERSION != savedForkVersion) { 184 if (updateMilestones) { 185 Services.prefs.setCharPref( 186 "browser.startup.homepage_override.buildID", 187 buildID 188 ); 189 Services.prefs.setCharPref( 190 FORK_VERSION_PREF, 191 AppConstants.BASE_BROWSER_VERSION 192 ); 193 } 194 return OVERRIDE_NEW_MSTONE; 195 } 196 197 if (buildID != savedBuildID) { 198 if (updateMilestones) { 199 Services.prefs.setCharPref( 200 "browser.startup.homepage_override.buildID", 201 buildID 202 ); 203 } 204 return OVERRIDE_NEW_BUILD_ID; 205 } 206 207 return OVERRIDE_NONE; 208 } 209 210 /** 211 * Gets the override page for the first run after the application has been 212 * updated. 213 * 214 * @param update 215 * The nsIUpdate for the update that has been applied. 216 * @param defaultOverridePage 217 * The default override page 218 * @param nimbusOverridePage 219 * Nimbus provided URL 220 * @param disableWnp 221 * Boolean, disables all WNPs if true 222 * @return The override page. 223 */ 224 function getPostUpdateOverridePage( 225 update, 226 defaultOverridePage, 227 nimbusOverridePage, 228 disableWnp 229 ) { 230 if (disableWnp) { 231 return ""; 232 } 233 234 update = update.QueryInterface(Ci.nsIWritablePropertyBag); 235 let actions = update.getProperty("actions"); 236 // When the update doesn't specify actions fallback to the original behavior 237 // of displaying the default override page. 238 if (!actions) { 239 return defaultOverridePage; 240 } 241 242 // The existence of silent or the non-existence of showURL in the actions both 243 // mean that an override page should not be displayed. 244 if (actions.includes("silent") || !actions.includes("showURL")) { 245 return ""; 246 } 247 248 // If a policy was set to not allow the update.xml-provided URL to be used, 249 // use the default fallback (which will also be provided by the policy). 250 if (!Services.policies.isAllowed("postUpdateCustomPage")) { 251 return defaultOverridePage; 252 } 253 254 if (nimbusOverridePage) { 255 return nimbusOverridePage; 256 } 257 258 return update.getProperty("openURL") || defaultOverridePage; 259 } 260 261 /** 262 * Open a browser window. If this is the initial launch, this function will 263 * attempt to use the navigator:blank window opened by BrowserGlue.sys.mjs during 264 * early startup. 265 * 266 * @param cmdLine 267 * The nsICommandLine object given to nsICommandLineHandler's handle 268 * method. 269 * Used to check if we are processing the command line for the initial launch. 270 * @param triggeringPrincipal 271 * The nsIPrincipal to use as triggering principal for the page load(s). 272 * @param urlOrUrlList (optional) 273 * When omitted, the browser window will be opened with the default 274 * arguments, which will usually load the homepage. 275 * This can be a JS array of urls provided as strings, each url will be 276 * loaded in a tab. postData will be ignored in this case. 277 * This can be a single url to load in the new window, provided as a string. 278 * postData will be used in this case if provided. 279 * @param postData (optional) 280 * An nsIInputStream object to use as POST data when loading the provided 281 * url, or null. 282 * @param forcePrivate (optional) 283 * Boolean. If set to true, the new window will be a private browsing one. 284 * 285 * @returns {ChromeWindow} 286 * Returns the top level window opened. 287 */ 288 function openBrowserWindow( 289 cmdLine, 290 triggeringPrincipal, 291 urlOrUrlList, 292 postData = null, 293 forcePrivate = false 294 ) { 295 const isStartup = 296 cmdLine && cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH; 297 298 let args; 299 if (!urlOrUrlList) { 300 // Just pass in the defaultArgs directly. We'll use system principal on the other end. 301 if (isStartup) { 302 args = [gBrowserContentHandler.getFirstWindowArgs()]; 303 } else { 304 args = [gBrowserContentHandler.getNewWindowArgs()]; 305 } 306 } else if (Array.isArray(urlOrUrlList)) { 307 // There isn't an explicit way to pass a principal here, so we load multiple URLs 308 // with system principal when we get to actually loading them. 309 if ( 310 !triggeringPrincipal || 311 !triggeringPrincipal.equals(lazy.gSystemPrincipal) 312 ) { 313 throw new Error( 314 "Can't open multiple URLs with something other than system principal." 315 ); 316 } 317 // Passing an nsIArray for the url disables the "|"-splitting behavior. 318 let uriArray = Cc["@mozilla.org/array;1"].createInstance( 319 Ci.nsIMutableArray 320 ); 321 urlOrUrlList.forEach(function (uri) { 322 var sstring = Cc["@mozilla.org/supports-string;1"].createInstance( 323 Ci.nsISupportsString 324 ); 325 sstring.data = uri; 326 uriArray.appendElement(sstring); 327 }); 328 args = [uriArray]; 329 } else { 330 let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( 331 Ci.nsIWritablePropertyBag2 332 ); 333 extraOptions.setPropertyAsBool("fromExternal", true); 334 335 // Always pass at least 3 arguments to avoid the "|"-splitting behavior, 336 // ie. avoid the loadOneOrMoreURIs function. 337 // Also, we need to pass the triggering principal. 338 args = [ 339 urlOrUrlList, 340 extraOptions, 341 null, // refererInfo 342 postData, 343 undefined, // allowThirdPartyFixup; this would be `false` but that 344 // needs a conversion. Hopefully bug 1485961 will fix. 345 undefined, // user context id 346 null, // origin principal 347 null, // origin storage principal 348 triggeringPrincipal, 349 ]; 350 } 351 352 if (isStartup) { 353 let win = gBrowserContentHandler.replaceStartupWindow(args, forcePrivate); 354 if (win) { 355 return win; 356 } 357 } 358 359 // We can't provide arguments to openWindow as a JS array. 360 if (!urlOrUrlList) { 361 // If we have a single string guaranteed to not contain '|' we can simply 362 // wrap it in an nsISupportsString object. 363 let [url] = args; 364 args = Cc["@mozilla.org/supports-string;1"].createInstance( 365 Ci.nsISupportsString 366 ); 367 args.data = url; 368 } else { 369 // Otherwise, pass an nsIArray. 370 if (args.length > 1) { 371 let string = Cc["@mozilla.org/supports-string;1"].createInstance( 372 Ci.nsISupportsString 373 ); 374 string.data = args[0]; 375 args[0] = string; 376 } 377 let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 378 args.forEach(a => { 379 array.appendElement(a); 380 }); 381 args = array; 382 } 383 384 return lazy.BrowserWindowTracker.openWindow({ 385 args, 386 features: gBrowserContentHandler.getFeatures(cmdLine), 387 private: forcePrivate, 388 }); 389 } 390 391 function openPreferences(cmdLine) { 392 openBrowserWindow(cmdLine, lazy.gSystemPrincipal, "about:preferences"); 393 } 394 395 async function doSearch(searchText, cmdLine) { 396 // XXXbsmedberg: use handURIToExistingBrowser to obey tabbed-browsing 397 // preferences, but need nsIBrowserDOMWindow extensions 398 // Open the window immediately as BrowserContentHandler needs to 399 // be handled synchronously. Then load the search URI when the 400 // SearchService has loaded. 401 let win = openBrowserWindow(cmdLine, lazy.gSystemPrincipal, "about:blank"); 402 await lazy.BrowserUtils.promiseObserved( 403 "browser-delayed-startup-finished", 404 subject => subject == win 405 ); 406 407 lazy.SearchUIUtils.loadSearch({ 408 window: win, 409 searchText, 410 usePrivateWindow: 411 lazy.PrivateBrowsingUtils.isInTemporaryAutoStartMode || 412 lazy.PrivateBrowsingUtils.isWindowPrivate(win), 413 triggeringPrincipal: lazy.gSystemPrincipal, 414 policyContainer: win.gBrowser.selectedBrowser.policyContainer, 415 sapSource: "system", 416 }).catch(console.error); 417 } 418 419 function spinForLastUpdateInstalled() { 420 return spinResolve(lazy.UpdateManager.lastUpdateInstalled()); 421 } 422 423 function spinForUpdateInstalledAtStartup() { 424 return spinResolve(lazy.UpdateManager.updateInstalledAtStartup()); 425 } 426 427 function spinResolve(promise) { 428 if (!(promise instanceof Promise)) { 429 return promise; 430 } 431 let done = false; 432 let result = null; 433 let error = null; 434 promise 435 .catch(e => { 436 error = e; 437 }) 438 .then(r => { 439 result = r; 440 done = true; 441 }); 442 443 Services.tm.spinEventLoopUntil( 444 "BrowserContentHandler.sys.mjs:BCH_spinResolve", 445 () => done 446 ); 447 if (!done) { 448 throw new Error("Forcefully exited event loop."); 449 } else if (error) { 450 throw error; 451 } else { 452 return result; 453 } 454 } 455 456 export function nsBrowserContentHandler() { 457 if (!gBrowserContentHandler) { 458 gBrowserContentHandler = this; 459 } 460 return gBrowserContentHandler; 461 } 462 463 nsBrowserContentHandler.prototype = { 464 /* nsISupports */ 465 QueryInterface: ChromeUtils.generateQI([ 466 "nsICommandLineHandler", 467 "nsIBrowserHandler", 468 "nsIContentHandler", 469 "nsICommandLineValidator", 470 ]), 471 472 /* nsICommandLineHandler */ 473 handle: function bch_handle(cmdLine) { 474 if ( 475 cmdLine.handleFlag("kiosk", false) || 476 cmdLine.handleFlagWithParam("kiosk-monitor", false) 477 ) { 478 gKiosk = true; 479 Glean.browserStartup.kioskMode.set(true); 480 } 481 if (cmdLine.handleFlag("disable-pinch", false)) { 482 let defaults = Services.prefs.getDefaultBranch(null); 483 defaults.setBoolPref("apz.allow_zooming", false); 484 Services.prefs.lockPref("apz.allow_zooming"); 485 defaults.setCharPref("browser.gesture.pinch.in", ""); 486 Services.prefs.lockPref("browser.gesture.pinch.in"); 487 defaults.setCharPref("browser.gesture.pinch.in.shift", ""); 488 Services.prefs.lockPref("browser.gesture.pinch.in.shift"); 489 defaults.setCharPref("browser.gesture.pinch.out", ""); 490 Services.prefs.lockPref("browser.gesture.pinch.out"); 491 defaults.setCharPref("browser.gesture.pinch.out.shift", ""); 492 Services.prefs.lockPref("browser.gesture.pinch.out.shift"); 493 } 494 if (cmdLine.handleFlag("browser", false)) { 495 openBrowserWindow(cmdLine, lazy.gSystemPrincipal); 496 cmdLine.preventDefault = true; 497 } 498 499 var uriparam; 500 try { 501 while ((uriparam = cmdLine.handleFlagWithParam("new-window", false))) { 502 let { uri, principal } = resolveURIInternal(cmdLine, uriparam); 503 if (!shouldLoadURI(uri)) { 504 continue; 505 } 506 openBrowserWindow(cmdLine, principal, uri.spec); 507 cmdLine.preventDefault = true; 508 } 509 } catch (e) { 510 console.error(e); 511 } 512 513 try { 514 while ((uriparam = cmdLine.handleFlagWithParam("new-tab", false))) { 515 let { uri, principal } = resolveURIInternal(cmdLine, uriparam); 516 handURIToExistingBrowser( 517 uri, 518 Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, 519 cmdLine, 520 false, 521 principal 522 ); 523 cmdLine.preventDefault = true; 524 } 525 } catch (e) { 526 console.error(e); 527 } 528 529 var chromeParam = cmdLine.handleFlagWithParam("chrome", false); 530 if (chromeParam) { 531 // Handle old preference dialog URLs. 532 if ( 533 chromeParam == "chrome://browser/content/pref/pref.xul" || 534 chromeParam == "chrome://browser/content/preferences/preferences.xul" 535 ) { 536 openPreferences(cmdLine); 537 cmdLine.preventDefault = true; 538 } else { 539 try { 540 let { uri: resolvedURI } = resolveURIInternal(cmdLine, chromeParam); 541 let isLocal = uri => { 542 let localSchemes = new Set(["chrome", "file", "resource"]); 543 if (uri instanceof Ci.nsINestedURI) { 544 uri = uri.QueryInterface(Ci.nsINestedURI).innerMostURI; 545 } 546 return localSchemes.has(uri.scheme); 547 }; 548 if (isLocal(resolvedURI)) { 549 // If the URI is local, we are sure it won't wrongly inherit chrome privs 550 let features = "chrome,dialog=no,all" + this.getFeatures(cmdLine); 551 // Provide 1 null argument, as openWindow has a different behavior 552 // when the arg count is 0. 553 let argArray = Cc["@mozilla.org/array;1"].createInstance( 554 Ci.nsIMutableArray 555 ); 556 argArray.appendElement(null); 557 Services.ww.openWindow( 558 null, 559 resolvedURI.spec, 560 "_blank", 561 features, 562 argArray 563 ); 564 cmdLine.preventDefault = true; 565 } else { 566 dump("*** Preventing load of web URI as chrome\n"); 567 dump( 568 " If you're trying to load a webpage, do not pass --chrome.\n" 569 ); 570 } 571 } catch (e) { 572 console.error(e); 573 } 574 } 575 } 576 if (cmdLine.handleFlag("preferences", false)) { 577 openPreferences(cmdLine); 578 cmdLine.preventDefault = true; 579 } 580 if (cmdLine.handleFlag("silent", false)) { 581 cmdLine.preventDefault = true; 582 } 583 584 try { 585 var privateWindowParam = cmdLine.handleFlagWithParam( 586 "private-window", 587 false 588 ); 589 if (privateWindowParam) { 590 let forcePrivate = true; 591 let resolvedInfo; 592 if (!lazy.PrivateBrowsingUtils.enabled) { 593 // Load about:privatebrowsing in a normal tab, which will display an error indicating 594 // access to private browsing has been disabled. 595 forcePrivate = false; 596 resolvedInfo = { 597 uri: Services.io.newURI("about:privatebrowsing"), 598 principal: lazy.gSystemPrincipal, 599 }; 600 } else { 601 resolvedInfo = resolveURIInternal(cmdLine, privateWindowParam); 602 } 603 handURIToExistingBrowser( 604 resolvedInfo.uri, 605 Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, 606 cmdLine, 607 forcePrivate, 608 resolvedInfo.principal 609 ); 610 cmdLine.preventDefault = true; 611 } 612 } catch (e) { 613 if (e.result != Cr.NS_ERROR_INVALID_ARG) { 614 throw e; 615 } 616 // NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param. 617 if (cmdLine.handleFlag("private-window", false)) { 618 openBrowserWindow( 619 cmdLine, 620 lazy.gSystemPrincipal, 621 "about:privatebrowsing", 622 null, 623 lazy.PrivateBrowsingUtils.enabled 624 ); 625 cmdLine.preventDefault = true; 626 } 627 } 628 629 var searchParam = cmdLine.handleFlagWithParam("search", false); 630 if (searchParam) { 631 doSearch(searchParam, cmdLine); 632 cmdLine.preventDefault = true; 633 } 634 635 // The global PB Service consumes this flag, so only eat it in per-window 636 // PB builds. 637 if ( 638 cmdLine.handleFlag("private", false) && 639 lazy.PrivateBrowsingUtils.enabled 640 ) { 641 lazy.PrivateBrowsingUtils.enterTemporaryAutoStartMode(); 642 if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { 643 let win = Services.wm.getMostRecentWindow("navigator:blank"); 644 if (win) { 645 win.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = 646 true; 647 } 648 } 649 } 650 if (cmdLine.handleFlag("setDefaultBrowser", false)) { 651 // Note that setDefaultBrowser is an async function, but "handle" (the method being executed) 652 // is an implementation of an interface method and changing it to be async would be complicated 653 // and ultimately nothing here needs the result of setDefaultBrowser, so we do not bother doing 654 // an await. 655 lazy.ShellService.setDefaultBrowser(true).catch(e => { 656 console.error("setDefaultBrowser failed:", e); 657 }); 658 } 659 660 if (cmdLine.handleFlag("first-startup", false)) { 661 // We don't want subsequent calls to needHompageOverride to have different 662 // values because the milestones in prefs got updated, so we intentionally 663 // tell needHomepageOverride to leave the milestone prefs alone when doing 664 // this check. 665 let override = needHomepageOverride(false /* updateMilestones */); 666 lazy.FirstStartup.init(override == OVERRIDE_NEW_PROFILE /* newProfile */); 667 } 668 669 var fileParam = cmdLine.handleFlagWithParam("file", false); 670 if (fileParam) { 671 var file = cmdLine.resolveFile(fileParam); 672 var fileURI = Services.io.newFileURI(file); 673 openBrowserWindow(cmdLine, lazy.gSystemPrincipal, fileURI.spec); 674 cmdLine.preventDefault = true; 675 } 676 677 if (AppConstants.platform == "win") { 678 // Handle "? searchterm" for Windows Vista start menu integration 679 for (var i = cmdLine.length - 1; i >= 0; --i) { 680 var param = cmdLine.getArgument(i); 681 if (param.match(/^\? /)) { 682 cmdLine.removeArguments(i, i); 683 cmdLine.preventDefault = true; 684 685 searchParam = param.substr(2); 686 doSearch(searchParam, cmdLine); 687 } 688 } 689 } 690 }, 691 692 get helpInfo() { 693 let info = 694 " --browser Open a browser window.\n" + 695 " --new-window <url> Open <url> in a new window.\n" + 696 " --new-tab <url> Open <url> in a new tab.\n" + 697 " --private-window [<url>] Open <url> in a new private window.\n"; 698 if (AppConstants.platform == "win") { 699 info += " --preferences Open Options dialog.\n"; 700 } else { 701 info += " --preferences Open Preferences dialog.\n"; 702 } 703 info += 704 " --screenshot [<path>] Save screenshot to <path> or in working directory.\n"; 705 info += 706 " --window-size width[,height] Width and optionally height of screenshot.\n"; 707 info += 708 " --search <term> Search <term> with your default search engine.\n"; 709 info += " --setDefaultBrowser Set this app as the default browser.\n"; 710 info += 711 " --first-startup Run post-install actions before opening a new window.\n"; 712 info += " --kiosk Start the browser in kiosk mode.\n"; 713 info += 714 " --kiosk-monitor <num> Place kiosk browser window on given monitor.\n"; 715 info += 716 " --disable-pinch Disable touch-screen and touch-pad pinch gestures.\n"; 717 return info; 718 }, 719 720 /* nsIBrowserHandler */ 721 722 get defaultArgs() { 723 return this.getNewWindowArgs(); 724 }, 725 726 // This function is expected to be called in non-startup cases, 727 // a WNP will not be retrieved within this function, but it will retrieve 728 // any new profile override page(s) or regular startup page(s). 729 // For the startup version of this function, please use getFirstWindowArgs(). 730 // See Bug 1642039 for more information. 731 getNewWindowArgs(skipStartPage = false) { 732 var page = lazy.LaterRun.getURL(); 733 if (page == "about:blank") { 734 page = ""; 735 } 736 var startPage = ""; 737 var prefb = Services.prefs; 738 try { 739 var choice = prefb.getIntPref("browser.startup.page"); 740 if (choice == 1 || choice == 3) { 741 startPage = lazy.HomePage.get(); 742 } 743 } catch (e) { 744 console.error(e); 745 } 746 747 if (startPage == "about:blank") { 748 startPage = ""; 749 } 750 751 if (!skipStartPage && startPage) { 752 if (page) { 753 page += "|" + startPage; 754 } else { 755 page = startPage; 756 } 757 } else if (!page) { 758 page = startPage; 759 } 760 761 return page || "about:blank"; 762 }, 763 764 // This function is expected to be called very early during Firefox startup, 765 // It will retrieve a WNP if avaliable, before calling getNewWindowsArg() 766 // to retrieve any other startup pages that needs to be displayed. 767 // See Bug 1642039 for more information. 768 getFirstWindowArgs() { 769 var prefb = Services.prefs; 770 771 if (!gFirstWindow) { 772 gFirstWindow = true; 773 if (lazy.PrivateBrowsingUtils.isInTemporaryAutoStartMode) { 774 return "about:privatebrowsing"; 775 } 776 } 777 778 var override; 779 var overridePage = ""; 780 var additionalPage = ""; 781 var willRestoreSession = false; 782 let openAboutTor = false; 783 try { 784 // Read the old value of homepage_override.mstone before 785 // needHomepageOverride updates it, so that we can later add it to the 786 // URL if we do end up showing an overridePage. This makes it possible 787 // to have the overridePage's content vary depending on the version we're 788 // upgrading from. 789 let old_mstone = Services.prefs.getCharPref( 790 "browser.startup.homepage_override.mstone", 791 "unknown" 792 ); 793 let old_buildId = Services.prefs.getCharPref( 794 "browser.startup.homepage_override.buildID", 795 "unknown" 796 ); 797 798 // We do the same for the fork version. 799 let old_forkVersion = Services.prefs.getCharPref(FORK_VERSION_PREF, null); 800 801 override = needHomepageOverride(); 802 // An object to measure the progress of handleUpdateSuccess 803 let progress = { 804 updateFetched: false, 805 payloadCreated: false, 806 pingFailed: false, 807 }; 808 809 if (override != OVERRIDE_NONE) { 810 switch (override) { 811 case OVERRIDE_NEW_PROFILE: 812 // New profile. 813 gFirstRunProfile = true; 814 overridePage = Services.urlFormatter.formatURLPref( 815 "startup.homepage_welcome_url" 816 ); 817 additionalPage = Services.urlFormatter.formatURLPref( 818 "startup.homepage_welcome_url.additional" 819 ); 820 // Turn on 'later run' pages for new profiles. 821 lazy.LaterRun.enable(lazy.LaterRun.ENABLE_REASON_NEW_PROFILE); 822 break; 823 case OVERRIDE_NEW_MSTONE: { 824 // Check whether we will restore a session. If we will, we assume 825 // that this is an "update" session. This does not take crashes 826 // into account because that requires waiting for the session file 827 // to be read. If a crash occurs after updating, before restarting, 828 // we may open the startPage in addition to restoring the session. 829 willRestoreSession = 830 lazy.SessionStartup.isAutomaticRestoreEnabled(); 831 832 overridePage = Services.urlFormatter.formatURLPref( 833 "startup.homepage_override_url" 834 ); 835 836 /* 837 The update manager loads its data asynchronously, off of the main thread. 838 However, making this function asynchronous would be very difficult and 839 wouldn't provide any benefit. This code is part of the sequence of operations 840 that must run before the first tab and its contents can be displayed. 841 The user has to wait for this to complete regardless of the method or thread of execution, 842 and the browser will be practically unusable until it finishes. 843 Therefore, asynchronous execution does not offer any real advantages in this context. 844 */ 845 let update = spinForLastUpdateInstalled(); 846 847 // Make sure the update is newer than the last WNP version 848 // and the update is not newer than the current Firefox version. 849 if ( 850 update && 851 (Services.vc.compare(update.platformVersion, old_mstone) <= 0 || 852 Services.vc.compare( 853 update.appVersion, 854 Services.appinfo.version 855 ) > 0) 856 ) { 857 update = null; 858 overridePage = null; 859 } 860 861 /** 862 * If the override URL is provided by an experiment, is a valid 863 * Firefox What's New Page URL, and the update version is less than 864 * or equal to the maxVersion set by the experiment, we'll try to use 865 * the experiment override URL instead of the default or the 866 * update-provided URL. Additional policy checks are done in 867 * 868 * @see getPostUpdateOverridePage 869 */ 870 const nimbusOverrideUrl = Services.urlFormatter.formatURLPref( 871 "startup.homepage_override_url_nimbus" 872 ); 873 // This defines the maximum allowed Fx update version to see the 874 // nimbus WNP. For ex, if maxVersion is set to 127 but user updates 875 // to 128, they will not qualify. 876 const maxVersion = Services.prefs.getCharPref( 877 "startup.homepage_override_nimbus_maxVersion", 878 "" 879 ); 880 // This defines the minimum allowed Fx update version to see the 881 // nimbus WNP. For ex, if minVersion is set to 126 but user updates 882 // to 124, they will not qualify. 883 const minVersion = Services.prefs.getCharPref( 884 "startup.homepage_override_nimbus_minVersion", 885 "" 886 ); 887 // Pref used to disable all WNPs 888 const disableWNP = Services.prefs.getBoolPref( 889 "startup.homepage_override_nimbus_disable_wnp", 890 false 891 ); 892 let nimbusWNP; 893 // minVersion and maxVersion optional variables 894 const versionMatch = 895 (!maxVersion || 896 Services.vc.compare(update.appVersion, maxVersion) <= 0) && 897 (!minVersion || 898 Services.vc.compare(update.appVersion, minVersion) >= 0); 899 900 // The update version should be less than or equal to maxVersion and 901 // greater or equal to minVersion set by the experiment. 902 if (nimbusOverrideUrl && versionMatch) { 903 try { 904 let uri = Services.io.newURI(nimbusOverrideUrl); 905 // Only allow https://www.mozilla.org and https://www.mozilla.com 906 if ( 907 uri.scheme === "https" && 908 ["www.mozilla.org", "www.mozilla.com"].includes(uri.host) 909 ) { 910 nimbusWNP = uri.spec; 911 } else { 912 throw new Error("Bad URL"); 913 } 914 } catch { 915 console.error("Invalid WNP URL: ", nimbusOverrideUrl); 916 } 917 } 918 919 let old_version = old_forkVersion ? old_forkVersion : old_mstone; 920 if ( 921 update && 922 Services.vc.compare(update.appVersion, old_version) > 0 923 ) { 924 overridePage = getPostUpdateOverridePage( 925 update, 926 overridePage, 927 nimbusWNP, 928 disableWNP 929 ); 930 // Record a Nimbus exposure event for the whatsNewPage feature. 931 // The override page could be set in 3 ways: 1. set by Nimbus; 2. 932 // set by the update file (openURL); 3. defaulting to the 933 // evergreen page (set by the startup.homepage_override_url pref, 934 // value depends on the Fx channel). This is done to record that 935 // the control cohort could have seen the experimental What's New 936 // Page (and will instead see the default What's New Page, or 937 // won't see a WNP if the experiment disabled it by setting 938 // disable_wnp). `recordExposureEvent` only records an event if 939 // the user is enrolled in an experiment or rollout on the 940 // whatsNewPage feature, so it's safe to call it unconditionally. 941 if (overridePage || (versionMatch && disableWNP)) { 942 let nimbusWNPFeature = lazy.NimbusFeatures.whatsNewPage; 943 nimbusWNPFeature 944 .ready() 945 .then(() => nimbusWNPFeature.recordExposureEvent()); 946 } 947 948 lazy.LaterRun.enable(lazy.LaterRun.ENABLE_REASON_UPDATE_APPLIED); 949 } 950 951 // Send the update ping to signal that the update was successful. 952 // Only do this if the update is installed right now. 953 // The following code is ran asynchronously, but we won't await on it 954 // since the user may be still waiting for the browser to start up at this point. 955 let handleUpdateSuccessTask = 956 lazy.UpdateManager.updateInstalledAtStartup().then( 957 async updateInstalledAtStartup => { 958 if (updateInstalledAtStartup) { 959 await lazy.UpdatePing.handleUpdateSuccess( 960 old_mstone, 961 old_buildId, 962 progress 963 ); 964 } 965 } 966 ); 967 968 // Adding a shutdown blocker to ensure the 969 // update ping will be sent before Firefox exits. 970 lazy.AsyncShutdown.profileBeforeChange.addBlocker( 971 "BrowserContentHandler: running handleUpdateSuccess", 972 handleUpdateSuccessTask, 973 { fetchState: () => ({ progress }) } 974 ); 975 976 overridePage = overridePage.replace("%OLD_VERSION%", old_mstone); 977 overridePage = overridePage.replace( 978 "%OLD_BASE_BROWSER_VERSION%", 979 old_forkVersion 980 ); 981 if (AppConstants.BASE_BROWSER_UPDATE) { 982 // Tor Browser: Instead of opening the post-update "override page" 983 // directly, we ensure that about:tor will be opened, which should 984 // notify the user that their browser was updated. 985 // NOTE: We ignore any existing overridePage value, which can come 986 // from the openURL attribute within the updates.xml file. 987 Services.prefs.setBoolPref( 988 "torbrowser.post_update.shouldNotify", 989 true 990 ); 991 openAboutTor = true; 992 overridePage = "about:tor"; 993 } 994 break; 995 } 996 case OVERRIDE_NEW_BUILD_ID: { 997 // We must spin the events loop because `getFirstWindowArgs` cannot be 998 // easily made asynchronous, having too many synchronous callers. Additionally 999 // we must know the value of `updateInstalledAtStartup` immediately, 1000 // in order to properly enable `lazy.LaterRun`, that will be invoked shortly after this. 1001 let updateInstalledAtStartup = spinForUpdateInstalledAtStartup(); 1002 1003 if (updateInstalledAtStartup) { 1004 let handleUpdateSuccessTask = lazy.UpdatePing.handleUpdateSuccess( 1005 old_mstone, 1006 old_buildId, 1007 progress 1008 ); 1009 1010 lazy.AsyncShutdown.profileBeforeChange.addBlocker( 1011 "BrowserContentHandler: running handleUpdateSuccess", 1012 handleUpdateSuccessTask, 1013 { fetchState: () => ({ progress }) } 1014 ); 1015 1016 lazy.LaterRun.enable(lazy.LaterRun.ENABLE_REASON_UPDATE_APPLIED); 1017 } 1018 break; 1019 } 1020 } 1021 } 1022 } catch (ex) {} 1023 1024 // formatURLPref might return "about:blank" if getting the pref fails 1025 if (overridePage == "about:blank") { 1026 overridePage = ""; 1027 } 1028 1029 // Allow showing a one-time startup override if we're not showing one 1030 if (overridePage == "" && prefb.prefHasUserValue(ONCE_PREF)) { 1031 try { 1032 // Show if we haven't passed the expiration or there's no expiration 1033 const { expire, url } = JSON.parse( 1034 Services.urlFormatter.formatURLPref(ONCE_PREF) 1035 ); 1036 if (!(Date.now() > expire)) { 1037 // Only set allowed urls as override pages 1038 overridePage = url 1039 .split("|") 1040 .map(val => { 1041 let parsed = URL.parse(val); 1042 if (!parsed) { 1043 // Invalid URL, so filter out below 1044 console.error(`Invalid once url: ${val}`); 1045 } 1046 return parsed; 1047 }) 1048 .filter( 1049 parsed => 1050 parsed?.protocol == "https:" && 1051 // Only accept exact hostname or subdomain; without port 1052 ONCE_DOMAINS.has( 1053 Services.eTLD.getBaseDomainFromHost(parsed.host) 1054 ) 1055 ) 1056 .join("|"); 1057 1058 // Be noisy as properly configured urls should be unchanged 1059 if (overridePage != url) { 1060 console.error(`Mismatched once urls: ${url}`); 1061 } 1062 } 1063 } catch (ex) { 1064 // Invalid json pref, so ignore (and clear below) 1065 console.error("Invalid once pref:", ex); 1066 } finally { 1067 prefb.clearUserPref(ONCE_PREF); 1068 } 1069 } 1070 1071 if (!additionalPage) { 1072 additionalPage = lazy.LaterRun.getURL() || ""; 1073 } 1074 1075 if (additionalPage && additionalPage != "about:blank") { 1076 if (overridePage) { 1077 overridePage += "|" + additionalPage; 1078 } else { 1079 overridePage = additionalPage; 1080 } 1081 } 1082 1083 let skipStartPage = 1084 override == OVERRIDE_NEW_PROFILE && 1085 prefb.getBoolPref("browser.startup.firstrunSkipsHomepage"); 1086 1087 var startPage = this.getNewWindowArgs(skipStartPage && !willRestoreSession); 1088 1089 if (startPage == "about:blank") { 1090 startPage = ""; 1091 } 1092 1093 // If the user's homepage is about:tor, we do not want to open it twice with 1094 // the override. 1095 if ( 1096 openAboutTor && 1097 startPage === "about:tor" && 1098 overridePage?.split("|").includes("about:tor") 1099 ) { 1100 startPage = ""; 1101 } 1102 1103 // Only show the startPage if we're not restoring an update session and are 1104 // not set to skip the start page on this profile 1105 if (overridePage && startPage && !willRestoreSession && !skipStartPage) { 1106 return overridePage + "|" + startPage; 1107 } 1108 1109 return overridePage || startPage || "about:blank"; 1110 }, 1111 1112 mFeatures: null, 1113 1114 getFeatures: function bch_features(cmdLine) { 1115 if (this.mFeatures === null) { 1116 this.mFeatures = ""; 1117 1118 if (cmdLine) { 1119 try { 1120 var width = cmdLine.handleFlagWithParam("width", false); 1121 var height = cmdLine.handleFlagWithParam("height", false); 1122 var left = cmdLine.handleFlagWithParam("left", false); 1123 var top = cmdLine.handleFlagWithParam("top", false); 1124 1125 if (width) { 1126 this.mFeatures += ",width=" + width; 1127 } 1128 if (height) { 1129 this.mFeatures += ",height=" + height; 1130 } 1131 if (left) { 1132 this.mFeatures += ",left=" + left; 1133 } 1134 if (top) { 1135 this.mFeatures += ",top=" + top; 1136 } 1137 } catch (e) {} 1138 } 1139 1140 // The global PB Service consumes this flag, so only eat it in per-window 1141 // PB builds. 1142 if (lazy.PrivateBrowsingUtils.isInTemporaryAutoStartMode) { 1143 this.mFeatures += ",private"; 1144 } 1145 1146 if ( 1147 Services.prefs.getBoolPref("browser.suppress_first_window_animation") && 1148 !Services.wm.getMostRecentWindow("navigator:browser") 1149 ) { 1150 this.mFeatures += ",suppressanimation"; 1151 } 1152 } 1153 1154 return this.mFeatures; 1155 }, 1156 1157 get kiosk() { 1158 return gKiosk; 1159 }, 1160 1161 get majorUpgrade() { 1162 return gMajorUpgrade; 1163 }, 1164 1165 set majorUpgrade(val) { 1166 gMajorUpgrade = val; 1167 }, 1168 1169 get firstRunProfile() { 1170 return gFirstRunProfile; 1171 }, 1172 1173 set firstRunProfile(val) { 1174 gFirstRunProfile = val; 1175 }, 1176 1177 /* nsIContentHandler */ 1178 1179 handleContent: function bch_handleContent(contentType, context, request) { 1180 const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001; 1181 1182 try { 1183 var webNavInfo = Cc["@mozilla.org/webnavigation-info;1"].getService( 1184 Ci.nsIWebNavigationInfo 1185 ); 1186 if (!webNavInfo.isTypeSupported(contentType)) { 1187 throw NS_ERROR_WONT_HANDLE_CONTENT; 1188 } 1189 } catch (e) { 1190 throw NS_ERROR_WONT_HANDLE_CONTENT; 1191 } 1192 1193 request.QueryInterface(Ci.nsIChannel); 1194 handURIToExistingBrowser( 1195 request.URI, 1196 Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, 1197 null, 1198 false, 1199 request.loadInfo.triggeringPrincipal 1200 ); 1201 request.cancel(Cr.NS_BINDING_ABORTED); 1202 }, 1203 1204 /** 1205 * Replace the startup UI window created in BrowserGlue with an actual window 1206 */ 1207 replaceStartupWindow(args, forcePrivate) { 1208 let win = Services.wm.getMostRecentWindow("navigator:blank"); 1209 if (win) { 1210 // Remove the windowtype of our blank window so that we don't close it 1211 // later on when seeing cmdLine.preventDefault is true. 1212 win.document.documentElement.removeAttribute("windowtype"); 1213 1214 if (forcePrivate) { 1215 win.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = 1216 true; 1217 1218 if ( 1219 AppConstants.platform == "win" && 1220 Services.prefs.getBoolPref( 1221 "browser.privateWindowSeparation.enabled", 1222 true 1223 ) 1224 ) { 1225 lazy.WinTaskbar.setGroupIdForWindow( 1226 win, 1227 lazy.WinTaskbar.defaultPrivateGroupId 1228 ); 1229 lazy.WindowsUIUtils.setWindowIconFromExe( 1230 win, 1231 Services.dirsvc.get("XREExeF", Ci.nsIFile).path, 1232 // This corresponds to the definitions in 1233 // nsNativeAppSupportWin.h 1234 PRIVATE_BROWSING_ICON_INDEX 1235 ); 1236 } 1237 } 1238 1239 let openTime = win.openTime; 1240 win.location = AppConstants.BROWSER_CHROME_URL; 1241 win.arguments = args; // <-- needs to be a plain JS array here. 1242 1243 ChromeUtils.addProfilerMarker("earlyBlankWindowVisible", openTime); 1244 lazy.BrowserWindowTracker.registerOpeningWindow(win, forcePrivate); 1245 return win; 1246 } 1247 return null; 1248 }, 1249 1250 /* nsICommandLineValidator */ 1251 validate: function bch_validate(cmdLine) { 1252 var urlFlagIdx = cmdLine.findFlag("url", false); 1253 if ( 1254 urlFlagIdx > -1 && 1255 cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_EXPLICIT 1256 ) { 1257 var urlParam = cmdLine.getArgument(urlFlagIdx + 1); 1258 if ( 1259 cmdLine.length != urlFlagIdx + 2 || 1260 /firefoxurl(-[a-f0-9]+)?:/i.test(urlParam) 1261 ) { 1262 throw Components.Exception("", Cr.NS_ERROR_ABORT); 1263 } 1264 } 1265 }, 1266 }; 1267 var gBrowserContentHandler = new nsBrowserContentHandler(); 1268 1269 function handURIToExistingBrowser( 1270 uri, 1271 location, 1272 cmdLine, 1273 forcePrivate, 1274 triggeringPrincipal 1275 ) { 1276 if (!shouldLoadURI(uri)) { 1277 return; 1278 } 1279 1280 let openInWindow = ({ browserDOMWindow }) => { 1281 browserDOMWindow.openURI( 1282 uri, 1283 null, 1284 location, 1285 Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL, 1286 triggeringPrincipal 1287 ); 1288 }; 1289 1290 // Unless using a private window is forced, open external links in private 1291 // windows only if we're in perma-private mode. 1292 let allowPrivate = 1293 forcePrivate || lazy.PrivateBrowsingUtils.permanentPrivateBrowsing; 1294 let navWin = lazy.BrowserWindowTracker.getTopWindow({ 1295 private: allowPrivate, 1296 }); 1297 1298 if (navWin) { 1299 openInWindow(navWin); 1300 return; 1301 } 1302 1303 let pending = lazy.BrowserWindowTracker.getPendingWindow({ 1304 private: allowPrivate, 1305 }); 1306 if (pending) { 1307 // Note that we cannot make this function async as some callers rely on 1308 // catching exceptions it can throw in some cases and some of those callers 1309 // cannot be made async. 1310 pending.then(openInWindow); 1311 return; 1312 } 1313 1314 // if we couldn't load it in an existing window, open a new one 1315 openBrowserWindow(cmdLine, triggeringPrincipal, uri.spec, null, forcePrivate); 1316 } 1317 1318 /** 1319 * If given URI is a file type or a protocol, record telemetry that 1320 * Firefox was invoked or launched (if `isLaunch` is truth-y). If the 1321 * file type or protocol is not registered by default, record it as 1322 * ".<other extension>" or "<other protocol>". 1323 * 1324 * @param uri 1325 * The URI Firefox was asked to handle. 1326 * @param isLaunch 1327 * truth-y if Firefox was launched/started rather than running and invoked. 1328 */ 1329 function maybeRecordToHandleTelemetry(uri, isLaunch) { 1330 let counter = isLaunch 1331 ? Glean.osEnvironment.launchedToHandle 1332 : Glean.osEnvironment.invokedToHandle; 1333 1334 if (uri instanceof Ci.nsIFileURL) { 1335 let extension = "." + uri.fileExtension.toLowerCase(); 1336 // Keep synchronized with https://searchfox.org/mozilla-central/source/browser/installer/windows/nsis/shared.nsh 1337 // and https://searchfox.org/mozilla-central/source/browser/installer/windows/msix/AppxManifest.xml.in. 1338 let registeredExtensions = new Set([ 1339 ".avif", 1340 ".htm", 1341 ".html", 1342 ".pdf", 1343 ".shtml", 1344 ".xht", 1345 ".xhtml", 1346 ".svg", 1347 ".webp", 1348 ]); 1349 if (registeredExtensions.has(extension)) { 1350 counter[extension].add(1); 1351 } else { 1352 counter[".<other extension>"].add(1); 1353 } 1354 } else if (uri) { 1355 let scheme = uri.scheme.toLowerCase(); 1356 let registeredSchemes = new Set(["about", "http", "https", "mailto"]); 1357 if (registeredSchemes.has(scheme)) { 1358 counter[scheme].add(1); 1359 } else { 1360 counter["<other protocol>"].add(1); 1361 } 1362 } 1363 } 1364 1365 export function nsDefaultCommandLineHandler() {} 1366 1367 nsDefaultCommandLineHandler.prototype = { 1368 /* nsISupports */ 1369 QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]), 1370 1371 _haveProfile: false, 1372 1373 /** 1374 * @param {nsICommandLine} cmdLine 1375 * @returns {boolean} true if the command is handled as notification, otherwise false. 1376 */ 1377 handleNotification(cmdLine) { 1378 if (AppConstants.platform !== "win") { 1379 // Only for Windows for now 1380 return false; 1381 } 1382 1383 // Windows itself does disk I/O when the notification service is 1384 // initialized, so make sure that is lazy. 1385 let tag = cmdLine.handleFlagWithParam("notification-windowsTag", false); 1386 if (!tag) { 1387 return false; 1388 } 1389 1390 // All notifications will invoke Firefox with an action. Prior to Bug 1805514, 1391 // this data was extracted from the Windows toast object directly (keyed by the 1392 // notification ID) and not passed over the command line. This is acceptable 1393 // because the data passed is chrome-controlled, but if we implement the `actions` 1394 // part of the DOM Web Notifications API, this will no longer be true: 1395 // content-controlled data might transit over the command line. This could lead 1396 // to escaping bugs and overflows. In the future, we intend to avoid any such 1397 // issue by once again extracting all such data from the Windows toast object. 1398 let notificationData = cmdLine.handleFlagWithParam( 1399 "notification-windowsAction", 1400 false 1401 ); 1402 if (!notificationData) { 1403 return false; 1404 } 1405 1406 let alertService = lazy.gWindowsAlertsService; 1407 if (!alertService) { 1408 console.error("Windows alert service not available."); 1409 return false; 1410 } 1411 1412 // Notification handling occurs asynchronously to prevent blocking the 1413 // main thread. As a result we won't have the information we need to open 1414 // a new tab in the case of notification fallback handling before 1415 // returning. We call `enterLastWindowClosingSurvivalArea` to prevent 1416 // the browser from exiting in case early blank window is pref'd off. 1417 if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { 1418 Services.startup.enterLastWindowClosingSurvivalArea(); 1419 } 1420 this.handleNotificationImpl(cmdLine, tag, notificationData, alertService) 1421 .catch(e => { 1422 console.error( 1423 `Error handling Windows notification with tag '${tag}':`, 1424 e 1425 ); 1426 }) 1427 .finally(() => { 1428 if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { 1429 Services.startup.exitLastWindowClosingSurvivalArea(); 1430 } 1431 }); 1432 return true; 1433 }, 1434 1435 /** 1436 * Returns when the signal is sent to the relevant notification handler, either: 1437 * 1. The service worker via nsINotificationHandler for a web notification, or 1438 * 2. SpecialMessageActions for a Messaging-System-invoked one 1439 * 1440 * @param {nsICommandLine} cmdLine 1441 * @param {string} notificationId 1442 * @param {string} notificationData 1443 * @param {nsIWindowsAlertsService} alertService 1444 */ 1445 async handleNotificationImpl( 1446 cmdLine, 1447 notificationId, 1448 notificationData, 1449 alertService 1450 ) { 1451 let { tagWasHandled } = await alertService.handleWindowsTag(notificationId); 1452 1453 try { 1454 notificationData = JSON.parse(notificationData); 1455 } catch (e) { 1456 console.error( 1457 `Failed to parse (notificationData=${notificationData}) for Windows notification (id=${notificationId})` 1458 ); 1459 } 1460 1461 // This is awkward: the relaunch data set by the caller is _wrapped_ 1462 // into a compound object that includes additional notification data, 1463 // and everything is exchanged as strings. Unwrap and parse here. 1464 let opaqueRelaunchData = null; 1465 if (notificationData?.opaqueRelaunchData) { 1466 try { 1467 opaqueRelaunchData = JSON.parse(notificationData.opaqueRelaunchData); 1468 } catch (e) { 1469 console.error( 1470 `Failed to parse (opaqueRelaunchData=${notificationData.opaqueRelaunchData}) for Windows notification (id=${notificationId})` 1471 ); 1472 } 1473 } 1474 1475 if (notificationData?.privilegedName) { 1476 Glean.browserLaunchedToHandle.systemNotification.record({ 1477 name: notificationData.privilegedName, 1478 }); 1479 } 1480 1481 // If we have an action in the notification data, this will be the 1482 // window to perform the action in. 1483 let winForAction; 1484 1485 // Fall back to launchUrl to not break notifications opened from 1486 // previous builds after browser updates, as such notification would 1487 // still have the old field. 1488 let origin = notificationData?.origin ?? notificationData?.launchUrl; 1489 let action = notificationData?.action; 1490 1491 if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { 1492 // Get a browser window where we can open a tab. Having a browser 1493 // window here helps: 1494 // 1495 // 1. Earlier loading without having to wait for service worker 1496 // 2. Makes sure a browser window loads even if no service worker exists 1497 // or the worker decides to not open anything 1498 // 1499 // instead of making user to wait on the initial navigator:blank. 1500 winForAction = openBrowserWindow(cmdLine, lazy.gSystemPrincipal); 1501 await lazy.BrowserUtils.promiseObserved( 1502 "browser-delayed-startup-finished", 1503 subject => subject == winForAction 1504 ); 1505 } 1506 1507 if (!tagWasHandled && origin && !opaqueRelaunchData) { 1508 let originPrincipal = 1509 Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin); 1510 1511 const handler = Cc["@mozilla.org/notification-handler;1"].getService( 1512 Ci.nsINotificationHandler 1513 ); 1514 1515 await handler.respondOnClick( 1516 originPrincipal, 1517 notificationId, 1518 action, 1519 /* aAutoClosed */ true 1520 ); 1521 return; 1522 } 1523 1524 // Relaunch in private windows only if we're in perma-private mode. 1525 let allowPrivate = lazy.PrivateBrowsingUtils.permanentPrivateBrowsing; 1526 winForAction = lazy.BrowserWindowTracker.getTopWindow({ 1527 private: allowPrivate, 1528 allowFromInactiveWorkspace: true, 1529 }); 1530 1531 // Note: at time of writing `opaqueRelaunchData` was only used by the 1532 // Messaging System; if present it could be inferred that the message 1533 // originated from the Messaging System. The Messaging System did not 1534 // act on Windows 8 style notification callbacks, so there was no risk 1535 // of duplicating behavior. If a non-Messaging System consumer is 1536 // modified to populate `opaqueRelaunchData` or the Messaging System 1537 // modified to use the callback directly, we will need to revisit 1538 // this assumption. 1539 if (opaqueRelaunchData && winForAction) { 1540 // Without dispatch, `OPEN_URL` with `where: "tab"` does not work on relaunch. 1541 Services.tm.dispatchToMainThread(() => { 1542 lazy.SpecialMessageActions.handleAction( 1543 opaqueRelaunchData, 1544 winForAction.gBrowser 1545 ); 1546 }); 1547 } 1548 }, 1549 1550 /* nsICommandLineHandler */ 1551 handle: function dch_handle(cmdLine) { 1552 var urilist = []; 1553 var principalList = []; 1554 1555 if ( 1556 cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH && 1557 cmdLine.findFlag("os-autostart", true) != -1 1558 ) { 1559 // Relaunching after reboot (or quickly opening the application on reboot) and launch-on-login interact. If we see an after reboot command line while already running, ignore it. 1560 return; 1561 } 1562 1563 if (this.handleNotification(cmdLine)) { 1564 // This command is about notification and is handled already 1565 return; 1566 } 1567 1568 if ( 1569 cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH && 1570 Services.startup.wasSilentlyStarted 1571 ) { 1572 // If we are starting up in silent mode, don't open a window. We also need 1573 // to make sure that the application doesn't immediately exit, so stay in 1574 // a LastWindowClosingSurvivalArea until a window opens. 1575 Services.startup.enterLastWindowClosingSurvivalArea(); 1576 Services.obs.addObserver(function windowOpenObserver() { 1577 Services.startup.exitLastWindowClosingSurvivalArea(); 1578 Services.obs.removeObserver(windowOpenObserver, "domwindowopened"); 1579 }, "domwindowopened"); 1580 return; 1581 } 1582 1583 if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { 1584 // Handle the case where we don't have a profile selected yet (e.g. the 1585 // Profile Manager is displayed). 1586 // On Windows, we will crash if we open an url and then select a profile. 1587 // On macOS, if we open an url we don't experience a crash but a broken 1588 // window is opened. 1589 // To prevent this handle all url command line flags and set the 1590 // command line's preventDefault to true to prevent the display of the ui. 1591 // The initial command line will be retained when nsAppRunner calls 1592 // LaunchChild though urls launched after the initial launch will be lost. 1593 if (!this._haveProfile) { 1594 try { 1595 // This will throw when a profile has not been selected. 1596 Services.dirsvc.get("ProfD", Ci.nsIFile); 1597 this._haveProfile = true; 1598 } catch (e) { 1599 // eslint-disable-next-line no-empty 1600 while ((ar = cmdLine.handleFlagWithParam("url", false))) {} 1601 cmdLine.preventDefault = true; 1602 } 1603 } 1604 } 1605 1606 // `-osint` and handling registered file types and protocols is Windows-only. 1607 let launchedWithArg_osint = 1608 AppConstants.platform == "win" && cmdLine.findFlag("osint", false) == 0; 1609 if (launchedWithArg_osint) { 1610 cmdLine.handleFlag("osint", false); 1611 } 1612 1613 try { 1614 var ar; 1615 while ((ar = cmdLine.handleFlagWithParam("url", false))) { 1616 let { uri, principal } = resolveURIInternal(cmdLine, ar); 1617 urilist.push(uri); 1618 principalList.push(principal); 1619 1620 if (launchedWithArg_osint) { 1621 launchedWithArg_osint = false; 1622 1623 // We use the resolved URI here, even though it can produce 1624 // surprising results where-by `-osint -url test.pdf` resolves to 1625 // a query with search parameter "test.pdf". But that shouldn't 1626 // happen when Firefox is launched by Windows itself: files should 1627 // exist and be resolved to file URLs. 1628 const isLaunch = 1629 cmdLine && cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH; 1630 1631 maybeRecordToHandleTelemetry(uri, isLaunch); 1632 } 1633 } 1634 } catch (e) { 1635 console.error(e); 1636 } 1637 1638 if (cmdLine.findFlag("screenshot", true) != -1) { 1639 // Shouldn't have to push principal here with the screenshot flag 1640 lazy.HeadlessShell.handleCmdLineArgs( 1641 cmdLine, 1642 urilist.filter(shouldLoadURI).map(u => u.spec) 1643 ); 1644 return; 1645 } 1646 1647 for (let i = 0; i < cmdLine.length; ++i) { 1648 var curarg = cmdLine.getArgument(i); 1649 if (curarg.match(/^-/)) { 1650 console.error("Warning: unrecognized command line flag", curarg); 1651 // To emulate the pre-nsICommandLine behavior, we ignore 1652 // the argument after an unrecognized flag. 1653 ++i; 1654 } else { 1655 try { 1656 let { uri, principal } = resolveURIInternal(cmdLine, curarg); 1657 urilist.push(uri); 1658 principalList.push(principal); 1659 } catch (e) { 1660 console.error( 1661 `Error opening URI ${curarg} from the command line:`, 1662 e 1663 ); 1664 } 1665 } 1666 } 1667 1668 if (urilist.length) { 1669 if ( 1670 cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH && 1671 urilist.length == 1 1672 ) { 1673 // Try to find an existing window and load our URI into the 1674 // current tab, new tab, or new window as prefs determine. 1675 try { 1676 handURIToExistingBrowser( 1677 urilist[0], 1678 Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW, 1679 cmdLine, 1680 false, 1681 principalList[0] ?? lazy.gSystemPrincipal 1682 ); 1683 return; 1684 } catch (e) {} 1685 } 1686 1687 // Can't open multiple URLs without using system principal. 1688 var URLlist = urilist.filter(shouldLoadURI).map(u => u.spec); 1689 if (URLlist.length) { 1690 openBrowserWindow(cmdLine, lazy.gSystemPrincipal, URLlist); 1691 } 1692 } else if (!cmdLine.preventDefault) { 1693 if ( 1694 AppConstants.platform == "win" && 1695 cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH && 1696 lazy.WindowsUIUtils.inWin10TabletMode 1697 ) { 1698 // In Win10's tablet mode, do not create a new window, but reuse the 1699 // existing one. (Win11's tablet mode is still windowed and has no need 1700 // for this workaround.) 1701 let win = lazy.BrowserWindowTracker.getTopWindow(); 1702 if (win) { 1703 win.focus(); 1704 return; 1705 } 1706 } 1707 openBrowserWindow(cmdLine, lazy.gSystemPrincipal); 1708 } else { 1709 // Need a better solution in the future to avoid opening the blank window 1710 // when command line parameters say we are not going to show a browser 1711 // window, but for now the blank window getting closed quickly (and 1712 // causing only a slight flicker) is better than leaving it open. 1713 let win = Services.wm.getMostRecentWindow("navigator:blank"); 1714 if (win) { 1715 win.close(); 1716 } 1717 } 1718 }, 1719 1720 helpInfo: "", 1721 };