forked from mirrors/gecko-dev
This does two things. First, it expects the special FirefoxPDF-... ProgID to be available for UserChoice. We could manage without it for a while, but eventually we expect set-to-default to include PDFs. When that is the case, if it doesn't exist, something has gone very wrong, and we'd like to find that out (via our existing telemetry, which reports "missing" ProgIDs). Second, it arranges to use the new FirefoxPDF-... ProgID when setting-to-default. Differential Revision: https://phabricator.services.mozilla.com/D142303
480 lines
15 KiB
JavaScript
480 lines
15 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["ShellService"];
|
|
|
|
const { AppConstants } = ChromeUtils.import(
|
|
"resource://gre/modules/AppConstants.jsm"
|
|
);
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
NimbusFeatures: "resource://nimbus/ExperimentAPI.jsm",
|
|
setTimeout: "resource://gre/modules/Timer.jsm",
|
|
Subprocess: "resource://gre/modules/Subprocess.jsm",
|
|
WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
this,
|
|
"XreDirProvider",
|
|
"@mozilla.org/xre/directory-provider;1",
|
|
"nsIXREDirProvider"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
|
let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
|
|
let consoleOptions = {
|
|
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
|
|
// messages during development. See LOG_LEVELS in Console.jsm for details.
|
|
maxLogLevel: "debug",
|
|
maxLogLevelPref: "browser.shell.loglevel",
|
|
prefix: "ShellService",
|
|
};
|
|
return new ConsoleAPI(consoleOptions);
|
|
});
|
|
|
|
/**
|
|
* Internal functionality to save and restore the docShell.allow* properties.
|
|
*/
|
|
let ShellServiceInternal = {
|
|
/**
|
|
* Used to determine whether or not to offer "Set as desktop background"
|
|
* functionality. Even if shell service is available it is not
|
|
* guaranteed that it is able to set the background for every desktop
|
|
* which is especially true for Linux with its many different desktop
|
|
* environments.
|
|
*/
|
|
get canSetDesktopBackground() {
|
|
if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
|
|
return true;
|
|
}
|
|
|
|
if (AppConstants.platform == "linux") {
|
|
if (this.shellService) {
|
|
let linuxShellService = this.shellService.QueryInterface(
|
|
Ci.nsIGNOMEShellService
|
|
);
|
|
return linuxShellService.canSetDesktopBackground;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
isDefaultBrowserOptOut() {
|
|
if (AppConstants.platform == "win") {
|
|
let optOutValue = WindowsRegistry.readRegKey(
|
|
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
|
"Software\\Mozilla\\Firefox",
|
|
"DefaultBrowserOptOut"
|
|
);
|
|
WindowsRegistry.removeRegKey(
|
|
Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
|
|
"Software\\Mozilla\\Firefox",
|
|
"DefaultBrowserOptOut"
|
|
);
|
|
if (optOutValue == "True") {
|
|
Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", false);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Used to determine whether or not to show a "Set Default Browser"
|
|
* query dialog. This attribute is true if the application is starting
|
|
* up and "browser.shell.checkDefaultBrowser" is true, otherwise it
|
|
* is false.
|
|
*/
|
|
_checkedThisSession: false,
|
|
get shouldCheckDefaultBrowser() {
|
|
// If we've already checked, the browser has been started and this is a
|
|
// new window open, and we don't want to check again.
|
|
if (this._checkedThisSession) {
|
|
return false;
|
|
}
|
|
|
|
if (!Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser")) {
|
|
return false;
|
|
}
|
|
|
|
if (this.isDefaultBrowserOptOut()) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
set shouldCheckDefaultBrowser(shouldCheck) {
|
|
Services.prefs.setBoolPref(
|
|
"browser.shell.checkDefaultBrowser",
|
|
!!shouldCheck
|
|
);
|
|
},
|
|
|
|
isDefaultBrowser(startupCheck, forAllTypes) {
|
|
// If this is the first browser window, maintain internal state that we've
|
|
// checked this session (so that subsequent window opens don't show the
|
|
// default browser dialog).
|
|
if (startupCheck) {
|
|
this._checkedThisSession = true;
|
|
}
|
|
if (this.shellService) {
|
|
return this.shellService.isDefaultBrowser(forAllTypes);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/*
|
|
* Invoke the Windows Default Browser agent with the given options.
|
|
*
|
|
* Separated for easy stubbing in tests.
|
|
*/
|
|
_callExternalDefaultBrowserAgent(options = {}) {
|
|
const wdba = Services.dirsvc.get("XREExeF", Ci.nsIFile);
|
|
wdba.leafName = "default-browser-agent.exe";
|
|
return Subprocess.call({
|
|
...options,
|
|
command: options.command || wdba.path,
|
|
});
|
|
},
|
|
|
|
/*
|
|
* Check if UserChoice is impossible.
|
|
*
|
|
* Separated for easy stubbing in tests.
|
|
*
|
|
* @return string telemetry result like "Err*", or null if UserChoice
|
|
* is possible.
|
|
*/
|
|
_userChoiceImpossibleTelemetryResult() {
|
|
if (!ShellService.checkAllProgIDsExist()) {
|
|
return "ErrProgID";
|
|
}
|
|
if (!ShellService.checkBrowserUserChoiceHashes()) {
|
|
return "ErrHash";
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/*
|
|
* Accommodate `setDefaultPDFHandlerOnlyReplaceBrowsers` feature.
|
|
* @return true if Firefox should set itself as default PDF handler, false
|
|
* otherwise.
|
|
*/
|
|
_shouldSetDefaultPDFHandler() {
|
|
if (
|
|
!NimbusFeatures.shellService.getVariable(
|
|
"setDefaultPDFHandlerOnlyReplaceBrowsers"
|
|
)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const knownBrowserPrefixes = [
|
|
"AppXq0fevzme2pys62n3e0fbqa7peapykr8v", // Edge before Blink, per https://stackoverflow.com/a/32724723.
|
|
"Brave", // For "BraveFile".
|
|
"Chrome", // For "ChromeHTML".
|
|
"Firefox", // For "FirefoxHTML-*" or "FirefoxPDF-*". Need to take from other installations of Firefox!
|
|
"IE", // Best guess.
|
|
"MSEdge", // For "MSEdgePDF". Edgium.
|
|
"Opera", // For "OperaStable", presumably varying with channel.
|
|
"Yandex", // For "YandexPDF.IHKFKZEIOKEMR6BGF62QXCRIKM", presumably varying with installation.
|
|
];
|
|
let currentProgID = "";
|
|
try {
|
|
// Returns the empty string when no association is registered, in
|
|
// which case the prefix matching will fail and we'll set Firefox as
|
|
// the default PDF handler.
|
|
currentProgID = this.queryCurrentDefaultHandlerFor(".pdf");
|
|
} catch (e) {
|
|
// We only get an exception when something went really wrong. Fail
|
|
// safely: don't set Firefox as default PDF handler.
|
|
log.warn(
|
|
"Failed to queryCurrentDefaultHandlerFor: " +
|
|
"not setting Firefox as default PDF handler!"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (currentProgID == "") {
|
|
log.debug(
|
|
`Current default PDF handler has no registered association; ` +
|
|
`should set as default PDF handler.`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
let knownBrowserPrefix = knownBrowserPrefixes.find(it =>
|
|
currentProgID.startsWith(it)
|
|
);
|
|
if (knownBrowserPrefix) {
|
|
log.debug(
|
|
`Current default PDF handler progID matches known browser prefix: ` +
|
|
`'${knownBrowserPrefix}'; should set as default PDF handler.`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
log.debug(
|
|
`Current default PDF handler progID does not match known browser prefix; ` +
|
|
`should not set as default PDF handler.`
|
|
);
|
|
return false;
|
|
},
|
|
|
|
/*
|
|
* Set the default browser through the UserChoice registry keys on Windows.
|
|
*
|
|
* NOTE: This does NOT open the System Settings app for manual selection
|
|
* in case of failure. If that is desired, catch the exception and call
|
|
* setDefaultBrowser().
|
|
*
|
|
* @return Promise, resolves when successful, rejects with Error on failure.
|
|
*/
|
|
async setAsDefaultUserChoice() {
|
|
if (AppConstants.platform != "win") {
|
|
throw new Error("Windows-only");
|
|
}
|
|
|
|
log.info("Setting Firefox as default using UserChoice");
|
|
|
|
// We launch the WDBA to handle the registry writes, see
|
|
// SetDefaultBrowserUserChoice() in
|
|
// toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp.
|
|
// This is external in case an overzealous antimalware product decides to
|
|
// quarrantine any program that writes UserChoice, though this has not
|
|
// occurred during extensive testing.
|
|
|
|
let telemetryResult = "ErrOther";
|
|
|
|
try {
|
|
telemetryResult =
|
|
this._userChoiceImpossibleTelemetryResult() ?? "ErrOther";
|
|
if (telemetryResult == "ErrProgID") {
|
|
throw new Error("checkAllProgIDsExist() failed");
|
|
}
|
|
if (telemetryResult == "ErrHash") {
|
|
throw new Error("checkBrowserUserChoiceHashes() failed");
|
|
}
|
|
|
|
const aumi = XreDirProvider.getInstallHash();
|
|
|
|
telemetryResult = "ErrLaunchExe";
|
|
const exeArgs = ["set-default-browser-user-choice", aumi];
|
|
if (NimbusFeatures.shellService.getVariable("setDefaultPDFHandler")) {
|
|
if (this._shouldSetDefaultPDFHandler()) {
|
|
log.info("Setting Firefox as default PDF handler");
|
|
exeArgs.push(".pdf", "FirefoxPDF");
|
|
} else {
|
|
log.info("Not setting Firefox as default PDF handler");
|
|
}
|
|
}
|
|
const exeProcess = await this._callExternalDefaultBrowserAgent({
|
|
arguments: exeArgs,
|
|
});
|
|
telemetryResult = "ErrOther";
|
|
|
|
// Exit codes, see toolkit/mozapps/defaultagent/SetDefaultBrowser.h
|
|
const S_OK = 0;
|
|
const STILL_ACTIVE = 0x103;
|
|
const MOZ_E_NO_PROGID = 0xa0000001;
|
|
const MOZ_E_HASH_CHECK = 0xa0000002;
|
|
const MOZ_E_REJECTED = 0xa0000003;
|
|
const MOZ_E_BUILD = 0xa0000004;
|
|
|
|
const exeWaitTimeoutMs = 2000; // 2 seconds
|
|
const exeWaitPromise = exeProcess.wait();
|
|
const timeoutPromise = new Promise(function(resolve, reject) {
|
|
setTimeout(() => resolve({ exitCode: STILL_ACTIVE }), exeWaitTimeoutMs);
|
|
});
|
|
const { exitCode } = await Promise.race([exeWaitPromise, timeoutPromise]);
|
|
|
|
if (exitCode != S_OK) {
|
|
telemetryResult =
|
|
new Map([
|
|
[STILL_ACTIVE, "ErrExeTimeout"],
|
|
[MOZ_E_NO_PROGID, "ErrExeProgID"],
|
|
[MOZ_E_HASH_CHECK, "ErrExeHash"],
|
|
[MOZ_E_REJECTED, "ErrExeRejected"],
|
|
[MOZ_E_BUILD, "ErrBuild"],
|
|
]).get(exitCode) ?? "ErrExeOther";
|
|
throw new Error(
|
|
`WDBA nonzero exit code ${exitCode}: ${telemetryResult}`
|
|
);
|
|
}
|
|
|
|
telemetryResult = "Success";
|
|
} finally {
|
|
try {
|
|
const histogram = Services.telemetry.getHistogramById(
|
|
"BROWSER_SET_DEFAULT_USER_CHOICE_RESULT"
|
|
);
|
|
histogram.add(telemetryResult);
|
|
} catch (ex) {}
|
|
}
|
|
},
|
|
|
|
// override nsIShellService.setDefaultBrowser() on the ShellService proxy.
|
|
setDefaultBrowser(claimAllTypes, forAllUsers) {
|
|
// On Windows 10, our best chance is to set UserChoice, so try that first.
|
|
if (
|
|
AppConstants.isPlatformAndVersionAtLeast("win", "10") &&
|
|
NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice")
|
|
) {
|
|
// nsWindowsShellService::SetDefaultBrowser() kicks off several
|
|
// operations, but doesn't wait for their result. So we don't need to
|
|
// await the result of setAsDefaultUserChoice() here, either, we just need
|
|
// to fall back in case it fails.
|
|
this.setAsDefaultUserChoice().catch(err => {
|
|
Cu.reportError(err);
|
|
this.shellService.setDefaultBrowser(claimAllTypes, forAllUsers);
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.shellService.setDefaultBrowser(claimAllTypes, forAllUsers);
|
|
},
|
|
|
|
setAsDefault() {
|
|
let claimAllTypes = true;
|
|
let setAsDefaultError = false;
|
|
if (AppConstants.platform == "win") {
|
|
try {
|
|
// In Windows 8+, the UI for selecting default protocol is much
|
|
// nicer than the UI for setting file type associations. So we
|
|
// only show the protocol association screen on Windows 8+.
|
|
// Windows 8 is version 6.2.
|
|
let version = Services.sysinfo.getProperty("version");
|
|
claimAllTypes = parseFloat(version) < 6.2;
|
|
} catch (ex) {}
|
|
}
|
|
try {
|
|
ShellService.setDefaultBrowser(claimAllTypes, false);
|
|
} catch (ex) {
|
|
setAsDefaultError = true;
|
|
Cu.reportError(ex);
|
|
}
|
|
// Here BROWSER_IS_USER_DEFAULT and BROWSER_SET_USER_DEFAULT_ERROR appear
|
|
// to be inverse of each other, but that is only because this function is
|
|
// called when the browser is set as the default. During startup we record
|
|
// the BROWSER_IS_USER_DEFAULT value without recording BROWSER_SET_USER_DEFAULT_ERROR.
|
|
Services.telemetry
|
|
.getHistogramById("BROWSER_IS_USER_DEFAULT")
|
|
.add(!setAsDefaultError);
|
|
Services.telemetry
|
|
.getHistogramById("BROWSER_SET_DEFAULT_ERROR")
|
|
.add(setAsDefaultError);
|
|
},
|
|
|
|
/**
|
|
* Determine if we're the default handler for the given file extension (like
|
|
* ".pdf") or protocol (like "https"). Windows-only for now.
|
|
*
|
|
* @returns true if we are the default handler, false otherwise.
|
|
*/
|
|
isDefaultHandlerFor(aFileExtensionOrProtocol) {
|
|
if (AppConstants.platform == "win") {
|
|
return this.shellService
|
|
.QueryInterface(Ci.nsIWindowsShellService)
|
|
.isDefaultHandlerFor(aFileExtensionOrProtocol);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Checks if Firefox app can and isn't pinned to OS "taskbar."
|
|
*
|
|
* @throws if not called from main process.
|
|
*/
|
|
async doesAppNeedPin(privateBrowsing = false) {
|
|
if (
|
|
Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT
|
|
) {
|
|
throw new Components.Exception(
|
|
"Can't determine pinned from child process",
|
|
Cr.NS_ERROR_NOT_AVAILABLE
|
|
);
|
|
}
|
|
|
|
// Pretend pinning is not needed/supported if remotely disabled.
|
|
if (NimbusFeatures.shellService.getVariable("disablePin")) {
|
|
return false;
|
|
}
|
|
|
|
// Currently this only works on certain Windows versions.
|
|
try {
|
|
// First check if we can even pin the app where an exception means no.
|
|
this.shellService
|
|
.QueryInterface(Ci.nsIWindowsShellService)
|
|
.checkPinCurrentAppToTaskbar(privateBrowsing);
|
|
let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"].getService(
|
|
Ci.nsIWinTaskbar
|
|
);
|
|
|
|
// Then check if we're already pinned.
|
|
return !(await this.shellService.isCurrentAppPinnedToTaskbarAsync(
|
|
privateBrowsing
|
|
? winTaskbar.defaultPrivateGroupId
|
|
: winTaskbar.defaultGroupId
|
|
));
|
|
} catch (ex) {}
|
|
|
|
// Next check mac pinning to dock.
|
|
try {
|
|
// Accessing this.macDockSupport will ensure we're actually running
|
|
// on Mac (it's possible to be on Linux in this block).
|
|
const isInDock = this.macDockSupport.isAppInDock;
|
|
// We can't pin Private Browsing mode on Mac, only a shortcut to the vanilla app
|
|
return privateBrowsing ? false : !isInDock;
|
|
} catch (ex) {}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Pin Firefox app to the OS "taskbar."
|
|
*/
|
|
async pinToTaskbar(privateBrowsing = false) {
|
|
if (await this.doesAppNeedPin(privateBrowsing)) {
|
|
try {
|
|
if (AppConstants.platform == "win") {
|
|
this.shellService.pinCurrentAppToTaskbar(privateBrowsing);
|
|
} else if (AppConstants.platform == "macosx") {
|
|
this.macDockSupport.ensureAppIsPinnedToDock();
|
|
}
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyServiceGetters(ShellServiceInternal, {
|
|
shellService: ["@mozilla.org/browser/shell-service;1", "nsIShellService"],
|
|
macDockSupport: ["@mozilla.org/widget/macdocksupport;1", "nsIMacDockSupport"],
|
|
});
|
|
|
|
/**
|
|
* The external API exported by this module.
|
|
*/
|
|
var ShellService = new Proxy(ShellServiceInternal, {
|
|
get(target, name) {
|
|
if (name in target) {
|
|
return target[name];
|
|
}
|
|
if (target.shellService) {
|
|
return target.shellService[name];
|
|
}
|
|
Services.console.logStringMessage(
|
|
`${name} not found in ShellService: ${target.shellService}`
|
|
);
|
|
return undefined;
|
|
},
|
|
});
|