Source code

Revision control

Copy as Markdown

Other Tools

/* 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<bool>}
* 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<bool>}
* 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<bool>}
* 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<bool>}
* 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<bool>}
* 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<bool>}
* 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.
*
*
* @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;
},
};