/* 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/. */ const LAUNCH_ON_LOGIN_TASKID = "LaunchOnLogin"; /** * "Launch on Login" is a Firefox feature automatically launches Firefox when the * user logs in to Windows. The technical mechanism is simply writing a registry * key to `Software\Microsoft\Windows\CurrentVersion\Run`, but there is an issue: * when disabled in the Windows UI, additional registry keys are written under * `Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run`. Any * keys stored here should be seen as a user override and should never be modified. * When such keys are present, the launch on login feature should be considered * disabled and not available from within Firefox. This module provides the * functionality to access and modify these registry keys. * * MSIX installs cannot write to the registry so we instead use the MSIX-exclusive * Windows StartupTask APIs. The difference here is that the startup task is always * "registered", we control whether it's enabled or disabled. As such some * functions such as getLaunchOnLoginApproved() have different behavior on MSIX installs. */ export var WindowsLaunchOnLogin = { /** * Accepts another function as an argument and provides an open Windows * launch on login registry key for the passed-in function to manipulate. * * @param func * The function to use. */ async withLaunchOnLoginRegistryKey(func) { let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( Ci.nsIWindowsRegKey ); wrk.open( wrk.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", wrk.ACCESS_ALL ); try { await func(wrk); } finally { wrk.close(); } }, /** * Safely creates a Windows launch on login registry key */ async createLaunchOnLoginRegistryKey() { try { await this.withLaunchOnLoginRegistryKey(async wrk => { // Added launch option -os-autostart for telemetry // Add quote marks around path to account for spaces in path let autostartPath = this.quoteString(Services.dirsvc.get("XREExeF", Ci.nsIFile).path) + " -os-autostart"; try { wrk.writeStringValue( this.getLaunchOnLoginRegistryName(), autostartPath ); } catch (e) { console.error("Could not write value to registry", e); } }); } catch (e) { // We should only end up here if we fail to open the registry console.error("Failed to open Windows registry", e); } }, /** * Either creates a Windows launch on login registry key on regular installs * or enables the startup task within the app manifest due to * restrictions on writing to the registry in MSIX. */ async createLaunchOnLogin() { if (Services.sysinfo.getProperty("hasWinPackageId")) { await this.enableLaunchOnLoginMSIX(); } else { await this.createLaunchOnLoginRegistryKey(); } }, /** * Stop launching on login by removing our registry key (on non-MSIX builds) or disabling * our startup task (on MSIX builds). * If the user has disabled launch on login via the Windows settings, this method will * no-op as we do not want to change the (separate) keys Windows uses to manage user * settings, and removing our own launch on login registry key but leaving the Windows * keys alone would leave the registry in a dubious state. */ async removeLaunchOnLogin() { if (await this.getLaunchOnLoginApproved()) { if (Services.sysinfo.getProperty("hasWinPackageId")) { await this._disableLaunchOnLoginMSIX(); } else { await this._removeLaunchOnLoginRegistryKey(); await this._removeLaunchOnLoginShortcuts(); } } }, /** * Safely removes a Windows launch on login registry key */ async _removeLaunchOnLoginRegistryKey() { try { await this.withLaunchOnLoginRegistryKey(async wrk => { let registryName = this.getLaunchOnLoginRegistryName(); if (wrk.hasValue(registryName)) { try { wrk.removeValue(registryName); } catch (e) { console.error("Failed to remove Windows registry value", e); } } }); } catch (e) { // We should only end up here if we fail to open the registry console.error("Failed to open Windows registry", e); } }, /** * Enables launch on login on MSIX installs by using the * StartupTask APIs. A task called "LaunchOnLogin" exists * in the packaged application manifest. * * @returns {Promise} * Whether the enable operation was successful. */ async enableLaunchOnLoginMSIX() { if (!Services.sysinfo.getProperty("hasWinPackageId")) { throw Components.Exception( "Called on non-MSIX build", Cr.NS_ERROR_NOT_IMPLEMENTED ); } let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService( Ci.nsIWindowsShellService ); return shellService.enableLaunchOnLoginMSIXAsync(LAUNCH_ON_LOGIN_TASKID); }, /** * Disables launch on login on MSIX installs by using the * StartupTask APIs. A task called "LaunchOnLogin" exists * in the packaged application manifest. * * @returns {Promise} * Whether the disable operation was successful. */ async _disableLaunchOnLoginMSIX() { if (!Services.sysinfo.getProperty("hasWinPackageId")) { throw Components.Exception( "Called on non-MSIX build", Cr.NS_ERROR_NOT_IMPLEMENTED ); } let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService( Ci.nsIWindowsShellService ); return shellService.disableLaunchOnLoginMSIXAsync(LAUNCH_ON_LOGIN_TASKID); }, /** * Determines whether launch on login on MSIX is enabled by using the * StartupTask APIs. A task called "LaunchOnLogin" exists * in the packaged application manifest. * * @returns {Promise} * Whether the startup task is enabled. */ async getLaunchOnLoginEnabledMSIX() { if (!Services.sysinfo.getProperty("hasWinPackageId")) { throw Components.Exception( "Called on non-MSIX build", Cr.NS_ERROR_NOT_IMPLEMENTED ); } let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService( Ci.nsIWindowsShellService ); let state = await shellService.getLaunchOnLoginEnabledMSIXAsync( LAUNCH_ON_LOGIN_TASKID ); return ( state == shellService.LAUNCH_ON_LOGIN_ENABLED || state == shellService.LAUNCH_ON_LOGIN_ENABLED_BY_POLICY ); }, /** * Gets a list of all launch on login shortcuts in the * %USERNAME%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup folder * that point to the current Firefox executable. */ getLaunchOnLoginShortcutList() { let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService( Ci.nsIWindowsShellService ); return shellService.getLaunchOnLoginShortcuts(); }, /** * Safely removes all launch on login shortcuts in the * %USERNAME%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup folder * that point to the current Firefox executable. */ async _removeLaunchOnLoginShortcuts() { let shortcuts = this.getLaunchOnLoginShortcutList(); for (let i = 0; i < shortcuts.length; i++) { await IOUtils.remove(shortcuts[i]); } }, /** * If the state is set to disabled from the Windows UI our API calls to * re-enable it will fail so we should say that it's not approved and * provide users the link to the App Startup settings so they can * re-enable it. * * @returns {Promise} * If launch on login has not been disabled by Windows settings * or enabled by policy. */ async getLaunchOnLoginApprovedMSIX() { if (!Services.sysinfo.getProperty("hasWinPackageId")) { throw Components.Exception( "Called on non-MSIX build", Cr.NS_ERROR_NOT_IMPLEMENTED ); } let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService( Ci.nsIWindowsShellService ); let state = await shellService.getLaunchOnLoginEnabledMSIXAsync( LAUNCH_ON_LOGIN_TASKID ); return !( state == shellService.LAUNCH_ON_LOGIN_DISABLED_BY_SETTINGS || state == shellService.LAUNCH_ON_LOGIN_ENABLED_BY_POLICY ); }, /** * Checks if Windows launch on login was independently enabled or disabled * by the user in the Windows Startup Apps menu. The registry key that * stores this information should not be modified. * * If the state is set to disabled from the Windows UI on MSIX our API calls to * re-enable it will fail so report false. * * @returns {Promise} * Report whether launch on login is allowed on Windows. On MSIX * it's possible to set a startup app through policy making us * unable to modify it so we should account for that here. */ async getLaunchOnLoginApproved() { if (Services.sysinfo.getProperty("hasWinPackageId")) { return this.getLaunchOnLoginApprovedMSIX(); } try { let wrkApproved = Cc[ "@mozilla.org/windows-registry-key;1" ].createInstance(Ci.nsIWindowsRegKey); wrkApproved.open( wrkApproved.ROOT_KEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\Run", wrkApproved.ACCESS_READ ); let registryName = this.getLaunchOnLoginRegistryName(); if (!wrkApproved.hasValue(registryName)) { wrkApproved.close(); return true; } // There's very little consistency with these binary values aside from if the first byte // is even it's enabled and odd is disabled. There's also no published specification. let approvedByWindows = wrkApproved.readBinaryValue(registryName).charCodeAt(0) % 2 == 0; wrkApproved.close(); return approvedByWindows; } catch (e) { // We should only end up here if we fail to open the registry console.error("Failed to open Windows registry", e); } return true; }, /** * Checks if Windows launch on login has an existing registry key or user-created shortcut in * %USERNAME%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup. The registry key that * stores this information should not be modified. * * On MSIX installs we instead query whether the StartupTask is enabled or disabled * * @returns {Promise} * Whether launch on login is enabled. */ async getLaunchOnLoginEnabled() { if (Services.sysinfo.getProperty("hasWinPackageId")) { return this.getLaunchOnLoginEnabledMSIX(); } let registryName = this.getLaunchOnLoginRegistryName(); let regExists = false; let shortcutExists = false; this.withLaunchOnLoginRegistryKey(wrk => { try { // Start by checking if registry key exists. regExists = wrk.hasValue(registryName); } catch (e) { // We should only end up here if we fail to open the registry console.error("Failed to open Windows registry", e); } }); if (!regExists) { shortcutExists = !!this.getLaunchOnLoginShortcutList().length; } // Even if a user disables it later on we want the launch on login // infobar to remain disabled as the user is aware of the option. if (regExists || shortcutExists) { Services.prefs.setBoolPref( "browser.startup.windowsLaunchOnLogin.disableLaunchOnLoginPrompt", true ); } return regExists || shortcutExists; }, /** * Quotes a string for use as a single command argument, using Windows quoting * conventions. * * @see https://msdn.microsoft.com/en-us/library/17w5ykft(v=vs.85).aspx * * @param {string} str * The argument string to quote. * @returns {string} */ quoteString(str) { if (!/[\s"]/.test(str)) { return str; } let escaped = str.replace(/(\\*)("|$)/g, (m0, m1, m2) => { if (m2) { m2 = `\\${m2}`; } return `${m1}${m1}${m2}`; }); return `"${escaped}"`; }, /** * Generates a unique registry name for the current application * like "Mozilla-Firefox-71AE18FE3142402B". */ getLaunchOnLoginRegistryName() { let xreDirProvider = Cc[ "@mozilla.org/xre/directory-provider;1" ].createInstance(Ci.nsIXREDirProvider); let registryName = `${Services.appinfo.vendor}-${ Services.appinfo.name }-${xreDirProvider.getInstallHash()}`; return registryName; }, };