summaryrefslogtreecommitdiffstats
path: root/browser/components/shell
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/shell/HeadlessShell.sys.mjs262
-rw-r--r--browser/components/shell/ScreenshotChild.sys.mjs31
-rw-r--r--browser/components/shell/ShellService.sys.mjs572
-rw-r--r--browser/components/shell/WindowsDefaultBrowser.cpp162
-rw-r--r--browser/components/shell/WindowsDefaultBrowser.h20
-rw-r--r--browser/components/shell/WindowsUserChoice.cpp422
-rw-r--r--browser/components/shell/WindowsUserChoice.h103
-rw-r--r--browser/components/shell/content/setDesktopBackground.js252
-rw-r--r--browser/components/shell/content/setDesktopBackground.xhtml88
-rw-r--r--browser/components/shell/jar.mn7
-rw-r--r--browser/components/shell/moz.build92
-rw-r--r--browser/components/shell/nsGNOMEShellDBusHelper.cpp535
-rw-r--r--browser/components/shell/nsGNOMEShellDBusHelper.h37
-rw-r--r--browser/components/shell/nsGNOMEShellSearchProvider.cpp442
-rw-r--r--browser/components/shell/nsGNOMEShellSearchProvider.h139
-rw-r--r--browser/components/shell/nsGNOMEShellService.cpp506
-rw-r--r--browser/components/shell/nsGNOMEShellService.h46
-rw-r--r--browser/components/shell/nsIGNOMEShellService.idl23
-rw-r--r--browser/components/shell/nsIMacShellService.idl27
-rw-r--r--browser/components/shell/nsIShellService.idl69
-rw-r--r--browser/components/shell/nsIWindowsShellService.idl187
-rw-r--r--browser/components/shell/nsMacShellService.cpp281
-rw-r--r--browser/components/shell/nsMacShellService.h33
-rw-r--r--browser/components/shell/nsShellService.h11
-rw-r--r--browser/components/shell/nsToolkitShellService.h21
-rw-r--r--browser/components/shell/nsWindowsShellService.cpp1845
-rw-r--r--browser/components/shell/nsWindowsShellService.h38
-rw-r--r--browser/components/shell/search-provider-files/README20
-rw-r--r--browser/components/shell/search-provider-files/firefox-search-provider.ini5
-rw-r--r--browser/components/shell/search-provider-files/firefox.desktop273
-rw-r--r--browser/components/shell/test/browser.ini69
-rw-r--r--browser/components/shell/test/browser_1119088.js167
-rw-r--r--browser/components/shell/test/browser_420786.js105
-rw-r--r--browser/components/shell/test/browser_633221.js11
-rw-r--r--browser/components/shell/test/browser_doesAppNeedPin.js54
-rw-r--r--browser/components/shell/test/browser_headless_screenshot_1.js74
-rw-r--r--browser/components/shell/test/browser_headless_screenshot_2.js48
-rw-r--r--browser/components/shell/test/browser_headless_screenshot_3.js59
-rw-r--r--browser/components/shell/test/browser_headless_screenshot_4.js31
-rw-r--r--browser/components/shell/test/browser_headless_screenshot_cross_origin.js9
-rw-r--r--browser/components/shell/test/browser_headless_screenshot_redirect.js14
-rw-r--r--browser/components/shell/test/browser_setDefaultBrowser.js146
-rw-r--r--browser/components/shell/test/browser_setDefaultPDFHandler.js291
-rw-r--r--browser/components/shell/test/browser_setDesktopBackgroundPreview.js87
-rw-r--r--browser/components/shell/test/head.js159
-rw-r--r--browser/components/shell/test/headless.html6
-rw-r--r--browser/components/shell/test/headless_cross_origin.html7
-rw-r--r--browser/components/shell/test/headless_iframe.html6
-rw-r--r--browser/components/shell/test/headless_redirect.html0
-rw-r--r--browser/components/shell/test/headless_redirect.html^headers^2
-rwxr-xr-xbrowser/components/shell/test/mac_desktop_image.py168
-rw-r--r--browser/components/shell/test/unit/test_macOS_showSecurityPreferences.js30
-rw-r--r--browser/components/shell/test/unit/xpcshell.ini6
53 files changed, 8098 insertions, 0 deletions
diff --git a/browser/components/shell/HeadlessShell.sys.mjs b/browser/components/shell/HeadlessShell.sys.mjs
new file mode 100644
index 0000000000..c87a7a6d56
--- /dev/null
+++ b/browser/components/shell/HeadlessShell.sys.mjs
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { E10SUtils } from "resource://gre/modules/E10SUtils.sys.mjs";
+import { HiddenFrame } from "resource://gre/modules/HiddenFrame.sys.mjs";
+
+// Refrences to the progress listeners to keep them from being gc'ed
+// before they are called.
+const progressListeners = new Set();
+
+export class ScreenshotParent extends JSWindowActorParent {
+ getDimensions(params) {
+ return this.sendQuery("GetDimensions", params);
+ }
+}
+
+ChromeUtils.registerWindowActor("Screenshot", {
+ parent: {
+ esModuleURI: "resource:///modules/HeadlessShell.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///modules/ScreenshotChild.sys.mjs",
+ },
+});
+
+function loadContentWindow(browser, url) {
+ let uri;
+ try {
+ uri = Services.io.newURI(url);
+ } catch (e) {
+ let msg = `Invalid URL passed to loadContentWindow(): ${url}`;
+ console.error(msg);
+ return Promise.reject(new Error(msg));
+ }
+
+ const principal = Services.scriptSecurityManager.getSystemPrincipal();
+ return new Promise((resolve, reject) => {
+ let oa = E10SUtils.predictOriginAttributes({
+ browser,
+ });
+ let loadURIOptions = {
+ triggeringPrincipal: principal,
+ remoteType: E10SUtils.getRemoteTypeForURI(
+ url,
+ true,
+ false,
+ E10SUtils.DEFAULT_REMOTE_TYPE,
+ null,
+ oa
+ ),
+ };
+ browser.loadURI(uri, loadURIOptions);
+ let { webProgress } = browser;
+
+ let progressListener = {
+ onLocationChange(progress, request, location, flags) {
+ // Ignore inner-frame events
+ if (!progress.isTopLevel) {
+ return;
+ }
+ // Ignore events that don't change the document
+ if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ return;
+ }
+ // Ignore the initial about:blank, unless about:blank is requested
+ if (location.spec == "about:blank" && uri.spec != "about:blank") {
+ return;
+ }
+
+ progressListeners.delete(progressListener);
+ webProgress.removeProgressListener(progressListener);
+ resolve();
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+ };
+ progressListeners.add(progressListener);
+ webProgress.addProgressListener(
+ progressListener,
+ Ci.nsIWebProgress.NOTIFY_LOCATION
+ );
+ });
+}
+
+async function takeScreenshot(
+ fullWidth,
+ fullHeight,
+ contentWidth,
+ contentHeight,
+ path,
+ url
+) {
+ let frame;
+ try {
+ frame = new HiddenFrame();
+ let windowlessBrowser = await frame.get();
+
+ let doc = windowlessBrowser.document;
+ let browser = doc.createXULElement("browser");
+ browser.setAttribute("remote", "true");
+ browser.setAttribute("type", "content");
+ browser.setAttribute(
+ "style",
+ `width: ${contentWidth}px; min-width: ${contentWidth}px; height: ${contentHeight}px; min-height: ${contentHeight}px;`
+ );
+ browser.setAttribute("maychangeremoteness", "true");
+ doc.documentElement.appendChild(browser);
+
+ await loadContentWindow(browser, url);
+
+ let actor =
+ browser.browsingContext.currentWindowGlobal.getActor("Screenshot");
+ let dimensions = await actor.getDimensions();
+
+ let canvas = doc.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "html:canvas"
+ );
+ let context = canvas.getContext("2d");
+ let width = dimensions.innerWidth;
+ let height = dimensions.innerHeight;
+ if (fullWidth) {
+ width += dimensions.scrollMaxX - dimensions.scrollMinX;
+ }
+ if (fullHeight) {
+ height += dimensions.scrollMaxY - dimensions.scrollMinY;
+ }
+ canvas.width = width;
+ canvas.height = height;
+ let rect = new DOMRect(0, 0, width, height);
+
+ let snapshot =
+ await browser.browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ 1,
+ "rgb(255, 255, 255)"
+ );
+ context.drawImage(snapshot, 0, 0);
+
+ snapshot.close();
+
+ let blob = await new Promise(resolve => canvas.toBlob(resolve));
+
+ let reader = await new Promise(resolve => {
+ let fr = new FileReader();
+ fr.onloadend = () => resolve(fr);
+ fr.readAsArrayBuffer(blob);
+ });
+
+ await IOUtils.write(path, new Uint8Array(reader.result));
+ dump("Screenshot saved to: " + path + "\n");
+ } catch (e) {
+ dump("Failure taking screenshot: " + e + "\n");
+ } finally {
+ if (frame) {
+ frame.destroy();
+ }
+ }
+}
+
+export let HeadlessShell = {
+ async handleCmdLineArgs(cmdLine, URLlist) {
+ try {
+ // Don't quit even though we don't create a window
+ Services.startup.enterLastWindowClosingSurvivalArea();
+
+ // Default options
+ let fullWidth = true;
+ let fullHeight = true;
+ // Most common screen resolution of Firefox users
+ let contentWidth = 1366;
+ let contentHeight = 768;
+
+ // Parse `window-size`
+ try {
+ var dimensionsStr = cmdLine.handleFlagWithParam("window-size", true);
+ } catch (e) {
+ dump("expected format: --window-size width[,height]\n");
+ return;
+ }
+ if (dimensionsStr) {
+ let success;
+ let dimensions = dimensionsStr.split(",", 2);
+ if (dimensions.length == 1) {
+ success = dimensions[0] > 0;
+ if (success) {
+ fullWidth = false;
+ fullHeight = true;
+ contentWidth = dimensions[0];
+ }
+ } else {
+ success = dimensions[0] > 0 && dimensions[1] > 0;
+ if (success) {
+ fullWidth = false;
+ fullHeight = false;
+ contentWidth = dimensions[0];
+ contentHeight = dimensions[1];
+ }
+ }
+
+ if (!success) {
+ dump("expected format: --window-size width[,height]\n");
+ return;
+ }
+ }
+
+ let urlOrFileToSave = null;
+ try {
+ urlOrFileToSave = cmdLine.handleFlagWithParam("screenshot", true);
+ } catch (e) {
+ // We know that the flag exists so we only get here if there was no parameter.
+ cmdLine.handleFlag("screenshot", true); // Remove `screenshot`
+ }
+
+ // Assume that the remaining arguments that do not start
+ // with a hyphen are URLs
+ for (let i = 0; i < cmdLine.length; ++i) {
+ const argument = cmdLine.getArgument(i);
+ if (argument.startsWith("-")) {
+ dump(`Warning: unrecognized command line flag ${argument}\n`);
+ // To emulate the pre-nsICommandLine behavior, we ignore
+ // the argument after an unrecognized flag.
+ ++i;
+ } else {
+ URLlist.push(argument);
+ }
+ }
+
+ let path = null;
+ if (urlOrFileToSave && !URLlist.length) {
+ // URL was specified next to "-screenshot"
+ // Example: -screenshot https://www.example.com -attach-console
+ URLlist.push(urlOrFileToSave);
+ } else {
+ path = urlOrFileToSave;
+ }
+
+ if (!path) {
+ path = PathUtils.join(cmdLine.workingDirectory.path, "screenshot.png");
+ }
+
+ if (URLlist.length == 1) {
+ await takeScreenshot(
+ fullWidth,
+ fullHeight,
+ contentWidth,
+ contentHeight,
+ path,
+ URLlist[0]
+ );
+ } else {
+ dump("expected exactly one URL when using `screenshot`\n");
+ }
+ } finally {
+ Services.startup.exitLastWindowClosingSurvivalArea();
+ Services.startup.quit(Ci.nsIAppStartup.eForceQuit);
+ }
+ },
+};
diff --git a/browser/components/shell/ScreenshotChild.sys.mjs b/browser/components/shell/ScreenshotChild.sys.mjs
new file mode 100644
index 0000000000..994cb1a27e
--- /dev/null
+++ b/browser/components/shell/ScreenshotChild.sys.mjs
@@ -0,0 +1,31 @@
+/* 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/. */
+
+export class ScreenshotChild extends JSWindowActorChild {
+ receiveMessage(message) {
+ if (message.name == "GetDimensions") {
+ return this.getDimensions();
+ }
+ return null;
+ }
+
+ async getDimensions() {
+ if (this.document.readyState != "complete") {
+ await new Promise(resolve =>
+ this.contentWindow.addEventListener("load", resolve, { once: true })
+ );
+ }
+
+ let { contentWindow } = this;
+
+ return {
+ innerWidth: contentWindow.innerWidth,
+ innerHeight: contentWindow.innerHeight,
+ scrollMinX: contentWindow.scrollMinX,
+ scrollMaxX: contentWindow.scrollMaxX,
+ scrollMinY: contentWindow.scrollMinY,
+ scrollMaxY: contentWindow.scrollMaxY,
+ };
+ }
+}
diff --git a/browser/components/shell/ShellService.sys.mjs b/browser/components/shell/ShellService.sys.mjs
new file mode 100644
index 0000000000..b871040db1
--- /dev/null
+++ b/browser/components/shell/ShellService.sys.mjs
@@ -0,0 +1,572 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ Subprocess: "resource://gre/modules/Subprocess.sys.mjs",
+ WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "XreDirProvider",
+ "@mozilla.org/xre/directory-provider;1",
+ "nsIXREDirProvider"
+);
+
+XPCOMUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ let consoleOptions = {
+ // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+ // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
+ maxLogLevel: "error",
+ 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 = lazy.WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Mozilla\\Firefox",
+ "DefaultBrowserOptOut"
+ );
+ lazy.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 lazy.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 (
+ !lazy.NimbusFeatures.shellService.getVariable(
+ "setDefaultPDFHandlerOnlyReplaceBrowsers"
+ )
+ ) {
+ return true;
+ }
+
+ const handler = this.getDefaultPDFHandler();
+ if (handler === null) {
+ // We only get an exception when something went really wrong. Fail
+ // safely: don't set Firefox as default PDF handler.
+ lazy.log.warn(
+ "Could not determine default PDF handler: not setting Firefox as " +
+ "default PDF handler!"
+ );
+ return false;
+ }
+
+ if (!handler.registered) {
+ lazy.log.debug(
+ "Current default PDF handler has no registered association; " +
+ "should set as default PDF handler."
+ );
+ return true;
+ }
+
+ if (handler.knownBrowser) {
+ lazy.log.debug(
+ "Current default PDF handler progID matches known browser; should " +
+ "set as default PDF handler."
+ );
+ return true;
+ }
+
+ lazy.log.debug(
+ "Current default PDF handler progID does not match known browser " +
+ "prefix; should not set as default PDF handler."
+ );
+ return false;
+ },
+
+ getDefaultPDFHandler() {
+ 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.
+ lazy.log.warn("Failed to queryCurrentDefaultHandlerFor:");
+ return null;
+ }
+
+ if (currentProgID == "") {
+ return { registered: false, knownBrowser: false };
+ }
+
+ const knownBrowserPrefix = knownBrowserPrefixes.find(it =>
+ currentProgID.startsWith(it)
+ );
+
+ if (knownBrowserPrefix) {
+ lazy.log.debug(`Found known browser prefix: ${knownBrowserPrefix}`);
+ }
+
+ return {
+ registered: true,
+ knownBrowser: !!knownBrowserPrefix,
+ };
+ },
+
+ /*
+ * 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");
+ }
+
+ lazy.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 = lazy.XreDirProvider.getInstallHash();
+
+ telemetryResult = "ErrLaunchExe";
+ const exeArgs = ["set-default-browser-user-choice", aumi];
+ if (
+ lazy.NimbusFeatures.shellService.getVariable("setDefaultPDFHandler")
+ ) {
+ if (this._shouldSetDefaultPDFHandler()) {
+ lazy.log.info("Setting Firefox as default PDF handler");
+ exeArgs.push(".pdf", "FirefoxPDF");
+ } else {
+ lazy.log.info("Not setting Firefox as default PDF handler");
+ }
+ }
+ const exeProcess = await this._callExternalDefaultBrowserAgent({
+ arguments: exeArgs,
+ });
+ telemetryResult = "ErrOther";
+ await this._handleWDBAResult(exeProcess);
+ telemetryResult = "Success";
+ } catch (ex) {
+ if (ex instanceof WDBAError) {
+ telemetryResult = ex.telemetryResult;
+ }
+
+ throw ex;
+ } finally {
+ try {
+ const histogram = Services.telemetry.getHistogramById(
+ "BROWSER_SET_DEFAULT_USER_CHOICE_RESULT"
+ );
+ histogram.add(telemetryResult);
+ } catch (ex) {}
+ }
+ },
+
+ async setAsDefaultPDFHandlerUserChoice() {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows-only");
+ }
+
+ // See comment in setAsDefaultUserChoice for an explanation of why we shell
+ // out to WDBA.
+ let telemetryResult = "ErrOther";
+
+ try {
+ const aumi = lazy.XreDirProvider.getInstallHash();
+ const exeProcess = await this._callExternalDefaultBrowserAgent({
+ arguments: [
+ "set-default-extension-handlers-user-choice",
+ aumi,
+ ".pdf",
+ "FirefoxPDF",
+ ],
+ });
+ telemetryResult = "ErrOther";
+ await this._handleWDBAResult(exeProcess);
+ telemetryResult = "Success";
+ } catch (ex) {
+ if (ex instanceof WDBAError) {
+ telemetryResult = ex.telemetryResult;
+ }
+
+ throw ex;
+ } finally {
+ try {
+ const histogram = Services.telemetry.getHistogramById(
+ "BROWSER_SET_DEFAULT_PDF_HANDLER_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") &&
+ lazy.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 => {
+ console.error(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;
+ console.error(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);
+ },
+
+ setAsDefaultPDFHandler(onlyIfKnownBrowser = false) {
+ if (onlyIfKnownBrowser && !this.getDefaultPDFHandler().knownBrowser) {
+ return;
+ }
+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ this.setAsDefaultPDFHandlerUserChoice();
+ }
+ },
+
+ /**
+ * Determine if we're the default handler for the given file extension (like
+ * ".pdf") or protocol (like "https"). Windows-only for now.
+ *
+ * @returns {boolean} 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 (lazy.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.
+ await this.shellService
+ .QueryInterface(Ci.nsIWindowsShellService)
+ .checkPinCurrentAppToTaskbarAsync(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") {
+ await this.shellService.pinCurrentAppToTaskbarAsync(privateBrowsing);
+ } else if (AppConstants.platform == "macosx") {
+ this.macDockSupport.ensureAppIsPinnedToDock();
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ },
+
+ async _handleWDBAResult(exeProcess, exeWaitTimeoutMs = 2000) {
+ // 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 exeWaitPromise = exeProcess.wait();
+ const timeoutPromise = new Promise(function (resolve) {
+ lazy.setTimeout(
+ () => resolve({ exitCode: STILL_ACTIVE }),
+ exeWaitTimeoutMs
+ );
+ });
+
+ const { exitCode } = await Promise.race([exeWaitPromise, timeoutPromise]);
+
+ if (exitCode != S_OK) {
+ const 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 WDBAError(exitCode, telemetryResult);
+ }
+ },
+};
+
+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.
+ */
+export 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;
+ },
+});
+
+class WDBAError extends Error {
+ constructor(exitCode, telemetryResult) {
+ super(`WDBA nonzero exit code ${exitCode}: ${telemetryResult}`);
+
+ this.exitCode = exitCode;
+ this.telemetryResult = telemetryResult;
+ }
+}
diff --git a/browser/components/shell/WindowsDefaultBrowser.cpp b/browser/components/shell/WindowsDefaultBrowser.cpp
new file mode 100644
index 0000000000..34e00be89f
--- /dev/null
+++ b/browser/components/shell/WindowsDefaultBrowser.cpp
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * This file exists so that LaunchModernSettingsDialogDefaultApps can be called
+ * without linking to libxul.
+ */
+#include "WindowsDefaultBrowser.h"
+
+#include "city.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+
+// Needed for access to IApplicationActivationManager
+// This must be before any other includes that might include shlobj.h
+#ifdef _WIN32_WINNT
+# undef _WIN32_WINNT
+#endif
+#define _WIN32_WINNT 0x0600
+#define INITGUID
+#undef NTDDI_VERSION
+#define NTDDI_VERSION NTDDI_WIN8
+#include <shlobj.h>
+
+#include <lm.h>
+#include <shlwapi.h>
+#include <wchar.h>
+#include <windows.h>
+
+#define APP_REG_NAME_BASE L"Firefox-"
+
+static bool IsWindowsLogonConnected() {
+ WCHAR userName[UNLEN + 1];
+ DWORD size = mozilla::ArrayLength(userName);
+ if (!GetUserNameW(userName, &size)) {
+ return false;
+ }
+
+ LPUSER_INFO_24 info;
+ if (NetUserGetInfo(nullptr, userName, 24, (LPBYTE*)&info) != NERR_Success) {
+ return false;
+ }
+ bool connected = info->usri24_internet_identity;
+ NetApiBufferFree(info);
+
+ return connected;
+}
+
+static bool SettingsAppBelievesConnected() {
+ const wchar_t* keyPath = L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations";
+ const wchar_t* valueName = L"IsConnectedAtLogon";
+
+ uint32_t value = 0;
+ DWORD size = sizeof(uint32_t);
+ LSTATUS ls = RegGetValueW(HKEY_CURRENT_USER, keyPath, valueName, RRF_RT_ANY,
+ nullptr, &value, &size);
+ if (ls != ERROR_SUCCESS) {
+ return false;
+ }
+
+ return !!value;
+}
+
+bool GetAppRegName(mozilla::UniquePtr<wchar_t[]>& aAppRegName) {
+ mozilla::UniquePtr<wchar_t[]> appDirStr;
+ bool success = GetInstallDirectory(appDirStr);
+ if (!success) {
+ return success;
+ }
+
+ uint64_t hash = CityHash64(reinterpret_cast<char*>(appDirStr.get()),
+ wcslen(appDirStr.get()) * sizeof(wchar_t));
+
+ const wchar_t* format = L"%s%I64X";
+ int bufferSize = _scwprintf(format, APP_REG_NAME_BASE, hash);
+ ++bufferSize; // Extra byte for terminating null
+ aAppRegName = mozilla::MakeUnique<wchar_t[]>(bufferSize);
+
+ _snwprintf_s(aAppRegName.get(), bufferSize, _TRUNCATE, format,
+ APP_REG_NAME_BASE, hash);
+
+ return success;
+}
+
+bool LaunchControlPanelDefaultPrograms() {
+ // Build the path control.exe path safely
+ WCHAR controlEXEPath[MAX_PATH + 1] = {'\0'};
+ if (!GetSystemDirectoryW(controlEXEPath, MAX_PATH)) {
+ return false;
+ }
+ LPCWSTR controlEXE = L"control.exe";
+ if (wcslen(controlEXEPath) + wcslen(controlEXE) >= MAX_PATH) {
+ return false;
+ }
+ if (!PathAppendW(controlEXEPath, controlEXE)) {
+ return false;
+ }
+
+ const wchar_t* paramFormat =
+ L"control.exe /name Microsoft.DefaultPrograms "
+ L"/page pageDefaultProgram\\pageAdvancedSettings?pszAppName=%s";
+ mozilla::UniquePtr<wchar_t[]> appRegName;
+ GetAppRegName(appRegName);
+ int bufferSize = _scwprintf(paramFormat, appRegName.get());
+ ++bufferSize; // Extra byte for terminating null
+ mozilla::UniquePtr<wchar_t[]> params =
+ mozilla::MakeUnique<wchar_t[]>(bufferSize);
+ _snwprintf_s(params.get(), bufferSize, _TRUNCATE, paramFormat,
+ appRegName.get());
+
+ STARTUPINFOW si = {sizeof(si), 0};
+ si.dwFlags = STARTF_USESHOWWINDOW;
+ si.wShowWindow = SW_SHOWDEFAULT;
+ PROCESS_INFORMATION pi = {0};
+ if (!CreateProcessW(controlEXEPath, params.get(), nullptr, nullptr, FALSE, 0,
+ nullptr, nullptr, &si, &pi)) {
+ return false;
+ }
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+
+ return true;
+}
+
+bool LaunchModernSettingsDialogDefaultApps() {
+ if (!mozilla::IsWindowsBuildOrLater(14965) && !IsWindowsLogonConnected() &&
+ SettingsAppBelievesConnected()) {
+ // Use the classic Control Panel to work around a bug of older
+ // builds of Windows 10.
+ return LaunchControlPanelDefaultPrograms();
+ }
+
+ IApplicationActivationManager* pActivator;
+ HRESULT hr = CoCreateInstance(
+ CLSID_ApplicationActivationManager, nullptr, CLSCTX_INPROC,
+ IID_IApplicationActivationManager, (void**)&pActivator);
+
+ if (SUCCEEDED(hr)) {
+ DWORD pid;
+ hr = pActivator->ActivateApplication(
+ L"windows.immersivecontrolpanel_cw5n1h2txyewy"
+ L"!microsoft.windows.immersivecontrolpanel",
+ L"page=SettingsPageAppsDefaults", AO_NONE, &pid);
+ if (SUCCEEDED(hr)) {
+ // Do not check error because we could at least open
+ // the "Default apps" setting.
+ pActivator->ActivateApplication(
+ L"windows.immersivecontrolpanel_cw5n1h2txyewy"
+ L"!microsoft.windows.immersivecontrolpanel",
+ L"page=SettingsPageAppsDefaults"
+ L"&target=SystemSettings_DefaultApps_Browser",
+ AO_NONE, &pid);
+ }
+ pActivator->Release();
+ return SUCCEEDED(hr);
+ }
+ return true;
+}
diff --git a/browser/components/shell/WindowsDefaultBrowser.h b/browser/components/shell/WindowsDefaultBrowser.h
new file mode 100644
index 0000000000..3e081aad38
--- /dev/null
+++ b/browser/components/shell/WindowsDefaultBrowser.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * This file exists so that LaunchModernSettingsDialogDefaultApps can be called
+ * without linking to libxul.
+ */
+
+#ifndef windowsdefaultbrowser_h____
+#define windowsdefaultbrowser_h____
+
+#include "mozilla/UniquePtr.h"
+
+bool GetAppRegName(mozilla::UniquePtr<wchar_t[]>& aAppRegName);
+bool LaunchControlPanelDefaultPrograms();
+bool LaunchModernSettingsDialogDefaultApps();
+
+#endif // windowsdefaultbrowser_h____
diff --git a/browser/components/shell/WindowsUserChoice.cpp b/browser/components/shell/WindowsUserChoice.cpp
new file mode 100644
index 0000000000..4d6f24704a
--- /dev/null
+++ b/browser/components/shell/WindowsUserChoice.cpp
@@ -0,0 +1,422 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/*
+ * Generate and check the UserChoice Hash, which protects file and protocol
+ * associations on Windows 10.
+ *
+ * NOTE: This is also used in the WDBA, so it avoids XUL and XPCOM.
+ *
+ * References:
+ * - PS-SFTA by Danysys <https://github.com/DanysysTeam/PS-SFTA>
+ * - based on a PureBasic version by LMongrain
+ * <https://github.com/DanysysTeam/SFTA>
+ * - AssocHashGen by "halfmeasuresdisabled", see bug 1225660 and
+ * <https://www.reddit.com/r/ReverseEngineering/comments/3t7q9m/assochashgen_a_reverse_engineered_version_of/>
+ * - SetUserFTA changelog
+ * <https://kolbi.cz/blog/2017/10/25/setuserfta-userchoice-hash-defeated-set-file-type-associations-per-user/>
+ */
+
+#include <windows.h>
+#include <sddl.h> // for ConvertSidToStringSidW
+#include <wincrypt.h> // for CryptoAPI base64
+#include <bcrypt.h> // for CNG MD5
+#include <winternl.h> // for NT_SUCCESS()
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/UniquePtr.h"
+#include "nsWindowsHelpers.h"
+
+#include "WindowsUserChoice.h"
+
+using namespace mozilla;
+
+UniquePtr<wchar_t[]> GetCurrentUserStringSid() {
+ HANDLE rawProcessToken;
+ if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY,
+ &rawProcessToken)) {
+ return nullptr;
+ }
+ nsAutoHandle processToken(rawProcessToken);
+
+ DWORD userSize = 0;
+ if (!(!::GetTokenInformation(processToken.get(), TokenUser, nullptr, 0,
+ &userSize) &&
+ GetLastError() == ERROR_INSUFFICIENT_BUFFER)) {
+ return nullptr;
+ }
+
+ auto userBytes = MakeUnique<unsigned char[]>(userSize);
+ if (!::GetTokenInformation(processToken.get(), TokenUser, userBytes.get(),
+ userSize, &userSize)) {
+ return nullptr;
+ }
+
+ wchar_t* rawSid = nullptr;
+ if (!::ConvertSidToStringSidW(
+ reinterpret_cast<PTOKEN_USER>(userBytes.get())->User.Sid, &rawSid)) {
+ return nullptr;
+ }
+ UniquePtr<wchar_t, LocalFreeDeleter> sid(rawSid);
+
+ // Copy instead of passing UniquePtr<wchar_t, LocalFreeDeleter> back to
+ // the caller.
+ int sidLen = ::lstrlenW(sid.get()) + 1;
+ auto outSid = MakeUnique<wchar_t[]>(sidLen);
+ memcpy(outSid.get(), sid.get(), sidLen * sizeof(wchar_t));
+
+ return outSid;
+}
+
+/*
+ * Create the string which becomes the input to the UserChoice hash.
+ *
+ * @see GenerateUserChoiceHash() for parameters.
+ *
+ * @return The formatted string, nullptr on failure.
+ *
+ * NOTE: This uses the format as of Windows 10 20H2 (latest as of this writing),
+ * used at least since 1803.
+ * There was at least one older version, not currently supported: On Win10 RTM
+ * (build 10240, aka 1507) the hash function is the same, but the timestamp and
+ * User Experience string aren't included; instead (for protocols) the string
+ * ends with the exe path. The changelog of SetUserFTA suggests the algorithm
+ * changed in 1703, so there may be two versions: before 1703, and 1703 to now.
+ */
+static UniquePtr<wchar_t[]> FormatUserChoiceString(const wchar_t* aExt,
+ const wchar_t* aUserSid,
+ const wchar_t* aProgId,
+ SYSTEMTIME aTimestamp) {
+ aTimestamp.wSecond = 0;
+ aTimestamp.wMilliseconds = 0;
+
+ FILETIME fileTime = {0};
+ if (!::SystemTimeToFileTime(&aTimestamp, &fileTime)) {
+ return nullptr;
+ }
+
+ // This string is built into Windows as part of the UserChoice hash algorithm.
+ // It might vary across Windows SKUs (e.g. Windows 10 vs. Windows Server), or
+ // across builds of the same SKU, but this is the only currently known
+ // version. There isn't any known way of deriving it, so we assume this
+ // constant value. If we are wrong, we will not be able to generate correct
+ // UserChoice hashes.
+ const wchar_t* userExperience =
+ L"User Choice set via Windows User Experience "
+ L"{D18B6DD5-6124-4341-9318-804003BAFA0B}";
+
+ const wchar_t* userChoiceFmt =
+ L"%s%s%s"
+ L"%08lx"
+ L"%08lx"
+ L"%s";
+ int userChoiceLen = _scwprintf(userChoiceFmt, aExt, aUserSid, aProgId,
+ fileTime.dwHighDateTime,
+ fileTime.dwLowDateTime, userExperience);
+ userChoiceLen += 1; // _scwprintf does not include the terminator
+
+ auto userChoice = MakeUnique<wchar_t[]>(userChoiceLen);
+ _snwprintf_s(userChoice.get(), userChoiceLen, _TRUNCATE, userChoiceFmt, aExt,
+ aUserSid, aProgId, fileTime.dwHighDateTime,
+ fileTime.dwLowDateTime, userExperience);
+
+ ::CharLowerW(userChoice.get());
+
+ return userChoice;
+}
+
+// @return The MD5 hash of the input, nullptr on failure.
+static UniquePtr<DWORD[]> CNG_MD5(const unsigned char* bytes, ULONG bytesLen) {
+ constexpr ULONG MD5_BYTES = 16;
+ constexpr ULONG MD5_DWORDS = MD5_BYTES / sizeof(DWORD);
+ UniquePtr<DWORD[]> hash;
+
+ BCRYPT_ALG_HANDLE hAlg = nullptr;
+ if (NT_SUCCESS(::BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM,
+ nullptr, 0))) {
+ BCRYPT_HASH_HANDLE hHash = nullptr;
+ // As of Windows 7 the hash handle will manage its own object buffer when
+ // pbHashObject is nullptr and cbHashObject is 0.
+ if (NT_SUCCESS(
+ ::BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0))) {
+ // BCryptHashData promises not to modify pbInput.
+ if (NT_SUCCESS(::BCryptHashData(hHash, const_cast<unsigned char*>(bytes),
+ bytesLen, 0))) {
+ hash = MakeUnique<DWORD[]>(MD5_DWORDS);
+ if (!NT_SUCCESS(::BCryptFinishHash(
+ hHash, reinterpret_cast<unsigned char*>(hash.get()),
+ MD5_DWORDS * sizeof(DWORD), 0))) {
+ hash.reset();
+ }
+ }
+ ::BCryptDestroyHash(hHash);
+ }
+ ::BCryptCloseAlgorithmProvider(hAlg, 0);
+ }
+
+ return hash;
+}
+
+// @return The input bytes encoded as base64, nullptr on failure.
+static UniquePtr<wchar_t[]> CryptoAPI_Base64Encode(const unsigned char* bytes,
+ DWORD bytesLen) {
+ DWORD base64Len = 0;
+ if (!::CryptBinaryToStringW(bytes, bytesLen,
+ CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
+ nullptr, &base64Len)) {
+ return nullptr;
+ }
+ auto base64 = MakeUnique<wchar_t[]>(base64Len);
+ if (!::CryptBinaryToStringW(bytes, bytesLen,
+ CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
+ base64.get(), &base64Len)) {
+ return nullptr;
+ }
+
+ return base64;
+}
+
+static inline DWORD WordSwap(DWORD v) { return (v >> 16) | (v << 16); }
+
+/*
+ * Generate the UserChoice Hash.
+ *
+ * This implementation is based on the references listed above.
+ * It is organized to show the logic as clearly as possible, but at some
+ * point the reasoning is just "this is how it works".
+ *
+ * @param inputString A null-terminated string to hash.
+ *
+ * @return The base64-encoded hash, or nullptr on failure.
+ */
+static UniquePtr<wchar_t[]> HashString(const wchar_t* inputString) {
+ auto inputBytes = reinterpret_cast<const unsigned char*>(inputString);
+ int inputByteCount = (::lstrlenW(inputString) + 1) * sizeof(wchar_t);
+
+ constexpr size_t DWORDS_PER_BLOCK = 2;
+ constexpr size_t BLOCK_SIZE = sizeof(DWORD) * DWORDS_PER_BLOCK;
+ // Incomplete blocks are ignored.
+ int blockCount = inputByteCount / BLOCK_SIZE;
+
+ if (blockCount == 0) {
+ return nullptr;
+ }
+
+ // Compute an MD5 hash. md5[0] and md5[1] will be used as constant multipliers
+ // in the scramble below.
+ auto md5 = CNG_MD5(inputBytes, inputByteCount);
+ if (!md5) {
+ return nullptr;
+ }
+
+ // The following loop effectively computes two checksums, scrambled like a
+ // hash after every DWORD is added.
+
+ // Constant multipliers for the scramble, one set for each DWORD in a block.
+ const DWORD C0s[DWORDS_PER_BLOCK][5] = {
+ {md5[0] | 1, 0xCF98B111uL, 0x87085B9FuL, 0x12CEB96DuL, 0x257E1D83uL},
+ {md5[1] | 1, 0xA27416F5uL, 0xD38396FFuL, 0x7C932B89uL, 0xBFA49F69uL}};
+ const DWORD C1s[DWORDS_PER_BLOCK][5] = {
+ {md5[0] | 1, 0xEF0569FBuL, 0x689B6B9FuL, 0x79F8A395uL, 0xC3EFEA97uL},
+ {md5[1] | 1, 0xC31713DBuL, 0xDDCD1F0FuL, 0x59C3AF2DuL, 0x35BD1EC9uL}};
+
+ // The checksums.
+ DWORD h0 = 0;
+ DWORD h1 = 0;
+ // Accumulated total of the checksum after each DWORD.
+ DWORD h0Acc = 0;
+ DWORD h1Acc = 0;
+
+ for (int i = 0; i < blockCount; ++i) {
+ for (size_t j = 0; j < DWORDS_PER_BLOCK; ++j) {
+ const DWORD* C0 = C0s[j];
+ const DWORD* C1 = C1s[j];
+
+ DWORD input;
+ memcpy(&input, &inputBytes[(i * DWORDS_PER_BLOCK + j) * sizeof(DWORD)],
+ sizeof(DWORD));
+
+ h0 += input;
+ // Scramble 0
+ h0 *= C0[0];
+ h0 = WordSwap(h0) * C0[1];
+ h0 = WordSwap(h0) * C0[2];
+ h0 = WordSwap(h0) * C0[3];
+ h0 = WordSwap(h0) * C0[4];
+ h0Acc += h0;
+
+ h1 += input;
+ // Scramble 1
+ h1 = WordSwap(h1) * C1[1] + h1 * C1[0];
+ h1 = (h1 >> 16) * C1[2] + h1 * C1[3];
+ h1 = WordSwap(h1) * C1[4] + h1;
+ h1Acc += h1;
+ }
+ }
+
+ DWORD hash[2] = {h0 ^ h1, h0Acc ^ h1Acc};
+
+ return CryptoAPI_Base64Encode(reinterpret_cast<const unsigned char*>(hash),
+ sizeof(hash));
+}
+
+UniquePtr<wchar_t[]> GetAssociationKeyPath(const wchar_t* aExt) {
+ const wchar_t* keyPathFmt;
+ if (aExt[0] == L'.') {
+ keyPathFmt =
+ L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\%s";
+ } else {
+ keyPathFmt =
+ L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations\\"
+ L"UrlAssociations\\%s";
+ }
+
+ int keyPathLen = _scwprintf(keyPathFmt, aExt);
+ keyPathLen += 1; // _scwprintf does not include the terminator
+
+ auto keyPath = MakeUnique<wchar_t[]>(keyPathLen);
+ _snwprintf_s(keyPath.get(), keyPathLen, _TRUNCATE, keyPathFmt, aExt);
+
+ return keyPath;
+}
+
+UniquePtr<wchar_t[]> GenerateUserChoiceHash(const wchar_t* aExt,
+ const wchar_t* aUserSid,
+ const wchar_t* aProgId,
+ SYSTEMTIME aTimestamp) {
+ auto userChoice = FormatUserChoiceString(aExt, aUserSid, aProgId, aTimestamp);
+ if (!userChoice) {
+ return nullptr;
+ }
+ return HashString(userChoice.get());
+}
+
+/*
+ * NOTE: The passed-in current user SID is used here, instead of getting the SID
+ * for the owner of the key. We are assuming that this key in HKCU is owned by
+ * the current user, since we want to replace that key ourselves. If the key is
+ * owned by someone else, then this check will fail; this is ok because we would
+ * likely not want to replace that other user's key anyway.
+ */
+CheckUserChoiceHashResult CheckUserChoiceHash(const wchar_t* aExt,
+ const wchar_t* aUserSid) {
+ auto keyPath = GetAssociationKeyPath(aExt);
+ if (!keyPath) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ HKEY rawAssocKey;
+ if (::RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.get(), 0, KEY_READ,
+ &rawAssocKey) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ nsAutoRegKey assocKey(rawAssocKey);
+
+ FILETIME lastWriteFileTime;
+ {
+ HKEY rawUserChoiceKey;
+ if (::RegOpenKeyExW(assocKey.get(), L"UserChoice", 0, KEY_READ,
+ &rawUserChoiceKey) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ nsAutoRegKey userChoiceKey(rawUserChoiceKey);
+
+ if (::RegQueryInfoKeyW(userChoiceKey.get(), nullptr, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+ nullptr, &lastWriteFileTime) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ }
+
+ SYSTEMTIME lastWriteSystemTime;
+ if (!::FileTimeToSystemTime(&lastWriteFileTime, &lastWriteSystemTime)) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ // Read ProgId
+ DWORD dataSizeBytes = 0;
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ,
+ nullptr, nullptr, &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ // +1 in case dataSizeBytes was odd, +1 to ensure termination
+ DWORD dataSizeChars = (dataSizeBytes / sizeof(wchar_t)) + 2;
+ UniquePtr<wchar_t[]> progId(new wchar_t[dataSizeChars]());
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"ProgId", RRF_RT_REG_SZ,
+ nullptr, progId.get(), &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ // Read Hash
+ dataSizeBytes = 0;
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"Hash", RRF_RT_REG_SZ,
+ nullptr, nullptr, &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+ dataSizeChars = (dataSizeBytes / sizeof(wchar_t)) + 2;
+ UniquePtr<wchar_t[]> storedHash(new wchar_t[dataSizeChars]());
+ if (::RegGetValueW(assocKey.get(), L"UserChoice", L"Hash", RRF_RT_REG_SZ,
+ nullptr, storedHash.get(),
+ &dataSizeBytes) != ERROR_SUCCESS) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ auto computedHash =
+ GenerateUserChoiceHash(aExt, aUserSid, progId.get(), lastWriteSystemTime);
+ if (!computedHash) {
+ return CheckUserChoiceHashResult::ERR_OTHER;
+ }
+
+ if (::CompareStringOrdinal(computedHash.get(), -1, storedHash.get(), -1,
+ FALSE) != CSTR_EQUAL) {
+ return CheckUserChoiceHashResult::ERR_MISMATCH;
+ }
+
+ return CheckUserChoiceHashResult::OK_V1;
+}
+
+bool CheckBrowserUserChoiceHashes() {
+ auto userSid = GetCurrentUserStringSid();
+ if (!userSid) {
+ return false;
+ }
+
+ const wchar_t* exts[] = {L"https", L"http", L".html", L".htm"};
+
+ for (size_t i = 0; i < ArrayLength(exts); ++i) {
+ switch (CheckUserChoiceHash(exts[i], userSid.get())) {
+ case CheckUserChoiceHashResult::OK_V1:
+ break;
+ case CheckUserChoiceHashResult::ERR_MISMATCH:
+ case CheckUserChoiceHashResult::ERR_OTHER:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+UniquePtr<wchar_t[]> FormatProgID(const wchar_t* aProgIDBase,
+ const wchar_t* aAumi) {
+ const wchar_t* progIDFmt = L"%s-%s";
+ int progIDLen = _scwprintf(progIDFmt, aProgIDBase, aAumi);
+ progIDLen += 1; // _scwprintf does not include the terminator
+
+ auto progID = MakeUnique<wchar_t[]>(progIDLen);
+ _snwprintf_s(progID.get(), progIDLen, _TRUNCATE, progIDFmt, aProgIDBase,
+ aAumi);
+
+ return progID;
+}
+
+bool CheckProgIDExists(const wchar_t* aProgID) {
+ HKEY key;
+ if (::RegOpenKeyExW(HKEY_CLASSES_ROOT, aProgID, 0, KEY_READ, &key) !=
+ ERROR_SUCCESS) {
+ return false;
+ }
+ ::RegCloseKey(key);
+ return true;
+}
diff --git a/browser/components/shell/WindowsUserChoice.h b/browser/components/shell/WindowsUserChoice.h
new file mode 100644
index 0000000000..d7e887b667
--- /dev/null
+++ b/browser/components/shell/WindowsUserChoice.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef SHELL_WINDOWSUSERCHOICE_H__
+#define SHELL_WINDOWSUSERCHOICE_H__
+
+#include <windows.h>
+
+#include "mozilla/UniquePtr.h"
+
+/*
+ * Check the UserChoice Hashes for https, http, .html, .htm
+ *
+ * This should be checked before attempting to set a new default browser via
+ * the UserChoice key, to confirm our understanding of the existing hash.
+ * If an incorrect hash is written, Windows will prompt the user to choose a
+ * new default (or, in recent versions, it will just reset the default to Edge).
+ *
+ * Assuming that the existing hash value is correct (since Windows is fairly
+ * diligent about replacing bad keys), if we can recompute it from scratch,
+ * then we should be able to compute a correct hash for our new UserChoice key.
+ *
+ * @return true if we matched all the hashes, false otherwise.
+ */
+bool CheckBrowserUserChoiceHashes();
+
+/*
+ * Result from CheckUserChoiceHash()
+ *
+ * NOTE: Currently the only positive result is OK_V1 , but the enum
+ * could be extended to indicate different versions of the hash.
+ */
+enum class CheckUserChoiceHashResult {
+ OK_V1, // Matched the current version of the hash (as of Win10 20H2).
+ ERR_MISMATCH, // The hash did not match.
+ ERR_OTHER, // Error reading or generating the hash.
+};
+
+/*
+ * Generate a UserChoice Hash, compare it with the one that is stored.
+ *
+ * See comments on CheckBrowserUserChoiceHashes(), which calls this to check
+ * each of the browser associations.
+ *
+ * @param aExt File extension or protocol association to check
+ * @param aUserSid String SID of the current user
+ *
+ * @return Result of the check, see CheckUserChoiceHashResult
+ */
+CheckUserChoiceHashResult CheckUserChoiceHash(const wchar_t* aExt,
+ const wchar_t* aUserSid);
+
+/*
+ * Get the registry path for the given association, file extension or protocol.
+ *
+ * @return The path, or nullptr on failure.
+ */
+mozilla::UniquePtr<wchar_t[]> GetAssociationKeyPath(const wchar_t* aExt);
+
+/*
+ * Get the current user's SID
+ *
+ * @return String SID for the user of the current process, nullptr on failure.
+ */
+mozilla::UniquePtr<wchar_t[]> GetCurrentUserStringSid();
+
+/*
+ * Generate the UserChoice Hash
+ *
+ * @param aExt file extension or protocol being registered
+ * @param aUserSid string SID of the current user
+ * @param aProgId ProgId to associate with aExt
+ * @param aTimestamp approximate write time of the UserChoice key (within
+ * the same minute)
+ *
+ * @return UserChoice Hash, nullptr on failure.
+ */
+mozilla::UniquePtr<wchar_t[]> GenerateUserChoiceHash(const wchar_t* aExt,
+ const wchar_t* aUserSid,
+ const wchar_t* aProgId,
+ SYSTEMTIME aTimestamp);
+
+/*
+ * Build a ProgID from a base and AUMI
+ *
+ * @param aProgIDBase A base, such as FirefoxHTML or FirefoxURL
+ * @param aAumi The AUMI of the installation
+ *
+ * @return Formatted ProgID.
+ */
+mozilla::UniquePtr<wchar_t[]> FormatProgID(const wchar_t* aProgIDBase,
+ const wchar_t* aAumi);
+
+/*
+ * Check that the given ProgID exists in HKCR
+ *
+ * @return true if it could be opened for reading, false otherwise.
+ */
+bool CheckProgIDExists(const wchar_t* aProgID);
+
+#endif // SHELL_WINDOWSUSERCHOICE_H__
diff --git a/browser/components/shell/content/setDesktopBackground.js b/browser/components/shell/content/setDesktopBackground.js
new file mode 100644
index 0000000000..be6b108432
--- /dev/null
+++ b/browser/components/shell/content/setDesktopBackground.js
@@ -0,0 +1,252 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from /browser/base/content/utilityOverlay.js */
+
+var gSetBackground = {
+ _position: AppConstants.platform == "macosx" ? "STRETCH" : "",
+ _backgroundColor: AppConstants.platform != "macosx" ? 0 : undefined,
+ _screenWidth: 0,
+ _screenHeight: 0,
+ _image: null,
+ _canvas: null,
+ _imageName: null,
+
+ get _shell() {
+ return Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIShellService
+ );
+ },
+
+ load() {
+ this._canvas = document.getElementById("screen");
+ this._screenWidth = screen.width;
+ this._screenHeight = screen.height;
+ // Cap ratio to 4 so the dialog width doesn't get ridiculous. Highest
+ // regular screens seem to be 32:9 (3.56) according to Wikipedia.
+ let screenRatio = Math.min(this._screenWidth / this._screenHeight, 4);
+ this._canvas.width = this._canvas.height * screenRatio;
+ document.getElementById("preview-unavailable").style.width =
+ this._canvas.width + "px";
+
+ if (AppConstants.platform == "macosx") {
+ document
+ .getElementById("SetDesktopBackgroundDialog")
+ .getButton("accept").hidden = true;
+ } else {
+ let multiMonitors = false;
+ if (AppConstants.platform == "linux") {
+ // getMonitors only ever returns the primary monitor on Linux, so just
+ // always show the option
+ multiMonitors = true;
+ } else {
+ const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ const monitors = gfxInfo.getMonitors();
+ multiMonitors = monitors.length > 1;
+ }
+
+ if (
+ !multiMonitors ||
+ AppConstants.isPlatformAndVersionAtMost("win", 6.1)
+ ) {
+ // Hide span option if < Win8 since that's when it was introduced.
+ document.getElementById("spanPosition").hidden = true;
+ }
+ }
+
+ document.addEventListener("dialogaccept", function () {
+ gSetBackground.setDesktopBackground();
+ });
+ // make sure that the correct dimensions will be used
+ setTimeout(
+ function (self) {
+ self.init(window.arguments[0], window.arguments[1]);
+ },
+ 0,
+ this
+ );
+ },
+
+ init(aImage, aImageName) {
+ this._image = aImage;
+ this._imageName = aImageName;
+
+ // set the size of the coordinate space
+ this._canvas.width = this._canvas.clientWidth;
+ this._canvas.height = this._canvas.clientHeight;
+
+ var ctx = this._canvas.getContext("2d");
+ ctx.scale(
+ this._canvas.clientWidth / this._screenWidth,
+ this._canvas.clientHeight / this._screenHeight
+ );
+
+ if (AppConstants.platform != "macosx") {
+ this._initColor();
+ } else {
+ // Make sure to reset the button state in case the user has already
+ // set an image as their desktop background.
+ var setDesktopBackground = document.getElementById(
+ "setDesktopBackground"
+ );
+ setDesktopBackground.hidden = false;
+ var bundle = document.getElementById("backgroundBundle");
+ setDesktopBackground.label = bundle.getString("DesktopBackgroundSet");
+ setDesktopBackground.disabled = false;
+
+ document.getElementById("showDesktopPreferences").hidden = true;
+ }
+ this.updatePosition();
+ },
+
+ setDesktopBackground() {
+ if (AppConstants.platform != "macosx") {
+ Services.xulStore.persist(
+ document.getElementById("menuPosition"),
+ "value"
+ );
+ this._shell.desktopBackgroundColor = this._hexStringToLong(
+ this._backgroundColor
+ );
+ } else {
+ Services.obs.addObserver(this, "shell:desktop-background-changed");
+
+ var bundle = document.getElementById("backgroundBundle");
+ var setDesktopBackground = document.getElementById(
+ "setDesktopBackground"
+ );
+ setDesktopBackground.disabled = true;
+ setDesktopBackground.label = bundle.getString(
+ "DesktopBackgroundDownloading"
+ );
+ }
+ this._shell.setDesktopBackground(
+ this._image,
+ Ci.nsIShellService["BACKGROUND_" + this._position],
+ this._imageName
+ );
+ },
+
+ updatePosition() {
+ var ctx = this._canvas.getContext("2d");
+ ctx.clearRect(0, 0, this._screenWidth, this._screenHeight);
+ document.getElementById("preview-unavailable").hidden = true;
+
+ if (AppConstants.platform != "macosx") {
+ this._position = document.getElementById("menuPosition").value;
+ }
+
+ switch (this._position) {
+ case "TILE":
+ ctx.save();
+ ctx.fillStyle = ctx.createPattern(this._image, "repeat");
+ ctx.fillRect(0, 0, this._screenWidth, this._screenHeight);
+ ctx.restore();
+ break;
+ case "STRETCH":
+ ctx.drawImage(this._image, 0, 0, this._screenWidth, this._screenHeight);
+ break;
+ case "CENTER": {
+ let x = (this._screenWidth - this._image.naturalWidth) / 2;
+ let y = (this._screenHeight - this._image.naturalHeight) / 2;
+ ctx.drawImage(this._image, x, y);
+ break;
+ }
+ case "FILL": {
+ // Try maxing width first, overflow height.
+ let widthRatio = this._screenWidth / this._image.naturalWidth;
+ let width = this._image.naturalWidth * widthRatio;
+ let height = this._image.naturalHeight * widthRatio;
+ if (height < this._screenHeight) {
+ // Height less than screen, max height and overflow width.
+ let heightRatio = this._screenHeight / this._image.naturalHeight;
+ width = this._image.naturalWidth * heightRatio;
+ height = this._image.naturalHeight * heightRatio;
+ }
+ let x = (this._screenWidth - width) / 2;
+ let y = (this._screenHeight - height) / 2;
+ ctx.drawImage(this._image, x, y, width, height);
+ break;
+ }
+ case "FIT": {
+ // Try maxing width first, top and bottom borders.
+ let widthRatio = this._screenWidth / this._image.naturalWidth;
+ let width = this._image.naturalWidth * widthRatio;
+ let height = this._image.naturalHeight * widthRatio;
+ let x = 0;
+ let y = (this._screenHeight - height) / 2;
+ if (height > this._screenHeight) {
+ // Height overflow, maximise height, side borders.
+ let heightRatio = this._screenHeight / this._image.naturalHeight;
+ width = this._image.naturalWidth * heightRatio;
+ height = this._image.naturalHeight * heightRatio;
+ x = (this._screenWidth - width) / 2;
+ y = 0;
+ }
+ ctx.drawImage(this._image, x, y, width, height);
+ break;
+ }
+ case "SPAN": {
+ document.getElementById("preview-unavailable").hidden = false;
+ ctx.fillStyle = "#222";
+ ctx.fillRect(0, 0, this._screenWidth, this._screenHeight);
+ ctx.stroke();
+ }
+ }
+ },
+};
+
+if (AppConstants.platform != "macosx") {
+ gSetBackground._initColor = function () {
+ var color = this._shell.desktopBackgroundColor;
+
+ const rMask = 4294901760;
+ const gMask = 65280;
+ const bMask = 255;
+ var r = (color & rMask) >> 16;
+ var g = (color & gMask) >> 8;
+ var b = color & bMask;
+ this.updateColor(this._rgbToHex(r, g, b));
+
+ var colorpicker = document.getElementById("desktopColor");
+ colorpicker.value = this._backgroundColor;
+ };
+
+ gSetBackground.updateColor = function (aColor) {
+ this._backgroundColor = aColor;
+ this._canvas.style.backgroundColor = aColor;
+ };
+
+ // Converts a color string in the format "#RRGGBB" to an integer.
+ gSetBackground._hexStringToLong = function (aString) {
+ return (
+ (parseInt(aString.substring(1, 3), 16) << 16) |
+ (parseInt(aString.substring(3, 5), 16) << 8) |
+ parseInt(aString.substring(5, 7), 16)
+ );
+ };
+
+ gSetBackground._rgbToHex = function (aR, aG, aB) {
+ return (
+ "#" +
+ [aR, aG, aB]
+ .map(aInt => aInt.toString(16).replace(/^(.)$/, "0$1"))
+ .join("")
+ .toUpperCase()
+ );
+ };
+} else {
+ gSetBackground.observe = function (aSubject, aTopic, aData) {
+ if (aTopic == "shell:desktop-background-changed") {
+ document.getElementById("setDesktopBackground").hidden = true;
+ document.getElementById("showDesktopPreferences").hidden = false;
+
+ Services.obs.removeObserver(this, "shell:desktop-background-changed");
+ }
+ };
+
+ gSetBackground.showDesktopPrefs = function () {
+ this._shell.QueryInterface(Ci.nsIMacShellService).showDesktopPreferences();
+ };
+}
diff --git a/browser/components/shell/content/setDesktopBackground.xhtml b/browser/components/shell/content/setDesktopBackground.xhtml
new file mode 100644
index 0000000000..95b05ec277
--- /dev/null
+++ b/browser/components/shell/content/setDesktopBackground.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?> <!-- -*- Mode: HTML -*- -->
+
+# 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/.
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/setDesktopBackground.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="Shell:SetDesktopBackground"
+ onload="gSetBackground.load();"
+ data-l10n-id="set-desktop-background-window"
+ style="min-width: 30em;">
+
+<linkset>
+ <html:link rel="localization" href="browser/setDesktopBackground.ftl"/>
+</linkset>
+
+<dialog id="SetDesktopBackgroundDialog"
+#ifndef XP_MACOSX
+ buttons="accept,cancel"
+#else
+ buttons="accept"
+#endif
+ buttonidaccept="set-desktop-background-accept">
+
+#ifdef XP_MACOSX
+#include ../../../base/content/macWindow.inc.xhtml
+#else
+ <script src="chrome://browser/content/utilityOverlay.js"/>
+#endif
+
+ <stringbundle id="backgroundBundle"
+ src="chrome://browser/locale/shellservice.properties"/>
+ <script src="chrome://browser/content/setDesktopBackground.js"/>
+ <script src="chrome://global/content/contentAreaUtils.js"/>
+
+#ifndef XP_MACOSX
+ <hbox align="center">
+ <label data-l10n-id="set-background-position"/>
+ <menulist id="menuPosition"
+ oncommand="gSetBackground.updatePosition();" native="true">
+ <menupopup>
+ <menuitem data-l10n-id="set-background-center" value="CENTER"/>
+ <menuitem data-l10n-id="set-background-tile" value="TILE"/>
+ <menuitem data-l10n-id="set-background-stretch" value="STRETCH"/>
+ <menuitem data-l10n-id="set-background-fill" value="FILL"/>
+ <menuitem data-l10n-id="set-background-fit" value="FIT"/>
+ <menuitem data-l10n-id="set-background-span" value="SPAN" id="spanPosition"/>
+ </menupopup>
+ </menulist>
+ <spacer flex="1"/>
+ <label data-l10n-id="set-background-color"/>
+ <html:input id="desktopColor"
+ type="color"
+ onchange="gSetBackground.updateColor(this.value);"/>
+ </hbox>
+#endif
+
+ <vbox align="center">
+ <!-- default to 16:9, will be adjusted to match user's actual screen -->
+ <stack>
+ <html:canvas id="screen" width="202" height="114" role="presentation"/>
+ <vbox pack="center">
+ <html:p id="preview-unavailable" hidden="" data-l10n-id="set-background-preview-unavailable"></html:p>
+ </vbox>
+ </stack>
+ <image id="monitor-base"/>
+ </vbox>
+
+#ifdef XP_MACOSX
+ <separator/>
+
+ <hbox pack="end">
+ <button id="setDesktopBackground"
+ data-l10n-id="set-desktop-background-accept"
+ oncommand="gSetBackground.setDesktopBackground();"/>
+ <button id="showDesktopPreferences"
+ data-l10n-id="open-desktop-prefs"
+ oncommand="gSetBackground.showDesktopPrefs();"
+ hidden="true"/>
+ </hbox>
+#endif
+
+</dialog>
+</window>
diff --git a/browser/components/shell/jar.mn b/browser/components/shell/jar.mn
new file mode 100644
index 0000000000..0816cddd68
--- /dev/null
+++ b/browser/components/shell/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+browser.jar:
+* content/browser/setDesktopBackground.xhtml (content/setDesktopBackground.xhtml)
+ content/browser/setDesktopBackground.js (content/setDesktopBackground.js)
diff --git a/browser/components/shell/moz.build b/browser/components/shell/moz.build
new file mode 100644
index 0000000000..eb88cb287d
--- /dev/null
+++ b/browser/components/shell/moz.build
@@ -0,0 +1,92 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# For BinaryPath::GetLong for Windows
+LOCAL_INCLUDES += ["/xpcom/build"]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.ini"]
+XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+XPIDL_SOURCES += [
+ "nsIShellService.idl",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ XPIDL_SOURCES += [
+ "nsIMacShellService.idl",
+ ]
+
+ SOURCES += [
+ "nsMacShellService.cpp",
+ ]
+
+ LOCAL_INCLUDES += [
+ # For CocoaFileUtils
+ "/xpcom/io"
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ XPIDL_SOURCES += [
+ "nsIGNOMEShellService.idl",
+ ]
+
+ SOURCES += [
+ "nsGNOMEShellService.cpp",
+ ]
+ if CONFIG["MOZ_ENABLE_DBUS"]:
+ SOURCES += [
+ "nsGNOMEShellDBusHelper.cpp",
+ "nsGNOMEShellSearchProvider.cpp",
+ ]
+ include("/ipc/chromium/chromium-config.mozbuild")
+
+elif CONFIG["OS_ARCH"] == "WINNT":
+ XPIDL_SOURCES += [
+ "nsIWindowsShellService.idl",
+ ]
+ SOURCES += [
+ "nsWindowsShellService.cpp",
+ "WindowsDefaultBrowser.cpp",
+ "WindowsUserChoice.cpp",
+ ]
+ LOCAL_INCLUDES += [
+ "../../../other-licenses/nsis/Contrib/CityHash/cityhash",
+ "/toolkit/xre",
+ ]
+ OS_LIBS += [
+ "bcrypt",
+ "crypt32",
+ "propsys",
+ ]
+
+XPIDL_MODULE = "shellservice"
+
+if SOURCES:
+ FINAL_LIBRARY = "browsercomps"
+
+EXTRA_JS_MODULES += [
+ "HeadlessShell.sys.mjs",
+ "ScreenshotChild.sys.mjs",
+ "ShellService.sys.mjs",
+]
+
+for var in (
+ "MOZ_APP_DISPLAYNAME",
+ "MOZ_APP_NAME",
+ "MOZ_APP_VERSION",
+ "MOZ_DEFAULT_BROWSER_AGENT",
+):
+ DEFINES[var] = '"%s"' % CONFIG[var]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+
+if CONFIG["MOZ_ENABLE_DBUS"]:
+ CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Shell Integration")
diff --git a/browser/components/shell/nsGNOMEShellDBusHelper.cpp b/browser/components/shell/nsGNOMEShellDBusHelper.cpp
new file mode 100644
index 0000000000..9e1681c902
--- /dev/null
+++ b/browser/components/shell/nsGNOMEShellDBusHelper.cpp
@@ -0,0 +1,535 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=2:
+ */
+/* 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/. */
+
+#include "nsGNOMEShellSearchProvider.h"
+
+#include "RemoteUtils.h"
+#include "nsIStringBundle.h"
+#include "nsServiceManagerUtils.h"
+#include "nsPrintfCString.h"
+#include "mozilla/XREAppData.h"
+#include "nsAppRunner.h"
+
+#define DBUS_BUS_NAME_TEMPLATE "org.mozilla.%s.SearchProvider"
+#define DBUS_OBJECT_PATH_TEMPLATE "/org/mozilla/%s/SearchProvider"
+
+const char* GetDBusBusName() {
+ static const char* name = []() {
+ nsAutoCString appName;
+ gAppData->GetDBusAppName(appName);
+ return ToNewCString(nsPrintfCString(DBUS_BUS_NAME_TEMPLATE,
+ appName.get())); // Intentionally leak
+ }();
+ return name;
+}
+
+const char* GetDBusObjectPath() {
+ static const char* path = []() {
+ nsAutoCString appName;
+ gAppData->GetDBusAppName(appName);
+ return ToNewCString(nsPrintfCString(DBUS_OBJECT_PATH_TEMPLATE,
+ appName.get())); // Intentionally leak
+ }();
+ return path;
+}
+
+static bool GetGnomeSearchTitle(const char* aSearchedTerm,
+ nsAutoCString& aGnomeSearchTitle) {
+ static nsCOMPtr<nsIStringBundle> bundle;
+ if (!bundle) {
+ nsCOMPtr<nsIStringBundleService> sbs =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ if (NS_WARN_IF(!sbs)) {
+ return false;
+ }
+
+ sbs->CreateBundle("chrome://browser/locale/browser.properties",
+ getter_AddRefs(bundle));
+ if (NS_WARN_IF(!bundle)) {
+ return false;
+ }
+ }
+
+ AutoTArray<nsString, 1> formatStrings;
+ CopyUTF8toUTF16(nsCString(aSearchedTerm), *formatStrings.AppendElement());
+
+ nsAutoString gnomeSearchTitle;
+ bundle->FormatStringFromName("gnomeSearchProviderSearchWeb", formatStrings,
+ gnomeSearchTitle);
+ AppendUTF16toUTF8(gnomeSearchTitle, aGnomeSearchTitle);
+ return true;
+}
+
+static const char* introspect_template =
+ "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection "
+ "1.0//EN\"\n"
+ "\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
+ "<node>\n"
+ " <interface name=\"org.freedesktop.DBus.Introspectable\">\n"
+ " <method name=\"Introspect\">\n"
+ " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n"
+ " </method>\n"
+ " </interface>\n"
+ " <interface name=\"org.gnome.Shell.SearchProvider2\">\n"
+ " <method name=\"GetInitialResultSet\">\n"
+ " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n"
+ " <arg type=\"as\" name=\"results\" direction=\"out\" />\n"
+ " </method>\n"
+ " <method name=\"GetSubsearchResultSet\">\n"
+ " <arg type=\"as\" name=\"previous_results\" direction=\"in\" />\n"
+ " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n"
+ " <arg type=\"as\" name=\"results\" direction=\"out\" />\n"
+ " </method>\n"
+ " <method name=\"GetResultMetas\">\n"
+ " <arg type=\"as\" name=\"identifiers\" direction=\"in\" />\n"
+ " <arg type=\"aa{sv}\" name=\"metas\" direction=\"out\" />\n"
+ " </method>\n"
+ " <method name=\"ActivateResult\">\n"
+ " <arg type=\"s\" name=\"identifier\" direction=\"in\" />\n"
+ " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n"
+ " <arg type=\"u\" name=\"timestamp\" direction=\"in\" />\n"
+ " </method>\n"
+ " <method name=\"LaunchSearch\">\n"
+ " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n"
+ " <arg type=\"u\" name=\"timestamp\" direction=\"in\" />\n"
+ " </method>\n"
+ "</interface>\n"
+ "</node>\n";
+
+DBusHandlerResult DBusIntrospect(DBusConnection* aConnection,
+ DBusMessage* aMsg) {
+ DBusMessage* reply;
+
+ reply = dbus_message_new_method_return(aMsg);
+ if (!reply) {
+ return DBUS_HANDLER_RESULT_NEED_MEMORY;
+ }
+
+ dbus_message_append_args(reply, DBUS_TYPE_STRING, &introspect_template,
+ DBUS_TYPE_INVALID);
+
+ dbus_connection_send(aConnection, reply, nullptr);
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+int DBusGetIndexFromIDKey(const char* aIDKey) {
+ // ID is NN:URL where NN is index to our current history
+ // result container.
+ char tmp[] = {aIDKey[0], aIDKey[1], '\0'};
+ return atoi(tmp);
+}
+
+static void ConcatArray(nsACString& aOutputStr, const char** aStringArray) {
+ for (const char** term = aStringArray; *term; term++) {
+ aOutputStr.Append(*term);
+ if (*(term + 1)) {
+ aOutputStr.Append(" ");
+ }
+ }
+}
+
+DBusHandlerResult DBusHandleInitialResultSet(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) {
+ DBusMessage* reply;
+ char** stringArray = nullptr;
+ int elements;
+
+ if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING,
+ &stringArray, &elements, DBUS_TYPE_INVALID) ||
+ elements == 0) {
+ reply = dbus_message_new_error(aMsg, GetDBusBusName(), "Wrong argument");
+ dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr);
+ dbus_message_unref(reply);
+ } else {
+ aSearchResult->SetReply(dbus_message_new_method_return(aMsg));
+ nsAutoCString searchTerm;
+ ConcatArray(searchTerm, const_cast<const char**>(stringArray));
+ aSearchResult->SetSearchTerm(searchTerm.get());
+ GetGNOMEShellHistoryService()->QueryHistory(aSearchResult);
+ // DBus reply will be send asynchronously by
+ // nsGNOMEShellHistorySearchResult::SendDBusSearchResultReply()
+ // when GetGNOMEShellHistoryService() has the results.
+ }
+
+ if (stringArray) {
+ dbus_free_string_array(stringArray);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+DBusHandlerResult DBusHandleSubsearchResultSet(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) {
+ DBusMessage* reply;
+
+ char **unusedArray = nullptr, **stringArray = nullptr;
+ int unusedNum, elements;
+
+ if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING,
+ &unusedArray, &unusedNum, DBUS_TYPE_ARRAY,
+ DBUS_TYPE_STRING, &stringArray, &elements,
+ DBUS_TYPE_INVALID) ||
+ elements == 0) {
+ reply = dbus_message_new_error(aMsg, GetDBusBusName(), "Wrong argument");
+ dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr);
+ dbus_message_unref(reply);
+ } else {
+ aSearchResult->SetReply(dbus_message_new_method_return(aMsg));
+ nsAutoCString searchTerm;
+ ConcatArray(searchTerm, const_cast<const char**>(stringArray));
+ aSearchResult->SetSearchTerm(searchTerm.get());
+ GetGNOMEShellHistoryService()->QueryHistory(aSearchResult);
+ // DBus reply will be send asynchronously by
+ // nsGNOMEShellHistorySearchResult::SendDBusSearchResultReply()
+ // when GetGNOMEShellHistoryService() has the results.
+ }
+
+ if (unusedArray) {
+ dbus_free_string_array(unusedArray);
+ }
+ if (stringArray) {
+ dbus_free_string_array(stringArray);
+ }
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+static void appendStringDictionary(DBusMessageIter* aIter, const char* aKey,
+ const char* aValue) {
+ DBusMessageIter iterDict, iterVar;
+ dbus_message_iter_open_container(aIter, DBUS_TYPE_DICT_ENTRY, nullptr,
+ &iterDict);
+ dbus_message_iter_append_basic(&iterDict, DBUS_TYPE_STRING, &aKey);
+ dbus_message_iter_open_container(&iterDict, DBUS_TYPE_VARIANT, "s", &iterVar);
+ dbus_message_iter_append_basic(&iterVar, DBUS_TYPE_STRING, &aValue);
+ dbus_message_iter_close_container(&iterDict, &iterVar);
+ dbus_message_iter_close_container(aIter, &iterDict);
+}
+
+/*
+ "icon-data": a tuple of type (iiibiiay) describing a pixbuf with width,
+ height, rowstride, has-alpha,
+ bits-per-sample, channels,
+ image data
+*/
+static void DBusAppendIcon(GnomeHistoryIcon* aIcon, DBusMessageIter* aIter) {
+ DBusMessageIter iterDict, iterVar, iterStruct;
+ dbus_message_iter_open_container(aIter, DBUS_TYPE_DICT_ENTRY, nullptr,
+ &iterDict);
+ const char* key = "icon-data";
+ dbus_message_iter_append_basic(&iterDict, DBUS_TYPE_STRING, &key);
+ dbus_message_iter_open_container(&iterDict, DBUS_TYPE_VARIANT, "(iiibiiay)",
+ &iterVar);
+ dbus_message_iter_open_container(&iterVar, DBUS_TYPE_STRUCT, nullptr,
+ &iterStruct);
+
+ int width = aIcon->GetWidth();
+ int height = aIcon->GetHeight();
+ dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &width);
+ dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &height);
+ int rowstride = width * 4;
+ dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &rowstride);
+ int hasAlpha = true;
+ dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_BOOLEAN, &hasAlpha);
+ int bitsPerSample = 8;
+ dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &bitsPerSample);
+ int channels = 4;
+ dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &channels);
+
+ DBusMessageIter iterArray;
+ dbus_message_iter_open_container(&iterStruct, DBUS_TYPE_ARRAY, "y",
+ &iterArray);
+ unsigned char* array = aIcon->GetData();
+ dbus_message_iter_append_fixed_array(&iterArray, DBUS_TYPE_BYTE, &array,
+ width * height * 4);
+ dbus_message_iter_close_container(&iterStruct, &iterArray);
+
+ dbus_message_iter_close_container(&iterVar, &iterStruct);
+ dbus_message_iter_close_container(&iterDict, &iterVar);
+ dbus_message_iter_close_container(aIter, &iterDict);
+}
+
+/* Appends history search results to the DBUS reply.
+
+ We can return those fields at GetResultMetas:
+
+ "id": the result ID
+ "name": the display name for the result
+ "icon": a serialized GIcon (see g_icon_serialize()), or alternatively,
+ "gicon": a textual representation of a GIcon (see g_icon_to_string()),
+ or alternativly,
+ "icon-data": a tuple of type (iiibiiay) describing a pixbuf with width,
+ height, rowstride, has-alpha, bits-per-sample, and image data
+ "description": an optional short description (1-2 lines)
+*/
+static void DBusAppendResultID(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult,
+ DBusMessageIter* aIter, const char* aID) {
+ nsCOMPtr<nsINavHistoryContainerResultNode> container =
+ aSearchResult->GetSearchResultContainer();
+
+ int index = DBusGetIndexFromIDKey(aID);
+ nsCOMPtr<nsINavHistoryResultNode> child;
+ container->GetChild(index, getter_AddRefs(child));
+ nsAutoCString title;
+ if (NS_FAILED(child->GetTitle(title))) {
+ return;
+ }
+
+ if (title.IsEmpty()) {
+ if (NS_FAILED(child->GetUri(title)) || title.IsEmpty()) {
+ return;
+ }
+ }
+
+ const char* titleStr = title.get();
+ appendStringDictionary(aIter, "id", aID);
+ appendStringDictionary(aIter, "name", titleStr);
+
+ GnomeHistoryIcon* icon = aSearchResult->GetHistoryIcon(index);
+ if (icon) {
+ DBusAppendIcon(icon, aIter);
+ } else {
+ appendStringDictionary(aIter, "gicon", "text-html");
+ }
+}
+
+/* Search the web for: "searchTerm" to the DBUS reply.
+ */
+static void DBusAppendSearchID(DBusMessageIter* aIter, const char* aID) {
+ /* aID contains:
+
+ KEYWORD_SEARCH_STRING:ssssss
+
+ KEYWORD_SEARCH_STRING is a 'special:search' keyword
+ ssssss is a searched term, must be at least one character long
+ */
+
+ // aID contains only 'KEYWORD_SEARCH_STRING:' so we're missing searched
+ // string.
+ if (strlen(aID) <= KEYWORD_SEARCH_STRING_LEN + 1) {
+ return;
+ }
+
+ appendStringDictionary(aIter, "id", KEYWORD_SEARCH_STRING);
+
+ // Extract ssssss part from aID
+ auto searchTerm = nsAutoCStringN<32>(aID + KEYWORD_SEARCH_STRING_LEN + 1);
+ nsAutoCString gnomeSearchTitle;
+ if (GetGnomeSearchTitle(searchTerm.get(), gnomeSearchTitle)) {
+ appendStringDictionary(aIter, "name", gnomeSearchTitle.get());
+ // TODO: When running on flatpak/snap we may need to use
+ // icon like org.mozilla.Firefox or so.
+ appendStringDictionary(aIter, "gicon", "firefox");
+ }
+}
+
+DBusHandlerResult DBusHandleResultMetas(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) {
+ DBusMessage* reply;
+ char** stringArray;
+ int elements;
+
+ if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING,
+ &stringArray, &elements, DBUS_TYPE_INVALID) ||
+ elements == 0) {
+ reply = dbus_message_new_error(aMsg, GetDBusBusName(), "Wrong argument");
+ } else {
+ reply = dbus_message_new_method_return(aMsg);
+
+ DBusMessageIter iter;
+ dbus_message_iter_init_append(reply, &iter);
+ DBusMessageIter iterArray;
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "a{sv}",
+ &iterArray);
+
+ DBusMessageIter iterArray2;
+ for (int i = 0; i < elements; i++) {
+ dbus_message_iter_open_container(&iterArray, DBUS_TYPE_ARRAY, "{sv}",
+ &iterArray2);
+ if (strncmp(stringArray[i], KEYWORD_SEARCH_STRING,
+ KEYWORD_SEARCH_STRING_LEN) == 0) {
+ DBusAppendSearchID(&iterArray2, stringArray[i]);
+ } else {
+ DBusAppendResultID(aSearchResult, &iterArray2, stringArray[i]);
+ }
+ dbus_message_iter_close_container(&iterArray, &iterArray2);
+ }
+
+ dbus_message_iter_close_container(&iter, &iterArray);
+ dbus_free_string_array(stringArray);
+ }
+
+ dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr);
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+} // namespace mozilla
+
+static void ActivateResultID(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult,
+ const char* aResultID, uint32_t aTimeStamp) {
+ char* commandLine = nullptr;
+ int tmp;
+
+ if (strncmp(aResultID, KEYWORD_SEARCH_STRING, KEYWORD_SEARCH_STRING_LEN) ==
+ 0) {
+ const char* urlList[3] = {"unused", "--search",
+ aSearchResult->GetSearchTerm().get()};
+ commandLine = ConstructCommandLine(std::size(urlList), (char**)urlList,
+ nullptr, &tmp);
+ } else {
+ int keyIndex = atoi(aResultID);
+ nsCOMPtr<nsINavHistoryResultNode> child;
+ aSearchResult->GetSearchResultContainer()->GetChild(keyIndex,
+ getter_AddRefs(child));
+ if (!child) {
+ return;
+ }
+
+ nsAutoCString uri;
+ nsresult rv = child->GetUri(uri);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ const char* urlList[2] = {"unused", uri.get()};
+ commandLine = ConstructCommandLine(std::size(urlList), (char**)urlList,
+ nullptr, &tmp);
+ }
+
+ if (commandLine) {
+ aSearchResult->HandleCommandLine(commandLine, aTimeStamp);
+ free(commandLine);
+ }
+}
+
+static void DBusLaunchWithAllResults(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult,
+ uint32_t aTimeStamp) {
+ uint32_t childCount = 0;
+ nsresult rv =
+ aSearchResult->GetSearchResultContainer()->GetChildCount(&childCount);
+ if (NS_FAILED(rv) || childCount == 0) {
+ return;
+ }
+
+ if (childCount > MAX_SEARCH_RESULTS_NUM) {
+ childCount = MAX_SEARCH_RESULTS_NUM;
+ }
+
+ // Allocate space for all found results, "unused", "--search" and
+ // potential search request.
+ char** urlList = (char**)moz_xmalloc(sizeof(char*) * (childCount + 3));
+ int urlListElements = 0;
+
+ urlList[urlListElements++] = strdup("unused");
+
+ for (uint32_t i = 0; i < childCount; i++) {
+ nsCOMPtr<nsINavHistoryResultNode> child;
+ aSearchResult->GetSearchResultContainer()->GetChild(i,
+ getter_AddRefs(child));
+
+ if (!IsHistoryResultNodeURI(child)) {
+ continue;
+ }
+
+ nsAutoCString uri;
+ nsresult rv = child->GetUri(uri);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ urlList[urlListElements++] = strdup(uri.get());
+ }
+
+ // When there isn't any uri to open pass search at least.
+ if (!childCount) {
+ urlList[urlListElements++] = strdup("--search");
+ urlList[urlListElements++] = strdup(aSearchResult->GetSearchTerm().get());
+ }
+
+ int tmp;
+ char* commandLine =
+ ConstructCommandLine(urlListElements, urlList, nullptr, &tmp);
+ if (commandLine) {
+ aSearchResult->HandleCommandLine(commandLine, aTimeStamp);
+ free(commandLine);
+ }
+
+ for (int i = 0; i < urlListElements; i++) {
+ free(urlList[i]);
+ }
+ free(urlList);
+}
+
+DBusHandlerResult DBusActivateResult(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) {
+ DBusMessage* reply;
+ char* resultID;
+ char** stringArray;
+ int elements;
+ uint32_t timestamp;
+
+ if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_STRING, &resultID,
+ DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &stringArray,
+ &elements, DBUS_TYPE_UINT32, &timestamp,
+ DBUS_TYPE_INVALID) ||
+ resultID == nullptr) {
+ reply = dbus_message_new_error(aMsg, GetDBusBusName(), "Wrong argument");
+ } else {
+ reply = dbus_message_new_method_return(aMsg);
+ ActivateResultID(aSearchResult, resultID, timestamp);
+ dbus_free_string_array(stringArray);
+ }
+
+ dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr);
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+DBusHandlerResult DBusLaunchSearch(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) {
+ DBusMessage* reply;
+ char** stringArray;
+ int elements;
+ uint32_t timestamp;
+
+ if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING,
+ &stringArray, &elements, DBUS_TYPE_UINT32,
+ &timestamp, DBUS_TYPE_INVALID) ||
+ elements == 0) {
+ reply = dbus_message_new_error(aMsg, GetDBusBusName(), "Wrong argument");
+ } else {
+ reply = dbus_message_new_method_return(aMsg);
+ DBusLaunchWithAllResults(aSearchResult, timestamp);
+ dbus_free_string_array(stringArray);
+ }
+
+ dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr);
+ dbus_message_unref(reply);
+
+ return DBUS_HANDLER_RESULT_HANDLED;
+}
+
+bool IsHistoryResultNodeURI(nsINavHistoryResultNode* aHistoryNode) {
+ uint32_t type;
+ nsresult rv = aHistoryNode->GetType(&type);
+ if (NS_FAILED(rv) || type != nsINavHistoryResultNode::RESULT_TYPE_URI)
+ return false;
+
+ nsAutoCString title;
+ rv = aHistoryNode->GetTitle(title);
+ if (NS_SUCCEEDED(rv) && !title.IsEmpty()) {
+ return true;
+ }
+
+ rv = aHistoryNode->GetUri(title);
+ return NS_SUCCEEDED(rv) && !title.IsEmpty();
+}
diff --git a/browser/components/shell/nsGNOMEShellDBusHelper.h b/browser/components/shell/nsGNOMEShellDBusHelper.h
new file mode 100644
index 0000000000..924abf8356
--- /dev/null
+++ b/browser/components/shell/nsGNOMEShellDBusHelper.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=2:
+ */
+/* 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/. */
+
+#ifndef __nsGNOMEShellDBusHelper_h__
+#define __nsGNOMEShellDBusHelper_h__
+
+#include "mozilla/DBusHelpers.h"
+#include "nsINavHistoryService.h"
+
+#define MAX_SEARCH_RESULTS_NUM 9
+#define KEYWORD_SEARCH_STRING "special:search"
+#define KEYWORD_SEARCH_STRING_LEN 14
+
+class nsGNOMEShellHistorySearchResult;
+
+DBusHandlerResult DBusIntrospect(DBusConnection* aConnection,
+ DBusMessage* aMsg);
+DBusHandlerResult DBusHandleInitialResultSet(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg);
+DBusHandlerResult DBusHandleSubsearchResultSet(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg);
+DBusHandlerResult DBusHandleResultMetas(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg);
+DBusHandlerResult DBusActivateResult(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg);
+DBusHandlerResult DBusLaunchSearch(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg);
+bool IsHistoryResultNodeURI(nsINavHistoryResultNode* aHistoryNode);
+
+const char* GetDBusBusName();
+const char* GetDBusObjectPath();
+
+#endif // __nsGNOMEShellDBusHelper_h__
diff --git a/browser/components/shell/nsGNOMEShellSearchProvider.cpp b/browser/components/shell/nsGNOMEShellSearchProvider.cpp
new file mode 100644
index 0000000000..b5658da034
--- /dev/null
+++ b/browser/components/shell/nsGNOMEShellSearchProvider.cpp
@@ -0,0 +1,442 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=2:
+ */
+/* 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/. */
+
+#include "nsGNOMEShellSearchProvider.h"
+
+#include "nsToolkitCompsCID.h"
+#include "nsIFaviconService.h"
+#include "base/message_loop.h" // for MessageLoop
+#include "base/task.h" // for NewRunnableMethod, etc
+#include "mozilla/gfx/2D.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIIOService.h"
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+
+#include <dbus/dbus.h>
+#include <dbus/dbus-glib-lowlevel.h>
+
+#include "imgIContainer.h"
+#include "imgITools.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+class AsyncFaviconDataReady final : public nsIFaviconDataCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFAVICONDATACALLBACK
+
+ AsyncFaviconDataReady(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult,
+ int aIconIndex, int aTimeStamp)
+ : mSearchResult(aSearchResult),
+ mIconIndex(aIconIndex),
+ mTimeStamp(aTimeStamp){};
+
+ private:
+ ~AsyncFaviconDataReady() {}
+
+ RefPtr<nsGNOMEShellHistorySearchResult> mSearchResult;
+ int mIconIndex;
+ int mTimeStamp;
+};
+
+NS_IMPL_ISUPPORTS(AsyncFaviconDataReady, nsIFaviconDataCallback)
+
+// Inspired by SurfaceToPackedBGRA
+static UniquePtr<uint8_t[]> SurfaceToPackedRGBA(DataSourceSurface* aSurface) {
+ IntSize size = aSurface->GetSize();
+ CheckedInt<size_t> bufferSize =
+ CheckedInt<size_t>(size.width * 4) * CheckedInt<size_t>(size.height);
+ if (!bufferSize.isValid()) {
+ return nullptr;
+ }
+ UniquePtr<uint8_t[]> imageBuffer(new (std::nothrow)
+ uint8_t[bufferSize.value()]);
+ if (!imageBuffer) {
+ return nullptr;
+ }
+
+ DataSourceSurface::MappedSurface map;
+ if (!aSurface->Map(DataSourceSurface::MapType::READ, &map)) {
+ return nullptr;
+ }
+
+ // Convert BGRA to RGBA
+ uint32_t* aSrc = (uint32_t*)map.mData;
+ uint32_t* aDst = (uint32_t*)imageBuffer.get();
+ for (int i = 0; i < size.width * size.height; i++, aDst++, aSrc++) {
+ *aDst = *aSrc & 0xff00ff00;
+ *aDst |= (*aSrc & 0xff) << 16;
+ *aDst |= (*aSrc & 0xff0000) >> 16;
+ }
+
+ aSurface->Unmap();
+
+ return imageBuffer;
+}
+
+NS_IMETHODIMP
+AsyncFaviconDataReady::OnComplete(nsIURI* aFaviconURI, uint32_t aDataLen,
+ const uint8_t* aData,
+ const nsACString& aMimeType,
+ uint16_t aWidth) {
+ // This is a callback from some previous search so we don't want it
+ if (mTimeStamp != mSearchResult->GetTimeStamp() || !aData || !aDataLen) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Decode the image from the format it was returned to us in (probably PNG)
+ nsCOMPtr<imgIContainer> container;
+ nsCOMPtr<imgITools> imgtool = do_CreateInstance("@mozilla.org/image/tools;1");
+ nsresult rv = imgtool->DecodeImageFromBuffer(
+ reinterpret_cast<const char*>(aData), aDataLen, aMimeType,
+ getter_AddRefs(container));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<SourceSurface> surface = container->GetFrame(
+ imgIContainer::FRAME_FIRST,
+ imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
+
+ if (!surface || surface->GetFormat() != SurfaceFormat::B8G8R8A8) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Allocate a new buffer that we own.
+ RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
+ UniquePtr<uint8_t[]> data = SurfaceToPackedRGBA(dataSurface);
+ if (!data) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ mSearchResult->SetHistoryIcon(mTimeStamp, std::move(data),
+ surface->GetSize().width,
+ surface->GetSize().height, mIconIndex);
+ return NS_OK;
+}
+
+DBusHandlerResult nsGNOMEShellSearchProvider::HandleSearchResultSet(
+ DBusMessage* aMsg, bool aInitialSearch) {
+ // Discard any existing search results.
+ mSearchResult = nullptr;
+
+ RefPtr<nsGNOMEShellHistorySearchResult> newSearch =
+ new nsGNOMEShellHistorySearchResult(this, mConnection,
+ mSearchResultTimeStamp);
+ mSearchResultTimeStamp++;
+ newSearch->SetTimeStamp(mSearchResultTimeStamp);
+
+ // Send the search request over DBus. We'll get reply over DBus it will be
+ // set to mSearchResult by nsGNOMEShellSearchProvider::SetSearchResult().
+ return aInitialSearch
+ ? DBusHandleInitialResultSet(newSearch.forget(), aMsg)
+ : DBusHandleSubsearchResultSet(newSearch.forget(), aMsg);
+}
+
+DBusHandlerResult nsGNOMEShellSearchProvider::HandleResultMetas(
+ DBusMessage* aMsg) {
+ if (!mSearchResult) {
+ NS_WARNING("Missing nsGNOMEShellHistorySearchResult.");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ return DBusHandleResultMetas(mSearchResult, aMsg);
+}
+
+DBusHandlerResult nsGNOMEShellSearchProvider::ActivateResult(
+ DBusMessage* aMsg) {
+ if (!mSearchResult) {
+ NS_WARNING("Missing nsGNOMEShellHistorySearchResult.");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ return DBusActivateResult(mSearchResult, aMsg);
+}
+
+DBusHandlerResult nsGNOMEShellSearchProvider::LaunchSearch(DBusMessage* aMsg) {
+ if (!mSearchResult) {
+ NS_WARNING("Missing nsGNOMEShellHistorySearchResult.");
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+ }
+ return DBusLaunchSearch(mSearchResult, aMsg);
+}
+
+DBusHandlerResult nsGNOMEShellSearchProvider::HandleDBusMessage(
+ DBusConnection* aConnection, DBusMessage* aMsg) {
+ NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection.");
+
+ const char* method = dbus_message_get_member(aMsg);
+ const char* iface = dbus_message_get_interface(aMsg);
+
+ if ((strcmp("Introspect", method) == 0) &&
+ (strcmp("org.freedesktop.DBus.Introspectable", iface) == 0)) {
+ return DBusIntrospect(mConnection, aMsg);
+ }
+
+ if (strcmp("org.gnome.Shell.SearchProvider2", iface) == 0) {
+ if (strcmp("GetInitialResultSet", method) == 0) {
+ return HandleSearchResultSet(aMsg, /* aInitialSearch */ true);
+ }
+ if (strcmp("GetSubsearchResultSet", method) == 0) {
+ return HandleSearchResultSet(aMsg, /* aInitialSearch */ false);
+ }
+ if (strcmp("GetResultMetas", method) == 0) {
+ return HandleResultMetas(aMsg);
+ }
+ if (strcmp("ActivateResult", method) == 0) {
+ return ActivateResult(aMsg);
+ }
+ if (strcmp("LaunchSearch", method) == 0) {
+ return LaunchSearch(aMsg);
+ }
+ }
+
+ return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+void nsGNOMEShellSearchProvider::UnregisterDBusInterface(
+ DBusConnection* aConnection) {
+ NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection.");
+ // Not implemented
+}
+
+static DBusHandlerResult message_handler(DBusConnection* conn,
+ DBusMessage* aMsg, void* user_data) {
+ auto interface = static_cast<nsGNOMEShellSearchProvider*>(user_data);
+ return interface->HandleDBusMessage(conn, aMsg);
+}
+
+static void unregister(DBusConnection* conn, void* user_data) {
+ auto interface = static_cast<nsGNOMEShellSearchProvider*>(user_data);
+ interface->UnregisterDBusInterface(conn);
+}
+
+static DBusObjectPathVTable remoteHandlersTable = {
+ .unregister_function = unregister,
+ .message_function = message_handler,
+};
+
+nsresult nsGNOMEShellSearchProvider::Startup() {
+ if (mConnection && dbus_connection_get_is_connected(mConnection)) {
+ // We're already connected so we don't need to reconnect
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+
+ mConnection =
+ already_AddRefed<DBusConnection>(dbus_bus_get(DBUS_BUS_SESSION, nullptr));
+ if (!mConnection) {
+ return NS_ERROR_FAILURE;
+ }
+ dbus_connection_set_exit_on_disconnect(mConnection, false);
+ dbus_connection_setup_with_g_main(mConnection, nullptr);
+
+ DBusError err;
+ dbus_error_init(&err);
+ dbus_bus_request_name(mConnection, GetDBusBusName(),
+ DBUS_NAME_FLAG_DO_NOT_QUEUE, &err);
+ // The interface is already owned - there is another application/profile
+ // instance already running.
+ if (dbus_error_is_set(&err)) {
+ dbus_error_free(&err);
+ mConnection = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!dbus_connection_register_object_path(mConnection, GetDBusObjectPath(),
+ &remoteHandlersTable, this)) {
+ mConnection = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ mSearchResultTimeStamp = 0;
+ return NS_OK;
+}
+
+void nsGNOMEShellSearchProvider::Shutdown() {
+ if (!mConnection) {
+ return;
+ }
+
+ dbus_connection_unregister_object_path(mConnection, GetDBusObjectPath());
+
+ // dbus_connection_unref() will be called by RefPtr here.
+ mConnection = nullptr;
+}
+
+bool nsGNOMEShellSearchProvider::SetSearchResult(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult) {
+ MOZ_ASSERT(!mSearchResult);
+
+ if (mSearchResultTimeStamp != aSearchResult->GetTimeStamp()) {
+ NS_WARNING("Time stamp mismatch.");
+ return false;
+ }
+ mSearchResult = aSearchResult;
+ return true;
+}
+
+static void DispatchSearchResults(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult,
+ nsCOMPtr<nsINavHistoryContainerResultNode> aHistResultContainer) {
+ aSearchResult->ReceiveSearchResultContainer(aHistResultContainer);
+}
+
+nsresult nsGNOMEShellHistoryService::QueryHistory(
+ RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult) {
+ if (!mHistoryService) {
+ mHistoryService = do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID);
+ if (!mHistoryService) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsINavHistoryQuery> histQuery;
+ rv = mHistoryService->GetNewQuery(getter_AddRefs(histQuery));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = histQuery->SetSearchTerms(
+ NS_ConvertUTF8toUTF16(aSearchResult->GetSearchTerm()));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINavHistoryQueryOptions> histQueryOpts;
+ rv = mHistoryService->GetNewQueryOptions(getter_AddRefs(histQueryOpts));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = histQueryOpts->SetSortingMode(
+ nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = histQueryOpts->SetMaxResults(MAX_SEARCH_RESULTS_NUM);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINavHistoryResult> histResult;
+ rv = mHistoryService->ExecuteQuery(histQuery, histQueryOpts,
+ getter_AddRefs(histResult));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINavHistoryContainerResultNode> resultContainer;
+
+ rv = histResult->GetRoot(getter_AddRefs(resultContainer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = resultContainer->SetContainerOpen(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Simulate async searching by delayed reply. This search API will
+ // likely become async in the future and we want to be sure to not rely on
+ // its current synchronous behavior.
+ MOZ_ASSERT(MessageLoop::current());
+ MessageLoop::current()->PostTask(
+ NewRunnableFunction("Gnome shell search results", &DispatchSearchResults,
+ aSearchResult, resultContainer));
+
+ return NS_OK;
+}
+
+static void DBusGetIDKeyForURI(int aIndex, nsAutoCString& aUri,
+ nsAutoCString& aIDKey) {
+ // Compose ID as NN:URL where NN is index to our current history
+ // result container.
+ aIDKey = nsPrintfCString("%.2d:%s", aIndex, aUri.get());
+}
+
+void nsGNOMEShellHistorySearchResult::HandleSearchResultReply() {
+ MOZ_ASSERT(mReply);
+
+ uint32_t childCount = 0;
+ nsresult rv = mHistResultContainer->GetChildCount(&childCount);
+
+ DBusMessageIter iter;
+ dbus_message_iter_init_append(mReply, &iter);
+ DBusMessageIter iterArray;
+ dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "s", &iterArray);
+
+ if (NS_SUCCEEDED(rv) && childCount > 0) {
+ // Obtain the favicon service and get the favicon for the specified page
+ nsCOMPtr<nsIFaviconService> favIconSvc(
+ do_GetService("@mozilla.org/browser/favicon-service;1"));
+ nsCOMPtr<nsIIOService> ios(do_GetService(NS_IOSERVICE_CONTRACTID));
+
+ if (childCount > MAX_SEARCH_RESULTS_NUM) {
+ childCount = MAX_SEARCH_RESULTS_NUM;
+ }
+
+ for (uint32_t i = 0; i < childCount; i++) {
+ nsCOMPtr<nsINavHistoryResultNode> child;
+ mHistResultContainer->GetChild(i, getter_AddRefs(child));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+ if (!IsHistoryResultNodeURI(child)) {
+ continue;
+ }
+
+ nsAutoCString uri;
+ child->GetUri(uri);
+
+ nsCOMPtr<nsIURI> iconIri;
+ ios->NewURI(uri, nullptr, nullptr, getter_AddRefs(iconIri));
+ nsCOMPtr<nsIFaviconDataCallback> callback =
+ new AsyncFaviconDataReady(this, i, mTimeStamp);
+ favIconSvc->GetFaviconDataForPage(iconIri, callback, 0);
+
+ nsAutoCString idKey;
+ DBusGetIDKeyForURI(i, uri, idKey);
+
+ const char* id = idKey.get();
+ dbus_message_iter_append_basic(&iterArray, DBUS_TYPE_STRING, &id);
+ }
+ }
+
+ nsPrintfCString searchString("%s:%s", KEYWORD_SEARCH_STRING,
+ mSearchTerm.get());
+ const char* search = searchString.get();
+ dbus_message_iter_append_basic(&iterArray, DBUS_TYPE_STRING, &search);
+
+ dbus_message_iter_close_container(&iter, &iterArray);
+
+ dbus_connection_send(mConnection, mReply, nullptr);
+ dbus_message_unref(mReply);
+
+ mReply = nullptr;
+}
+
+void nsGNOMEShellHistorySearchResult::ReceiveSearchResultContainer(
+ nsCOMPtr<nsINavHistoryContainerResultNode> aHistResultContainer) {
+ // Propagate search results to nsGNOMEShellSearchProvider.
+ // SetSearchResult() checks this is up-to-date search (our time stamp matches
+ // latest requested search timestamp).
+ if (mSearchProvider->SetSearchResult(this)) {
+ mHistResultContainer = aHistResultContainer;
+ HandleSearchResultReply();
+ }
+}
+
+void nsGNOMEShellHistorySearchResult::SetHistoryIcon(int aTimeStamp,
+ UniquePtr<uint8_t[]> aData,
+ int aWidth, int aHeight,
+ int aIconIndex) {
+ MOZ_ASSERT(mTimeStamp == aTimeStamp);
+ MOZ_RELEASE_ASSERT(aIconIndex < MAX_SEARCH_RESULTS_NUM);
+ mHistoryIcons[aIconIndex].Set(mTimeStamp, std::move(aData), aWidth, aHeight);
+}
+
+GnomeHistoryIcon* nsGNOMEShellHistorySearchResult::GetHistoryIcon(
+ int aIconIndex) {
+ MOZ_RELEASE_ASSERT(aIconIndex < MAX_SEARCH_RESULTS_NUM);
+ if (mHistoryIcons[aIconIndex].GetTimeStamp() == mTimeStamp &&
+ mHistoryIcons[aIconIndex].IsLoaded()) {
+ return mHistoryIcons + aIconIndex;
+ }
+ return nullptr;
+}
+
+nsGNOMEShellHistoryService* GetGNOMEShellHistoryService() {
+ static nsGNOMEShellHistoryService gGNOMEShellHistoryService;
+ return &gGNOMEShellHistoryService;
+}
diff --git a/browser/components/shell/nsGNOMEShellSearchProvider.h b/browser/components/shell/nsGNOMEShellSearchProvider.h
new file mode 100644
index 0000000000..f116fe5797
--- /dev/null
+++ b/browser/components/shell/nsGNOMEShellSearchProvider.h
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:expandtab:shiftwidth=2:tabstop=2:
+ */
+/* 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/. */
+
+#ifndef __nsGNOMEShellSearchProvider_h__
+#define __nsGNOMEShellSearchProvider_h__
+
+#include "nsINavHistoryService.h"
+#include "nsUnixRemoteServer.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsGNOMEShellDBusHelper.h"
+
+class nsGNOMEShellSearchProvider;
+
+class GnomeHistoryIcon {
+ public:
+ GnomeHistoryIcon() : mTimeStamp(-1), mWidth(0), mHeight(0){};
+
+ // From which search is this icon
+ void Set(int aTimeStamp, mozilla::UniquePtr<uint8_t[]> aData, int aWidth,
+ int aHeight) {
+ mTimeStamp = aTimeStamp;
+ mWidth = aWidth;
+ mHeight = aHeight;
+ mData = std::move(aData);
+ }
+
+ bool IsLoaded() { return mData && mWidth > 0 && mHeight > 0; }
+ int GetTimeStamp() { return mTimeStamp; }
+ uint8_t* GetData() { return mData.get(); }
+ int GetWidth() { return mWidth; }
+ int GetHeight() { return mHeight; }
+
+ private:
+ int mTimeStamp;
+ mozilla::UniquePtr<uint8_t[]> mData;
+ int mWidth;
+ int mHeight;
+};
+
+// nsGNOMEShellHistorySearchResult is a container with contains search results
+// which are files asynchronously by nsGNOMEShellHistoryService.
+// The search results can be opened by Firefox then.
+class nsGNOMEShellHistorySearchResult : public nsUnixRemoteServer {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsGNOMEShellHistorySearchResult)
+
+ nsGNOMEShellHistorySearchResult(nsGNOMEShellSearchProvider* aSearchProvider,
+ DBusConnection* aConnection, int aTimeStamp)
+ : mSearchProvider(aSearchProvider),
+ mReply(nullptr),
+ mConnection(aConnection),
+ mTimeStamp(aTimeStamp){};
+
+ void SetReply(DBusMessage* aReply) { mReply = aReply; }
+ void SetSearchTerm(const char* aSearchTerm) {
+ mSearchTerm = nsAutoCString(aSearchTerm);
+ }
+ DBusConnection* GetDBusConnection() { return mConnection; }
+ void SetTimeStamp(int aTimeStamp) { mTimeStamp = aTimeStamp; }
+ int GetTimeStamp() { return mTimeStamp; }
+ nsAutoCString& GetSearchTerm() { return mSearchTerm; }
+
+ // Receive (asynchronously) history search results from history service.
+ // This is called asynchronously by nsGNOMEShellHistoryService
+ // when we have search results available.
+ void ReceiveSearchResultContainer(
+ nsCOMPtr<nsINavHistoryContainerResultNode> aHistResultContainer);
+
+ nsCOMPtr<nsINavHistoryContainerResultNode> GetSearchResultContainer() {
+ return mHistResultContainer;
+ }
+ void HandleCommandLine(const char* aBuffer, uint32_t aTimestamp) {
+ nsUnixRemoteServer::HandleCommandLine(aBuffer, aTimestamp);
+ }
+
+ void SetHistoryIcon(int aTimeStamp, mozilla::UniquePtr<uint8_t[]> aData,
+ int aWidth, int aHeight, int aIconIndex);
+ GnomeHistoryIcon* GetHistoryIcon(int aIconIndex);
+
+ private:
+ void HandleSearchResultReply();
+
+ ~nsGNOMEShellHistorySearchResult() = default;
+
+ private:
+ nsGNOMEShellSearchProvider* mSearchProvider;
+ nsCOMPtr<nsINavHistoryContainerResultNode> mHistResultContainer;
+ nsAutoCString mSearchTerm;
+ DBusMessage* mReply;
+ DBusConnection* mConnection;
+ int mTimeStamp;
+ GnomeHistoryIcon mHistoryIcons[MAX_SEARCH_RESULTS_NUM];
+};
+
+class nsGNOMEShellHistoryService {
+ public:
+ nsresult QueryHistory(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult);
+
+ private:
+ nsCOMPtr<nsINavHistoryService> mHistoryService;
+};
+
+class nsGNOMEShellSearchProvider {
+ public:
+ nsGNOMEShellSearchProvider()
+ : mConnection(nullptr), mSearchResultTimeStamp(0) {}
+ ~nsGNOMEShellSearchProvider() { Shutdown(); }
+
+ nsresult Startup();
+ void Shutdown();
+
+ DBusHandlerResult HandleDBusMessage(DBusConnection* aConnection,
+ DBusMessage* msg);
+ void UnregisterDBusInterface(DBusConnection* aConnection);
+
+ bool SetSearchResult(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult);
+
+ private:
+ DBusHandlerResult HandleSearchResultSet(DBusMessage* msg,
+ bool aInitialSearch);
+ DBusHandlerResult HandleResultMetas(DBusMessage* msg);
+ DBusHandlerResult ActivateResult(DBusMessage* msg);
+ DBusHandlerResult LaunchSearch(DBusMessage* msg);
+
+ // The connection is owned by DBus library
+ RefPtr<DBusConnection> mConnection;
+ RefPtr<nsGNOMEShellHistorySearchResult> mSearchResult;
+ int mSearchResultTimeStamp;
+};
+
+nsGNOMEShellHistoryService* GetGNOMEShellHistoryService();
+
+#endif // __nsGNOMEShellSearchProvider_h__
diff --git a/browser/components/shell/nsGNOMEShellService.cpp b/browser/components/shell/nsGNOMEShellService.cpp
new file mode 100644
index 0000000000..a07d37d109
--- /dev/null
+++ b/browser/components/shell/nsGNOMEShellService.cpp
@@ -0,0 +1,506 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Preferences.h"
+
+#include "nsCOMPtr.h"
+#include "nsGNOMEShellService.h"
+#include "nsShellService.h"
+#include "nsIFile.h"
+#include "nsIProperties.h"
+#include "nsDirectoryServiceDefs.h"
+#include "prenv.h"
+#include "nsString.h"
+#include "nsIGIOService.h"
+#include "nsIGSettingsService.h"
+#include "nsIStringBundle.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIImageLoadingContent.h"
+#include "imgIRequest.h"
+#include "imgIContainer.h"
+#include "mozilla/Components.h"
+#include "mozilla/GRefPtr.h"
+#include "mozilla/GUniquePtr.h"
+#include "mozilla/WidgetUtilsGtk.h"
+#include "mozilla/dom/Element.h"
+#include "nsImageToPixbuf.h"
+#include "nsXULAppAPI.h"
+#include "gfxPlatform.h"
+
+#include <glib.h>
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <stdlib.h>
+
+using namespace mozilla;
+
+struct ProtocolAssociation {
+ const char* name;
+ bool essential;
+};
+
+struct MimeTypeAssociation {
+ const char* mimeType;
+ const char* extensions;
+};
+
+static const ProtocolAssociation appProtocols[] = {
+ // clang-format off
+ { "http", true },
+ { "https", true },
+ { "chrome", false }
+ // clang-format on
+};
+
+static const MimeTypeAssociation appTypes[] = {
+ // clang-format off
+ { "text/html", "htm html shtml" },
+ { "application/xhtml+xml", "xhtml xht" }
+ // clang-format on
+};
+
+#define kDesktopBGSchema "org.gnome.desktop.background"_ns
+#define kDesktopColorGSKey "primary-color"_ns
+
+nsresult nsGNOMEShellService::Init() {
+ nsresult rv;
+
+ if (gfxPlatform::IsHeadless()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // GSettings or GIO _must_ be available, or we do not allow
+ // CreateInstance to succeed.
+
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+
+ if (!giovfs && !gsettings) return NS_ERROR_NOT_AVAILABLE;
+
+#ifdef MOZ_ENABLE_DBUS
+ if (widget::IsGnomeDesktopEnvironment() &&
+ Preferences::GetBool("browser.gnome-search-provider.enabled", false)) {
+ mSearchProvider.Startup();
+ }
+#endif
+
+ // Check G_BROKEN_FILENAMES. If it's set, then filenames in glib use
+ // the locale encoding. If it's not set, they use UTF-8.
+ mUseLocaleFilenames = PR_GetEnv("G_BROKEN_FILENAMES") != nullptr;
+
+ if (GetAppPathFromLauncher()) return NS_OK;
+
+ nsCOMPtr<nsIProperties> dirSvc(
+ do_GetService("@mozilla.org/file/directory_service;1"));
+ NS_ENSURE_TRUE(dirSvc, NS_ERROR_NOT_AVAILABLE);
+
+ nsCOMPtr<nsIFile> appPath;
+ rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
+ getter_AddRefs(appPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return appPath->GetNativePath(mAppPath);
+}
+
+NS_IMPL_ISUPPORTS(nsGNOMEShellService, nsIGNOMEShellService, nsIShellService,
+ nsIToolkitShellService)
+
+bool nsGNOMEShellService::GetAppPathFromLauncher() {
+ gchar* tmp;
+
+ const char* launcher = PR_GetEnv("MOZ_APP_LAUNCHER");
+ if (!launcher) return false;
+
+ if (g_path_is_absolute(launcher)) {
+ mAppPath = launcher;
+ tmp = g_path_get_basename(launcher);
+ gchar* fullpath = g_find_program_in_path(tmp);
+ if (fullpath && mAppPath.Equals(fullpath)) mAppIsInPath = true;
+ g_free(fullpath);
+ } else {
+ tmp = g_find_program_in_path(launcher);
+ if (!tmp) return false;
+ mAppPath = tmp;
+ mAppIsInPath = true;
+ }
+
+ g_free(tmp);
+ return true;
+}
+
+bool nsGNOMEShellService::KeyMatchesAppName(const char* aKeyValue) const {
+ gchar* commandPath;
+ if (mUseLocaleFilenames) {
+ gchar* nativePath =
+ g_filename_from_utf8(aKeyValue, -1, nullptr, nullptr, nullptr);
+ if (!nativePath) {
+ NS_ERROR("Error converting path to filesystem encoding");
+ return false;
+ }
+
+ commandPath = g_find_program_in_path(nativePath);
+ g_free(nativePath);
+ } else {
+ commandPath = g_find_program_in_path(aKeyValue);
+ }
+
+ if (!commandPath) return false;
+
+ bool matches = mAppPath.Equals(commandPath);
+ g_free(commandPath);
+ return matches;
+}
+
+bool nsGNOMEShellService::CheckHandlerMatchesAppName(
+ const nsACString& handler) const {
+ gint argc;
+ gchar** argv;
+ nsAutoCString command(handler);
+
+ // The string will be something of the form: [/path/to/]browser "%s"
+ // We want to remove all of the parameters and get just the binary name.
+
+ if (g_shell_parse_argv(command.get(), &argc, &argv, nullptr) && argc > 0) {
+ command.Assign(argv[0]);
+ g_strfreev(argv);
+ }
+
+ if (!KeyMatchesAppName(command.get()))
+ return false; // the handler is set to another app
+
+ return true;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::IsDefaultBrowser(bool aForAllTypes,
+ bool* aIsDefaultBrowser) {
+ *aIsDefaultBrowser = false;
+
+ if (widget::IsRunningUnderSnap()) {
+ const gchar* argv[] = {"xdg-settings", "check", "default-web-browser",
+ (MOZ_APP_NAME ".desktop"), nullptr};
+ GSpawnFlags flags = static_cast<GSpawnFlags>(G_SPAWN_SEARCH_PATH |
+ G_SPAWN_STDERR_TO_DEV_NULL);
+ gchar* output = nullptr;
+ gint exit_status = 0;
+ if (!g_spawn_sync(nullptr, (gchar**)argv, nullptr, flags, nullptr, nullptr,
+ &output, nullptr, &exit_status, nullptr)) {
+ return NS_OK;
+ }
+ if (exit_status != 0) {
+ g_free(output);
+ return NS_OK;
+ }
+ if (strcmp(output, "yes\n") == 0) {
+ *aIsDefaultBrowser = true;
+ }
+ g_free(output);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ nsAutoCString handler;
+ nsCOMPtr<nsIGIOMimeApp> gioApp;
+
+ for (unsigned int i = 0; i < ArrayLength(appProtocols); ++i) {
+ if (!appProtocols[i].essential) continue;
+
+ if (!IsDefaultForSchemeHelper(nsDependentCString(appProtocols[i].name),
+ giovfs)) {
+ return NS_OK;
+ }
+ }
+
+ *aIsDefaultBrowser = true;
+
+ return NS_OK;
+}
+
+bool nsGNOMEShellService::IsDefaultForSchemeHelper(
+ const nsACString& aScheme, nsIGIOService* giovfs) const {
+ nsCOMPtr<nsIGIOService> gioService;
+ if (!giovfs) {
+ gioService = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ giovfs = gioService.get();
+ }
+
+ if (!giovfs) {
+ return false;
+ }
+
+ nsCOMPtr<nsIGIOMimeApp> gioApp;
+ nsCOMPtr<nsIHandlerApp> handlerApp;
+ giovfs->GetAppForURIScheme(aScheme, getter_AddRefs(handlerApp));
+ gioApp = do_QueryInterface(handlerApp);
+ if (!gioApp) {
+ return false;
+ }
+
+ nsAutoCString handler;
+ gioApp->GetCommand(handler);
+ return CheckHandlerMatchesAppName(handler);
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::IsDefaultForScheme(const nsACString& aScheme,
+ bool* aIsDefaultBrowser) {
+ *aIsDefaultBrowser = IsDefaultForSchemeHelper(aScheme, nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::SetDefaultBrowser(bool aClaimAllTypes, bool aForAllUsers) {
+#ifdef DEBUG
+ if (aForAllUsers)
+ NS_WARNING(
+ "Setting the default browser for all users is not yet supported");
+#endif
+
+ if (widget::IsRunningUnderSnap()) {
+ const gchar* argv[] = {"xdg-settings", "set", "default-web-browser",
+ (MOZ_APP_NAME ".desktop"), nullptr};
+ GSpawnFlags flags = static_cast<GSpawnFlags>(G_SPAWN_SEARCH_PATH |
+ G_SPAWN_STDOUT_TO_DEV_NULL |
+ G_SPAWN_STDERR_TO_DEV_NULL);
+ g_spawn_sync(nullptr, (gchar**)argv, nullptr, flags, nullptr, nullptr,
+ nullptr, nullptr, nullptr, nullptr);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
+ if (giovfs) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ components::StringBundle::Service(&rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ rv = bundleService->CreateBundle(BRAND_PROPERTIES,
+ getter_AddRefs(brandBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString brandShortName;
+ brandBundle->GetStringFromName("brandShortName", brandShortName);
+
+ // use brandShortName as the application id.
+ NS_ConvertUTF16toUTF8 id(brandShortName);
+ nsCOMPtr<nsIGIOMimeApp> appInfo;
+ rv = giovfs->FindAppFromCommand(mAppPath, getter_AddRefs(appInfo));
+ if (NS_FAILED(rv)) {
+ // Application was not found in the list of installed applications
+ // provided by OS. Fallback to create appInfo from command and name.
+ rv = giovfs->CreateAppFromCommand(mAppPath, id, getter_AddRefs(appInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // set handler for the protocols
+ for (unsigned int i = 0; i < ArrayLength(appProtocols); ++i) {
+ if (appProtocols[i].essential || aClaimAllTypes) {
+ appInfo->SetAsDefaultForURIScheme(
+ nsDependentCString(appProtocols[i].name));
+ }
+ }
+
+ // set handler for .html and xhtml files and MIME types:
+ if (aClaimAllTypes) {
+ // Add mime types for html, xhtml extension and set app to just created
+ // appinfo.
+ for (unsigned int i = 0; i < ArrayLength(appTypes); ++i) {
+ appInfo->SetAsDefaultForMimeType(
+ nsDependentCString(appTypes[i].mimeType));
+ appInfo->SetAsDefaultForFileExtensions(
+ nsDependentCString(appTypes[i].extensions));
+ }
+ }
+ }
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ (void)prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true);
+ // Reset the number of times the dialog should be shown
+ // before it is silenced.
+ (void)prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::GetCanSetDesktopBackground(bool* aResult) {
+ // setting desktop background is currently only supported
+ // for Gnome or desktops using the same GSettings keys
+ if (widget::IsGnomeDesktopEnvironment()) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ *aResult = !!getenv("GNOME_DESKTOP_SESSION_ID");
+ return NS_OK;
+}
+
+static nsresult WriteImage(const nsCString& aPath, imgIContainer* aImage) {
+ RefPtr<GdkPixbuf> pixbuf = nsImageToPixbuf::ImageToPixbuf(aImage);
+ if (!pixbuf) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ gboolean res = gdk_pixbuf_save(pixbuf, aPath.get(), "png", nullptr, nullptr);
+ return res ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::SetDesktopBackground(dom::Element* aElement,
+ int32_t aPosition,
+ const nsACString& aImageName) {
+ nsCOMPtr<nsIImageLoadingContent> imageContent = do_QueryInterface(aElement);
+ if (!imageContent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get the image container
+ nsCOMPtr<imgIRequest> request;
+ imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
+ getter_AddRefs(request));
+ if (!request) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<imgIContainer> container;
+ request->GetImage(getter_AddRefs(container));
+ if (!container) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Set desktop wallpaper filling style
+ nsAutoCString options;
+ if (aPosition == BACKGROUND_TILE)
+ options.AssignLiteral("wallpaper");
+ else if (aPosition == BACKGROUND_STRETCH)
+ options.AssignLiteral("stretched");
+ else if (aPosition == BACKGROUND_FILL)
+ options.AssignLiteral("zoom");
+ else if (aPosition == BACKGROUND_FIT)
+ options.AssignLiteral("scaled");
+ else if (aPosition == BACKGROUND_SPAN)
+ options.AssignLiteral("spanned");
+ else
+ options.AssignLiteral("centered");
+
+ // Write the background file to the home directory.
+ nsAutoCString filePath(PR_GetEnv("HOME"));
+ nsAutoString brandName;
+
+ // get the product brand name from localized strings
+ if (nsCOMPtr<nsIStringBundleService> bundleService =
+ components::StringBundle::Service()) {
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ bundleService->CreateBundle(BRAND_PROPERTIES, getter_AddRefs(brandBundle));
+ if (bundleService) {
+ brandBundle->GetStringFromName("brandShortName", brandName);
+ }
+ }
+
+ // build the file name
+ filePath.Append('/');
+ filePath.Append(NS_ConvertUTF16toUTF8(brandName));
+ filePath.AppendLiteral("_wallpaper.png");
+
+ // write the image to a file in the home dir
+ MOZ_TRY(WriteImage(filePath, container));
+
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ if (!gsettings) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIGSettingsCollection> backgroundSettings;
+ gsettings->GetCollectionForSchema(kDesktopBGSchema,
+ getter_AddRefs(backgroundSettings));
+ if (!backgroundSettings) {
+ return NS_ERROR_FAILURE;
+ }
+
+ GUniquePtr<gchar> fileURI(
+ g_filename_to_uri(filePath.get(), nullptr, nullptr));
+ if (!fileURI) {
+ return NS_ERROR_FAILURE;
+ }
+
+ backgroundSettings->SetString("picture-options"_ns, options);
+ backgroundSettings->SetString("picture-uri"_ns,
+ nsDependentCString(fileURI.get()));
+ backgroundSettings->SetString("picture-uri-dark"_ns,
+ nsDependentCString(fileURI.get()));
+ backgroundSettings->SetBoolean("draw-background"_ns, true);
+ return NS_OK;
+}
+
+#define COLOR_16_TO_8_BIT(_c) ((_c) >> 8)
+#define COLOR_8_TO_16_BIT(_c) ((_c) << 8 | (_c))
+
+NS_IMETHODIMP
+nsGNOMEShellService::GetDesktopBackgroundColor(uint32_t* aColor) {
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ nsCOMPtr<nsIGSettingsCollection> background_settings;
+ nsAutoCString background;
+
+ if (gsettings) {
+ gsettings->GetCollectionForSchema(kDesktopBGSchema,
+ getter_AddRefs(background_settings));
+ if (background_settings) {
+ background_settings->GetString(kDesktopColorGSKey, background);
+ }
+ }
+
+ if (background.IsEmpty()) {
+ *aColor = 0;
+ return NS_OK;
+ }
+
+ GdkColor color;
+ gboolean success = gdk_color_parse(background.get(), &color);
+
+ NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
+
+ *aColor = COLOR_16_TO_8_BIT(color.red) << 16 |
+ COLOR_16_TO_8_BIT(color.green) << 8 | COLOR_16_TO_8_BIT(color.blue);
+ return NS_OK;
+}
+
+static void ColorToCString(uint32_t aColor, nsCString& aResult) {
+ // The #rrrrggggbbbb format is used to match gdk_color_to_string()
+ aResult.SetLength(13);
+ char* buf = aResult.BeginWriting();
+ if (!buf) return;
+
+ uint16_t red = COLOR_8_TO_16_BIT((aColor >> 16) & 0xff);
+ uint16_t green = COLOR_8_TO_16_BIT((aColor >> 8) & 0xff);
+ uint16_t blue = COLOR_8_TO_16_BIT(aColor & 0xff);
+
+ snprintf(buf, 14, "#%04x%04x%04x", red, green, blue);
+}
+
+NS_IMETHODIMP
+nsGNOMEShellService::SetDesktopBackgroundColor(uint32_t aColor) {
+ NS_ASSERTION(aColor <= 0xffffff, "aColor has extra bits");
+ nsAutoCString colorString;
+ ColorToCString(aColor, colorString);
+
+ nsCOMPtr<nsIGSettingsService> gsettings =
+ do_GetService(NS_GSETTINGSSERVICE_CONTRACTID);
+ if (gsettings) {
+ nsCOMPtr<nsIGSettingsCollection> background_settings;
+ gsettings->GetCollectionForSchema(nsLiteralCString(kDesktopBGSchema),
+ getter_AddRefs(background_settings));
+ if (background_settings) {
+ background_settings->SetString(kDesktopColorGSKey, colorString);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
diff --git a/browser/components/shell/nsGNOMEShellService.h b/browser/components/shell/nsGNOMEShellService.h
new file mode 100644
index 0000000000..12190cea81
--- /dev/null
+++ b/browser/components/shell/nsGNOMEShellService.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsgnomeshellservice_h____
+#define nsgnomeshellservice_h____
+
+#include "nsIGNOMEShellService.h"
+#include "nsToolkitShellService.h"
+#include "nsString.h"
+#ifdef MOZ_ENABLE_DBUS
+# include "nsGNOMEShellSearchProvider.h"
+#endif
+
+class nsIGIOService;
+
+class nsGNOMEShellService final : public nsIGNOMEShellService,
+ public nsToolkitShellService {
+ public:
+ nsGNOMEShellService() : mAppIsInPath(false) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+ NS_DECL_NSIGNOMESHELLSERVICE
+
+ nsresult Init();
+
+ private:
+ ~nsGNOMEShellService() {}
+
+ bool KeyMatchesAppName(const char* aKeyValue) const;
+ bool CheckHandlerMatchesAppName(const nsACString& handler) const;
+ bool IsDefaultForSchemeHelper(const nsACString& aScheme,
+ nsIGIOService* giovfs) const;
+
+#ifdef MOZ_ENABLE_DBUS
+ nsGNOMEShellSearchProvider mSearchProvider;
+#endif
+ bool GetAppPathFromLauncher();
+ bool mUseLocaleFilenames;
+ nsCString mAppPath;
+ bool mAppIsInPath;
+};
+
+#endif // nsgnomeshellservice_h____
diff --git a/browser/components/shell/nsIGNOMEShellService.idl b/browser/components/shell/nsIGNOMEShellService.idl
new file mode 100644
index 0000000000..75fa5e0a30
--- /dev/null
+++ b/browser/components/shell/nsIGNOMEShellService.idl
@@ -0,0 +1,23 @@
+/* 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/. */
+
+#include "nsIShellService.idl"
+
+[scriptable, uuid(2ce5c803-edcd-443d-98eb-ceba86d02d13)]
+interface nsIGNOMEShellService : nsIShellService
+{
+ /**
+ * 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.
+ */
+ readonly attribute boolean canSetDesktopBackground;
+
+ /**
+ * Returns true if Firefox is set as the default handler for the scheme.
+ */
+ boolean isDefaultForScheme(in AUTF8String aScheme);
+};
diff --git a/browser/components/shell/nsIMacShellService.idl b/browser/components/shell/nsIMacShellService.idl
new file mode 100644
index 0000000000..52f97ab4a5
--- /dev/null
+++ b/browser/components/shell/nsIMacShellService.idl
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsIShellService.idl"
+
+[scriptable, uuid(387fdc80-0077-4b60-a0d9-d9e80a83ba64)]
+interface nsIMacShellService : nsIShellService
+{
+ /**
+ * Opens the desktop preferences, e.g. for after setting the background.
+ */
+ void showDesktopPreferences();
+
+ /**
+ * @param aPaneID used by macOS to identify the pane to open.
+ * Example arguments:
+ * * "" - use the default Security and Privacy pane.
+ * * General
+ * * Privacy
+ * * Privacy_AllFiles
+ * * Privacy_Camera
+ * * Privacy_Microphone
+ */
+ void showSecurityPreferences(in ACString aPaneID);
+};
diff --git a/browser/components/shell/nsIShellService.idl b/browser/components/shell/nsIShellService.idl
new file mode 100644
index 0000000000..4eae32c03e
--- /dev/null
+++ b/browser/components/shell/nsIShellService.idl
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+webidl Element;
+
+[scriptable, uuid(2d1a95e4-5bd8-4eeb-b0a8-c1455fd2a357)]
+interface nsIShellService : nsISupports
+{
+ /**
+ * Determines whether or not Firefox is the "Default Browser."
+ * This is simply whether or not Firefox is registered to handle
+ * http links.
+ *
+ * @param aForAllTypes true if the check should be made for HTTP and HTML.
+ * false if the check should be made for HTTP only.
+ * This parameter may be ignored on some platforms.
+ */
+ boolean isDefaultBrowser([optional] in boolean aForAllTypes);
+
+ /**
+ * Registers Firefox as the "Default Browser."
+ *
+ * @param aClaimAllTypes Register Firefox as the handler for
+ * additional protocols (chrome etc)
+ * and web documents (.html, .xhtml etc).
+ * @param aForAllUsers Whether or not Firefox should attempt
+ * to become the default browser for all
+ * users on a multi-user system.
+ */
+ void setDefaultBrowser(in boolean aClaimAllTypes, in boolean aForAllUsers);
+
+ /**
+ * Flags for positioning/sizing of the Desktop Background image.
+ */
+ const long BACKGROUND_TILE = 1;
+ const long BACKGROUND_STRETCH = 2;
+ const long BACKGROUND_CENTER = 3;
+ const long BACKGROUND_FILL = 4;
+ const long BACKGROUND_FIT = 5;
+ const long BACKGROUND_SPAN = 6;
+
+ /**
+ * Sets the desktop background image using either the HTML <IMG>
+ * element supplied or the background image of the element supplied.
+ *
+ * @param aImageElement Either a HTML <IMG> element or an element with
+ * a background image from which to source the
+ * background image.
+ * @param aPosition How to place the image on the desktop
+ * @param aImageName The image name. Equivalent to the leaf name of the
+ * location.href.
+ */
+ void setDesktopBackground(in Element aElement,
+ in long aPosition,
+ in ACString aImageName);
+
+ /**
+ * The desktop background color, visible when no background image is
+ * used, or if the background image is centered and does not fill the
+ * entire screen. A rgb value, where (r << 16 | g << 8 | b)
+ */
+ attribute unsigned long desktopBackgroundColor;
+};
diff --git a/browser/components/shell/nsIWindowsShellService.idl b/browser/components/shell/nsIWindowsShellService.idl
new file mode 100644
index 0000000000..26025290e9
--- /dev/null
+++ b/browser/components/shell/nsIWindowsShellService.idl
@@ -0,0 +1,187 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+[scriptable, uuid(fb9b59db-5a91-4e67-92b6-35e7d6e6d3fd)]
+interface nsIWindowsShellService : nsISupports
+{
+ /*
+ * Creates a new shortcut (.lnk) file. This shortcut will be recorded in
+ * a new shortcuts log file located in %PROGRAMDATA%\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38
+ * that is named after the currently running application and current user, eg:
+ * Firefox_user123_shortcuts.ini.
+ *
+ * For reasons that we haven't been able to pin down, these shortcuts get created with
+ * extra metadata on them (KnownFolderDataBlock, SpecialFolderDataBlock) that cause
+ * the Windows ShellLink classes to improperly read their target path with certain
+ * parameters. This causes any 32-bit programs that read the links (such as our
+ * installer and uninstaller) to think that 64-bit installs are located in the 32-bit
+ * Program Files directory.
+ * See https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/6f2e7920-50a9-459d-bfdd-316e459e87c0/ishelllink-getpath-returns-wrong-folder-for-64-bit-application-when-called-from-32-bit-application
+ * for some additional discussion of this.
+ *
+ * @param aBinary Target file of the shortcut.
+ * @param aArguments Arguments to set for the shortcut. May be empty.
+ * @param aDescription The description of the shortcut. The string used here
+ * shows up as the hover text of the shortcut in Explorer and on the
+ * Taskbar (if the shortcut is pinned there).
+ * @param aIconFile The file containing the desired icon for the shortcut. This
+ * can be the same file as aBinary.
+ * @param aIconIndex The index of the in aIconFile. Note that this is 0 based index
+ * that IShellLinkW requires, _not_ a Resource ID that is sometimes used
+ * for icons.
+ * @param aAppUserModelId The App User Model ID to set for the shortcut. This will
+ * affect which icon on the Taskbar the application groups with when first
+ * launched.
+ * @param aShortcutFolder The special Windows folder to create the shortcut in. One of:
+ * CommonStartMenu, StartMenu, PublicDesktop, Desktop, or QuickLaunch.
+ * @param aShortcutName The filename of the shortcut within aShortcutFolder.
+ * @return The full native path to the created shortcut.
+ *
+ * @throws NS_ERROR_INVALID_ARG if an invalid shortcut folder is passed
+ * @throws NS_ERROR_FILE_NOT_FOUND if the shortcut file or shortcuts log cannot be
+ * created or accessed
+ * @throws NS_ERROR_FAILURE for other types of failures
+ */
+ [implicit_jscontext]
+ Promise createShortcut(in nsIFile aBinary, in Array<AString> aArguments,
+ in AString aDescription, in nsIFile aIconFile, in unsigned short aIconIndex,
+ in AString aAppUserModelId, in AString aShortcutFolder, in AString aShortcutName);
+
+ /*
+ * Pin the current app to the taskbar. If aPrivateBrowsing is true, the
+ * Private Browsing version of the app (with a different icon and launch
+ * arguments) will be pinned instead.
+ *
+ * This MUST only be used in response to an active request from the user.
+ *
+ * If it exists, uses an existing shortcut on the Desktop or Start Menu,
+ * which would have been created by the installer (for All Users or
+ * Current User). If none can be found, one will be created with the correct
+ * AUMID for proper launching and grouping.
+ *
+ * NOTE: It is possible for the shortcut match to fail even when a
+ * shortcut refers to the current executable, if the paths differ due
+ * to e.g. symlinks. This should be rare.
+ *
+ * This is fully supported on Windows 7, 8, and 11. On Windows 10, it will
+ * definitely fail on a version prior to build 1809 (October 2018 Update).
+ *
+ * NOTE: Can only run on the main thread, but the actual work occurs on a
+ * background thread.
+ *
+ * @throws NS_ERROR_NOT_SAME_THREAD
+ * if called off main thread.
+ * @throws NS_ERROR_NOT_AVAILABLE
+ * if OS is not at least Windows 10 build 1809, or if creating the
+ * Taskband Pin object fails
+ * @throws NS_ERROR_FAILURE
+ * for unexpected errors
+ *
+ * @rejects NS_ERROR_FILE_NOT_FOUND
+ * if a shortcut matching this app's AUMID and exe path wasn't found
+ *
+ * @returns {Promise<void>} A promise that resolves to |undefined| if
+ * successful or rejects with an nserror.
+ */
+ [implicit_jscontext]
+ Promise pinCurrentAppToTaskbarAsync(in bool aPrivateBrowsing);
+
+ /*
+ * Do a dry run of pinCurrentAppToTaskbar().
+ *
+ * NOTE: Can only be run on the main thread, but the actual work occurs on a
+ * background thread.
+ *
+ * This does all the same checks and setup, throws the same errors, but doesn't
+ * do the final step of creating the pin.
+ *
+ * @throws same as pinCurrentAppToTaskbarAsync()
+ * @rejects same as pinCurrentAppToTaskbarAsync()
+ * @returns same as pinCurrentAppToTaskbarAsync()
+ */
+ [implicit_jscontext]
+ Promise checkPinCurrentAppToTaskbarAsync(in bool aPrivateBrowsing);
+
+ /*
+ * Search for the current executable among taskbar pins
+ *
+ * NOTE: Can only be run on the main thread, but the actual work occurs on a
+ * background thread.
+ *
+ * NOTE: It is possible for the check to fail even when a taskbar pin refers
+ * to this executable, if the paths differ due to e.g. symlinks.
+ * It is also possible for the check to succeed with a shortcut that doesn't
+ * actually appear on the taskbar.
+ * These cases should be rare.
+ *
+ * @return Promise that always resolves, true if pinned, false otherwise
+ * @throws NS_ERROR_NOT_SAME_THREAD if not run on the main thread
+ *
+ */
+ [implicit_jscontext]
+ Promise isCurrentAppPinnedToTaskbarAsync(in AString aumid);
+
+ /*
+ * Determine where a given shortcut likely appears in the shell.
+ *
+ * Returns one of:
+ * - "StartMenu" or "StartMenuPrivate", Current User or All Users Start
+ * Menu, including pins
+ * - "Desktop" or "DesktopPrivate", Current User or All Users Desktop
+ * - "Taskbar" or "TaskbarPrivate", Taskbar Pins
+ * - "" otherwise
+ *
+ * If a Private Browsing shortcut was used to launch, the "Private"
+ * variant of one of the above entries will be returned.
+ *
+ * NOTE: This tries to avoid I/O, so paths are compared directly as
+ * strings, which may not be accurate in all cases. It is intended
+ * for noncritical telemetry use.
+ */
+ AString classifyShortcut(in AString aPath);
+
+ [implicit_jscontext]
+ Promise hasMatchingShortcut(in AString aAUMID, in bool aPrivateBrowsing);
+
+ /*
+ * Check if setDefaultBrowserUserChoice() is expected to succeed.
+ *
+ * This checks the ProgIDs for this installation, and the hash of the existing
+ * UserChoice association.
+ *
+ * @return true if the check succeeds, false otherwise.
+ */
+ bool canSetDefaultBrowserUserChoice();
+
+ /*
+ * checkAllProgIDsExist() and checkBrowserUserChoiceHashes() are components
+ * of canSetDefaultBrowserUserChoice(), broken out for telemetry purposes.
+ *
+ * @return true if the check succeeds, false otherwise.
+ */
+ bool checkAllProgIDsExist();
+ bool checkBrowserUserChoiceHashes();
+
+ /*
+ * Determines whether or not Firefox is the "Default Handler", i.e.,
+ * is registered to handle, the given file extension (like ".pdf")
+ * or protocol (like "https").
+ */
+ boolean isDefaultHandlerFor(in AString aFileExtensionOrProtocol);
+
+
+ /*
+ * Return the Windows ProgID currently registered to handle the gven
+ * file extension (like ".pdf") or protocol (like "https").
+ *
+ * @return string ProgID, or "" when no association is registered.
+ * @throws NS_ERROR_FAILURE when the file extension or protocol
+ * cannot be determined.
+ */
+ AString queryCurrentDefaultHandlerFor(in AString aFileExtensionOrProtocol);
+};
diff --git a/browser/components/shell/nsMacShellService.cpp b/browser/components/shell/nsMacShellService.cpp
new file mode 100644
index 0000000000..2482032e42
--- /dev/null
+++ b/browser/components/shell/nsMacShellService.cpp
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "CocoaFileUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIImageLoadingContent.h"
+#include "mozilla/dom/Document.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIContent.h"
+#include "nsICookieJarSettings.h"
+#include "nsIObserverService.h"
+#include "nsIWebBrowserPersist.h"
+#include "nsMacShellService.h"
+#include "nsIProperties.h"
+#include "nsServiceManagerUtils.h"
+#include "nsShellService.h"
+#include "nsString.h"
+#include "nsIDocShell.h"
+#include "nsILoadContext.h"
+#include "nsIPrefService.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/ReferrerInfo.h"
+#include "DesktopBackgroundImage.h"
+
+#include <Carbon/Carbon.h>
+#include <CoreFoundation/CoreFoundation.h>
+#include <ApplicationServices/ApplicationServices.h>
+
+using mozilla::dom::Element;
+using mozilla::widget::SetDesktopImage;
+
+#define NETWORK_PREFPANE "/System/Library/PreferencePanes/Network.prefPane"_ns
+#define DESKTOP_PREFPANE \
+ nsLiteralCString( \
+ "/System/Library/PreferencePanes/DesktopScreenEffectsPref.prefPane")
+
+#define SAFARI_BUNDLE_IDENTIFIER "com.apple.Safari"
+
+NS_IMPL_ISUPPORTS(nsMacShellService, nsIMacShellService, nsIShellService,
+ nsIToolkitShellService, nsIWebProgressListener)
+
+NS_IMETHODIMP
+nsMacShellService::IsDefaultBrowser(bool aForAllTypes,
+ bool* aIsDefaultBrowser) {
+ *aIsDefaultBrowser = false;
+
+ CFStringRef firefoxID = ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
+ if (!firefoxID) {
+ // CFBundleGetIdentifier is expected to return nullptr only if the specified
+ // bundle doesn't have a bundle identifier in its plist. In this case, that
+ // means a failure, since our bundle does have an identifier.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the default http handler's bundle ID (or nullptr if it has not been
+ // explicitly set)
+ CFStringRef defaultBrowserID =
+ ::LSCopyDefaultHandlerForURLScheme(CFSTR("http"));
+ if (defaultBrowserID) {
+ *aIsDefaultBrowser =
+ ::CFStringCompare(firefoxID, defaultBrowserID, 0) == kCFCompareEqualTo;
+ ::CFRelease(defaultBrowserID);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::SetDefaultBrowser(bool aClaimAllTypes, bool aForAllUsers) {
+ // Note: We don't support aForAllUsers on Mac OS X.
+
+ CFStringRef firefoxID = ::CFBundleGetIdentifier(::CFBundleGetMainBundle());
+ if (!firefoxID) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (::LSSetDefaultHandlerForURLScheme(CFSTR("http"), firefoxID) != noErr) {
+ return NS_ERROR_FAILURE;
+ }
+ if (::LSSetDefaultHandlerForURLScheme(CFSTR("https"), firefoxID) != noErr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aClaimAllTypes) {
+ if (::LSSetDefaultRoleHandlerForContentType(kUTTypeHTML, kLSRolesAll,
+ firefoxID) != noErr) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ (void)prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true);
+ // Reset the number of times the dialog should be shown
+ // before it is silenced.
+ (void)prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::SetDesktopBackground(Element* aElement, int32_t aPosition,
+ const nsACString& aImageName) {
+ // Note: We don't support aPosition on OS X.
+
+ // Get the image URI:
+ nsresult rv;
+ nsCOMPtr<nsIImageLoadingContent> imageContent =
+ do_QueryInterface(aElement, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> imageURI;
+ rv = imageContent->GetCurrentURI(getter_AddRefs(imageURI));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIURI* docURI = aElement->OwnerDoc()->GetDocumentURI();
+ if (!docURI) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIProperties> fileLocator(
+ do_GetService("@mozilla.org/file/directory_service;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current user's "Pictures" folder (That's ~/Pictures):
+ fileLocator->Get(NS_OSX_PICTURE_DOCUMENTS_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(mBackgroundFile));
+ if (!mBackgroundFile) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsAutoString fileNameUnicode;
+ CopyUTF8toUTF16(aImageName, fileNameUnicode);
+
+ // and add the imgage file name itself:
+ mBackgroundFile->Append(fileNameUnicode);
+
+ // Download the image; the desktop background will be set in OnStateChange()
+ nsCOMPtr<nsIWebBrowserPersist> wbp(do_CreateInstance(
+ "@mozilla.org/embedding/browser/nsWebBrowserPersist;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t flags = nsIWebBrowserPersist::PERSIST_FLAGS_NO_CONVERSION |
+ nsIWebBrowserPersist::PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ nsIWebBrowserPersist::PERSIST_FLAGS_FROM_CACHE;
+
+ wbp->SetPersistFlags(flags);
+ wbp->SetProgressListener(this);
+
+ nsCOMPtr<nsILoadContext> loadContext;
+ nsCOMPtr<nsISupports> container = aElement->OwnerDoc()->GetContainer();
+ nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(container);
+ if (docShell) {
+ loadContext = do_QueryInterface(docShell);
+ }
+
+ auto referrerInfo =
+ mozilla::MakeRefPtr<mozilla::dom::ReferrerInfo>(*aElement);
+
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings =
+ aElement->OwnerDoc()->CookieJarSettings();
+ return wbp->SaveURI(imageURI, aElement->NodePrincipal(), 0, referrerInfo,
+ cookieJarSettings, nullptr, nullptr, mBackgroundFile,
+ nsIContentPolicy::TYPE_IMAGE,
+ loadContext->UsePrivateBrowsing());
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsIURI* aLocation,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aState) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aStateFlags,
+ nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus) && (aStateFlags & STATE_STOP) &&
+ (aRequest == nullptr)) {
+ nsCOMPtr<nsIObserverService> os(
+ do_GetService("@mozilla.org/observer-service;1"));
+ if (os)
+ os->NotifyObservers(nullptr, "shell:desktop-background-changed", nullptr);
+
+ bool exists = false;
+ nsresult rv = mBackgroundFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) {
+ return NS_OK;
+ }
+
+ SetDesktopImage(mBackgroundFile);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMacShellService::ShowDesktopPreferences() {
+ nsCOMPtr<nsIFile> lf;
+ nsresult rv =
+ NS_NewNativeLocalFile(DESKTOP_PREFPANE, true, getter_AddRefs(lf));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool exists;
+ lf->Exists(&exists);
+ if (!exists) return NS_ERROR_FILE_NOT_FOUND;
+ return lf->Launch();
+}
+
+NS_IMETHODIMP
+nsMacShellService::GetDesktopBackgroundColor(uint32_t* aColor) {
+ // This method and |SetDesktopBackgroundColor| has no meaning on Mac OS X.
+ // The mac desktop preferences UI uses pictures for the few solid colors it
+ // supports.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMacShellService::SetDesktopBackgroundColor(uint32_t aColor) {
+ // This method and |GetDesktopBackgroundColor| has no meaning on Mac OS X.
+ // The mac desktop preferences UI uses pictures for the few solid colors it
+ // supports.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMacShellService::ShowSecurityPreferences(const nsACString& aPaneID) {
+ nsresult rv = NS_ERROR_NOT_AVAILABLE;
+
+ CFStringRef paneID = ::CFStringCreateWithBytes(
+ kCFAllocatorDefault, (const UInt8*)PromiseFlatCString(aPaneID).get(),
+ aPaneID.Length(), kCFStringEncodingUTF8, false);
+
+ if (paneID) {
+ CFStringRef format =
+ CFSTR("x-apple.systempreferences:com.apple.preference.security?%@");
+ if (format) {
+ CFStringRef urlStr =
+ CFStringCreateWithFormat(kCFAllocatorDefault, NULL, format, paneID);
+ if (urlStr) {
+ CFURLRef url = ::CFURLCreateWithString(NULL, urlStr, NULL);
+ rv = CocoaFileUtils::OpenURL(url);
+
+ ::CFRelease(urlStr);
+ }
+
+ ::CFRelease(format);
+ }
+
+ ::CFRelease(paneID);
+ }
+ return rv;
+}
diff --git a/browser/components/shell/nsMacShellService.h b/browser/components/shell/nsMacShellService.h
new file mode 100644
index 0000000000..bb08c72b4e
--- /dev/null
+++ b/browser/components/shell/nsMacShellService.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nsmacshellservice_h____
+#define nsmacshellservice_h____
+
+#include "nsToolkitShellService.h"
+#include "nsIMacShellService.h"
+#include "nsIWebProgressListener.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+
+class nsMacShellService : public nsIMacShellService,
+ public nsToolkitShellService,
+ public nsIWebProgressListener {
+ public:
+ nsMacShellService(){};
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+ NS_DECL_NSIMACSHELLSERVICE
+ NS_DECL_NSIWEBPROGRESSLISTENER
+
+ protected:
+ virtual ~nsMacShellService(){};
+
+ private:
+ nsCOMPtr<nsIFile> mBackgroundFile;
+};
+
+#endif // nsmacshellservice_h____
diff --git a/browser/components/shell/nsShellService.h b/browser/components/shell/nsShellService.h
new file mode 100644
index 0000000000..346a16ad69
--- /dev/null
+++ b/browser/components/shell/nsShellService.h
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#define PREF_CHECKDEFAULTBROWSER "browser.shell.checkDefaultBrowser"
+#define PREF_DEFAULTBROWSERCHECKCOUNT "browser.shell.defaultBrowserCheckCount"
+
+#define SHELLSERVICE_PROPERTIES \
+ "chrome://browser/locale/shellservice.properties"
+#define BRAND_PROPERTIES "chrome://branding/locale/brand.properties"
diff --git a/browser/components/shell/nsToolkitShellService.h b/browser/components/shell/nsToolkitShellService.h
new file mode 100644
index 0000000000..552f3cd65d
--- /dev/null
+++ b/browser/components/shell/nsToolkitShellService.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nstoolkitshellservice_h____
+#define nstoolkitshellservice_h____
+
+#include "nsIToolkitShellService.h"
+
+class nsToolkitShellService : public nsIToolkitShellService {
+ public:
+ NS_IMETHOD IsDefaultBrowser(bool aForAllTypes, bool* aIsDefaultBrowser) = 0;
+
+ NS_IMETHODIMP IsDefaultApplication(bool* aIsDefaultBrowser) {
+ // Only care about the http(s) protocol. This only matters on Windows.
+ return IsDefaultBrowser(false, aIsDefaultBrowser);
+ }
+};
+
+#endif // nstoolkitshellservice_h____
diff --git a/browser/components/shell/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp
new file mode 100644
index 0000000000..6863ad300c
--- /dev/null
+++ b/browser/components/shell/nsWindowsShellService.cpp
@@ -0,0 +1,1845 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#define UNICODE
+
+#include "nsWindowsShellService.h"
+
+#include "BinaryPath.h"
+#include "imgIContainer.h"
+#include "imgIRequest.h"
+#include "mozilla/RefPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIContent.h"
+#include "nsIImageLoadingContent.h"
+#include "nsIOutputStream.h"
+#include "nsIPrefService.h"
+#include "nsIStringBundle.h"
+#include "nsNetUtil.h"
+#include "nsServiceManagerUtils.h"
+#include "nsShellService.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIWindowsRegKey.h"
+#include "nsUnicharUtils.h"
+#include "nsIURLFormatter.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/intl/Localization.h"
+#include "WindowsDefaultBrowser.h"
+#include "WindowsUserChoice.h"
+#include "nsLocalFile.h"
+#include "nsIXULAppInfo.h"
+#include "nsINIParser.h"
+#include "nsNativeAppSupportWin.h"
+#include "nsWindowsHelpers.h"
+
+#include <windows.h>
+#include <shellapi.h>
+#include <propvarutil.h>
+#include <propkey.h>
+
+#ifdef __MINGW32__
+// MinGW-w64 headers are missing PropVariantToString.
+# include <propsys.h>
+PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch);
+# define UNLEN 256
+#else
+# include <Lmcons.h> // For UNLEN
+#endif
+
+#include <comutil.h>
+#include <objbase.h>
+#include <shlobj.h>
+#include <knownfolders.h>
+#include "WinUtils.h"
+#include "mozilla/widget/WinTaskbar.h"
+
+#include <mbstring.h>
+
+#define PIN_TO_TASKBAR_SHELL_VERB 5386
+#define PRIVATE_BROWSING_BINARY L"private_browsing.exe"
+
+#undef ACCESS_READ
+
+#ifndef MAX_BUF
+# define MAX_BUF 4096
+#endif
+
+#define REG_SUCCEEDED(val) (val == ERROR_SUCCESS)
+
+#define REG_FAILED(val) (val != ERROR_SUCCESS)
+
+#ifdef DEBUG
+# define NS_ENSURE_HRESULT(hres, ret) \
+ do { \
+ HRESULT result = hres; \
+ if (MOZ_UNLIKELY(FAILED(result))) { \
+ mozilla::SmprintfPointer msg = mozilla::Smprintf( \
+ "NS_ENSURE_HRESULT(%s, %s) failed with " \
+ "result 0x%" PRIX32, \
+ #hres, #ret, static_cast<uint32_t>(result)); \
+ NS_WARNING(msg.get()); \
+ return ret; \
+ } \
+ } while (false)
+#else
+# define NS_ENSURE_HRESULT(hres, ret) \
+ if (MOZ_UNLIKELY(FAILED(hres))) return ret
+#endif
+
+using mozilla::IsWin8OrLater;
+using namespace mozilla;
+using mozilla::intl::Localization;
+
+struct SysFreeStringDeleter {
+ void operator()(BSTR aPtr) { ::SysFreeString(aPtr); }
+};
+using BStrPtr = mozilla::UniquePtr<OLECHAR, SysFreeStringDeleter>;
+
+NS_IMPL_ISUPPORTS(nsWindowsShellService, nsIToolkitShellService,
+ nsIShellService, nsIWindowsShellService)
+
+static nsresult OpenKeyForReading(HKEY aKeyRoot, const nsAString& aKeyName,
+ HKEY* aKey) {
+ const nsString& flatName = PromiseFlatString(aKeyName);
+
+ DWORD res = ::RegOpenKeyExW(aKeyRoot, flatName.get(), 0, KEY_READ, aKey);
+ switch (res) {
+ case ERROR_SUCCESS:
+ break;
+ case ERROR_ACCESS_DENIED:
+ return NS_ERROR_FILE_ACCESS_DENIED;
+ case ERROR_FILE_NOT_FOUND:
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return NS_OK;
+}
+
+nsresult GetHelperPath(nsAutoString& aPath) {
+ nsresult rv;
+ nsCOMPtr<nsIProperties> directoryService =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> appHelper;
+ rv = directoryService->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
+ getter_AddRefs(appHelper));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = appHelper->SetNativeLeafName("uninstall"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = appHelper->AppendNative("helper.exe"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = appHelper->GetPath(aPath);
+
+ aPath.Insert(L'"', 0);
+ aPath.Append(L'"');
+ return rv;
+}
+
+nsresult LaunchHelper(nsAutoString& aPath) {
+ STARTUPINFOW si = {sizeof(si), 0};
+ PROCESS_INFORMATION pi = {0};
+
+ if (!CreateProcessW(nullptr, (LPWSTR)aPath.get(), nullptr, nullptr, FALSE, 0,
+ nullptr, nullptr, &si, &pi)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ return NS_OK;
+}
+
+static bool IsPathDefaultForClass(
+ const RefPtr<IApplicationAssociationRegistration>& pAAR, wchar_t* exePath,
+ LPCWSTR aClassName) {
+ LPWSTR registeredApp;
+ bool isProtocol = *aClassName != L'.';
+ ASSOCIATIONTYPE queryType = isProtocol ? AT_URLPROTOCOL : AT_FILEEXTENSION;
+ HRESULT hr = pAAR->QueryCurrentDefault(aClassName, queryType, AL_EFFECTIVE,
+ &registeredApp);
+ if (FAILED(hr)) {
+ return false;
+ }
+
+ nsAutoString regAppName(registeredApp);
+ CoTaskMemFree(registeredApp);
+
+ // Make sure the application path for this progID is this installation.
+ regAppName.AppendLiteral("\\shell\\open\\command");
+ HKEY theKey;
+ nsresult rv = OpenKeyForReading(HKEY_CLASSES_ROOT, regAppName, &theKey);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ wchar_t cmdFromReg[MAX_BUF] = L"";
+ DWORD len = sizeof(cmdFromReg);
+ DWORD res = ::RegQueryValueExW(theKey, nullptr, nullptr, nullptr,
+ (LPBYTE)cmdFromReg, &len);
+ ::RegCloseKey(theKey);
+ if (REG_FAILED(res)) {
+ return false;
+ }
+
+ nsAutoString pathFromReg(cmdFromReg);
+ nsLocalFile::CleanupCmdHandlerPath(pathFromReg);
+
+ return _wcsicmp(exePath, pathFromReg.Data()) == 0;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::IsDefaultBrowser(bool aForAllTypes,
+ bool* aIsDefaultBrowser) {
+ *aIsDefaultBrowser = false;
+
+ RefPtr<IApplicationAssociationRegistration> pAAR;
+ HRESULT hr = CoCreateInstance(
+ CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
+ if (FAILED(hr)) {
+ return NS_OK;
+ }
+
+ wchar_t exePath[MAXPATHLEN] = L"";
+ nsresult rv = BinaryPath::GetLong(exePath);
+
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ *aIsDefaultBrowser = IsPathDefaultForClass(pAAR, exePath, L"http");
+ if (*aIsDefaultBrowser && aForAllTypes) {
+ *aIsDefaultBrowser = IsPathDefaultForClass(pAAR, exePath, L".html");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::IsDefaultHandlerFor(
+ const nsAString& aFileExtensionOrProtocol, bool* aIsDefaultHandlerFor) {
+ *aIsDefaultHandlerFor = false;
+
+ RefPtr<IApplicationAssociationRegistration> pAAR;
+ HRESULT hr = CoCreateInstance(
+ CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
+ if (FAILED(hr)) {
+ return NS_OK;
+ }
+
+ wchar_t exePath[MAXPATHLEN] = L"";
+ nsresult rv = BinaryPath::GetLong(exePath);
+
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ const nsString& flatClass = PromiseFlatString(aFileExtensionOrProtocol);
+
+ *aIsDefaultHandlerFor = IsPathDefaultForClass(pAAR, exePath, flatClass.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::QueryCurrentDefaultHandlerFor(
+ const nsAString& aFileExtensionOrProtocol, nsAString& aResult) {
+ aResult.Truncate();
+
+ RefPtr<IApplicationAssociationRegistration> pAAR;
+ HRESULT hr = CoCreateInstance(
+ CLSID_ApplicationAssociationRegistration, nullptr, CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration, getter_AddRefs(pAAR));
+ if (FAILED(hr)) {
+ return NS_OK;
+ }
+
+ const nsString& flatClass = PromiseFlatString(aFileExtensionOrProtocol);
+
+ LPWSTR registeredApp;
+ bool isProtocol = flatClass.First() != L'.';
+ ASSOCIATIONTYPE queryType = isProtocol ? AT_URLPROTOCOL : AT_FILEEXTENSION;
+ hr = pAAR->QueryCurrentDefault(flatClass.get(), queryType, AL_EFFECTIVE,
+ &registeredApp);
+ if (hr == HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION)) {
+ return NS_OK;
+ }
+ NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
+
+ aResult = registeredApp;
+ CoTaskMemFree(registeredApp);
+
+ return NS_OK;
+}
+
+nsresult nsWindowsShellService::LaunchControlPanelDefaultsSelectionUI() {
+ IApplicationAssociationRegistrationUI* pAARUI;
+ HRESULT hr = CoCreateInstance(
+ CLSID_ApplicationAssociationRegistrationUI, NULL, CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistrationUI, (void**)&pAARUI);
+ if (SUCCEEDED(hr)) {
+ mozilla::UniquePtr<wchar_t[]> appRegName;
+ GetAppRegName(appRegName);
+ hr = pAARUI->LaunchAdvancedAssociationUI(appRegName.get());
+ pAARUI->Release();
+ }
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsWindowsShellService::LaunchControlPanelDefaultPrograms() {
+ return ::LaunchControlPanelDefaultPrograms() ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::CheckAllProgIDsExist(bool* aResult) {
+ *aResult = false;
+ nsAutoString aumid;
+ if (!mozilla::widget::WinTaskbar::GetAppUserModelID(aumid)) {
+ return NS_OK;
+ }
+ *aResult =
+ CheckProgIDExists(FormatProgID(L"FirefoxURL", aumid.get()).get()) &&
+ CheckProgIDExists(FormatProgID(L"FirefoxHTML", aumid.get()).get()) &&
+ CheckProgIDExists(FormatProgID(L"FirefoxPDF", aumid.get()).get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::CheckBrowserUserChoiceHashes(bool* aResult) {
+ *aResult = ::CheckBrowserUserChoiceHashes();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::CanSetDefaultBrowserUserChoice(bool* aResult) {
+ *aResult = false;
+// If the WDBA is not available, this could never succeed.
+#ifdef MOZ_DEFAULT_BROWSER_AGENT
+ bool progIDsExist = false;
+ bool hashOk = false;
+ *aResult = NS_SUCCEEDED(CheckAllProgIDsExist(&progIDsExist)) &&
+ progIDsExist &&
+ NS_SUCCEEDED(CheckBrowserUserChoiceHashes(&hashOk)) && hashOk;
+#endif
+ return NS_OK;
+}
+
+nsresult nsWindowsShellService::LaunchModernSettingsDialogDefaultApps() {
+ return ::LaunchModernSettingsDialogDefaultApps() ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsWindowsShellService::InvokeHTTPOpenAsVerb() {
+ nsCOMPtr<nsIURLFormatter> formatter(
+ do_GetService("@mozilla.org/toolkit/URLFormatterService;1"));
+ if (!formatter) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsString urlStr;
+ nsresult rv = formatter->FormatURLPref(u"app.support.baseURL"_ns, urlStr);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!StringBeginsWith(urlStr, u"https://"_ns)) {
+ return NS_ERROR_FAILURE;
+ }
+ urlStr.AppendLiteral("win10-default-browser");
+
+ SHELLEXECUTEINFOW seinfo = {sizeof(SHELLEXECUTEINFOW)};
+ seinfo.lpVerb = L"openas";
+ seinfo.lpFile = urlStr.get();
+ seinfo.nShow = SW_SHOWNORMAL;
+ if (!ShellExecuteExW(&seinfo)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+nsresult nsWindowsShellService::LaunchHTTPHandlerPane() {
+ OPENASINFO info;
+ info.pcszFile = L"http";
+ info.pcszClass = nullptr;
+ info.oaifInFlags =
+ OAIF_FORCE_REGISTRATION | OAIF_URL_PROTOCOL | OAIF_REGISTER_EXT;
+
+ HRESULT hr = SHOpenWithDialog(nullptr, &info);
+ if (SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED))) {
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::SetDefaultBrowser(bool aClaimAllTypes,
+ bool aForAllUsers) {
+ // If running from within a package, don't attempt to set default with
+ // the helper, as it will not work and will only confuse our package's
+ // virtualized registry.
+ nsresult rv = NS_OK;
+ if (!widget::WinUtils::HasPackageIdentity()) {
+ nsAutoString appHelperPath;
+ if (NS_FAILED(GetHelperPath(appHelperPath))) return NS_ERROR_FAILURE;
+
+ if (aForAllUsers) {
+ appHelperPath.AppendLiteral(" /SetAsDefaultAppGlobal");
+ } else {
+ appHelperPath.AppendLiteral(" /SetAsDefaultAppUser");
+ }
+
+ rv = LaunchHelper(appHelperPath);
+ }
+
+ if (NS_SUCCEEDED(rv) && IsWin8OrLater()) {
+ if (aClaimAllTypes) {
+ if (IsWin10OrLater()) {
+ rv = LaunchModernSettingsDialogDefaultApps();
+ } else {
+ rv = LaunchControlPanelDefaultsSelectionUI();
+ }
+ // The above call should never really fail, but just in case
+ // fall back to showing the HTTP association screen only.
+ if (NS_FAILED(rv)) {
+ if (IsWin10OrLater()) {
+ rv = InvokeHTTPOpenAsVerb();
+ } else {
+ rv = LaunchHTTPHandlerPane();
+ }
+ }
+ } else {
+ // Windows 10 blocks attempts to load the
+ // HTTP Handler association dialog.
+ if (IsWin10OrLater()) {
+ rv = LaunchModernSettingsDialogDefaultApps();
+ } else {
+ rv = LaunchHTTPHandlerPane();
+ }
+
+ // The above call should never really fail, but just in case
+ // fall back to showing control panel for all defaults
+ if (NS_FAILED(rv)) {
+ rv = LaunchControlPanelDefaultsSelectionUI();
+ }
+ }
+ }
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ (void)prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true);
+ // Reset the number of times the dialog should be shown
+ // before it is silenced.
+ (void)prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0);
+ }
+
+ return rv;
+}
+
+static nsresult WriteBitmap(nsIFile* aFile, imgIContainer* aImage) {
+ nsresult rv;
+
+ RefPtr<gfx::SourceSurface> surface = aImage->GetFrame(
+ imgIContainer::FRAME_FIRST, imgIContainer::FLAG_SYNC_DECODE);
+ NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE);
+
+ // For either of the following formats we want to set the biBitCount member
+ // of the BITMAPINFOHEADER struct to 32, below. For that value the bitmap
+ // format defines that the A8/X8 WORDs in the bitmap byte stream be ignored
+ // for the BI_RGB value we use for the biCompression member.
+ MOZ_ASSERT(surface->GetFormat() == gfx::SurfaceFormat::B8G8R8A8 ||
+ surface->GetFormat() == gfx::SurfaceFormat::B8G8R8X8);
+
+ RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
+ NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
+
+ int32_t width = dataSurface->GetSize().width;
+ int32_t height = dataSurface->GetSize().height;
+ int32_t bytesPerPixel = 4 * sizeof(uint8_t);
+ uint32_t bytesPerRow = bytesPerPixel * width;
+
+ // initialize these bitmap structs which we will later
+ // serialize directly to the head of the bitmap file
+ BITMAPINFOHEADER bmi;
+ bmi.biSize = sizeof(BITMAPINFOHEADER);
+ bmi.biWidth = width;
+ bmi.biHeight = height;
+ bmi.biPlanes = 1;
+ bmi.biBitCount = (WORD)bytesPerPixel * 8;
+ bmi.biCompression = BI_RGB;
+ bmi.biSizeImage = bytesPerRow * height;
+ bmi.biXPelsPerMeter = 0;
+ bmi.biYPelsPerMeter = 0;
+ bmi.biClrUsed = 0;
+ bmi.biClrImportant = 0;
+
+ BITMAPFILEHEADER bf;
+ bf.bfType = 0x4D42; // 'BM'
+ bf.bfReserved1 = 0;
+ bf.bfReserved2 = 0;
+ bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
+ bf.bfSize = bf.bfOffBits + bmi.biSizeImage;
+
+ // get a file output stream
+ nsCOMPtr<nsIOutputStream> stream;
+ rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ gfx::DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(gfx::DataSourceSurface::MapType::READ, &map)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // write the bitmap headers and rgb pixel data to the file
+ rv = NS_ERROR_FAILURE;
+ if (stream) {
+ uint32_t written;
+ stream->Write((const char*)&bf, sizeof(BITMAPFILEHEADER), &written);
+ if (written == sizeof(BITMAPFILEHEADER)) {
+ stream->Write((const char*)&bmi, sizeof(BITMAPINFOHEADER), &written);
+ if (written == sizeof(BITMAPINFOHEADER)) {
+ // write out the image data backwards because the desktop won't
+ // show bitmaps with negative heights for top-to-bottom
+ uint32_t i = map.mStride * height;
+ do {
+ i -= map.mStride;
+ stream->Write(((const char*)map.mData) + i, bytesPerRow, &written);
+ if (written == bytesPerRow) {
+ rv = NS_OK;
+ } else {
+ rv = NS_ERROR_FAILURE;
+ break;
+ }
+ } while (i != 0);
+ }
+ }
+
+ stream->Close();
+ }
+
+ dataSurface->Unmap();
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::SetDesktopBackground(dom::Element* aElement,
+ int32_t aPosition,
+ const nsACString& aImageName) {
+ if (!aElement || !aElement->IsHTMLElement(nsGkAtoms::img)) {
+ // XXX write background loading stuff!
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIImageLoadingContent> imageContent =
+ do_QueryInterface(aElement, &rv);
+ if (!imageContent) return rv;
+
+ // get the image container
+ nsCOMPtr<imgIRequest> request;
+ rv = imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
+ getter_AddRefs(request));
+ if (!request) return rv;
+
+ nsCOMPtr<imgIContainer> container;
+ rv = request->GetImage(getter_AddRefs(container));
+ if (!container) return NS_ERROR_FAILURE;
+
+ // get the file name from localized strings
+ nsCOMPtr<nsIStringBundleService> bundleService(
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundle> shellBundle;
+ rv = bundleService->CreateBundle(SHELLSERVICE_PROPERTIES,
+ getter_AddRefs(shellBundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // e.g. "Desktop Background.bmp"
+ nsAutoString fileLeafName;
+ rv = shellBundle->GetStringFromName("desktopBackgroundLeafNameWin",
+ fileLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the profile root directory
+ nsCOMPtr<nsIFile> file;
+ rv = NS_GetSpecialDirectory(NS_APP_APPLICATION_REGISTRY_DIR,
+ getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // eventually, the path is "%APPDATA%\Mozilla\Firefox\Desktop Background.bmp"
+ rv = file->Append(fileLeafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString path;
+ rv = file->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // write the bitmap to a file in the profile directory.
+ // We have to write old bitmap format for Windows 7 wallpapar support.
+ rv = WriteBitmap(file, container);
+
+ // if the file was written successfully, set it as the system wallpaper
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ u"Control Panel\\Desktop"_ns,
+ nsIWindowsRegKey::ACCESS_SET_VALUE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString tile;
+ nsAutoString style;
+ switch (aPosition) {
+ case BACKGROUND_TILE:
+ style.Assign('0');
+ tile.Assign('1');
+ break;
+ case BACKGROUND_CENTER:
+ style.Assign('0');
+ tile.Assign('0');
+ break;
+ case BACKGROUND_STRETCH:
+ style.Assign('2');
+ tile.Assign('0');
+ break;
+ case BACKGROUND_FILL:
+ style.AssignLiteral("10");
+ tile.Assign('0');
+ break;
+ case BACKGROUND_FIT:
+ style.Assign('6');
+ tile.Assign('0');
+ break;
+ case BACKGROUND_SPAN:
+ style.AssignLiteral("22");
+ tile.Assign('0');
+ break;
+ }
+
+ rv = regKey->WriteStringValue(u"TileWallpaper"_ns, tile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = regKey->WriteStringValue(u"WallpaperStyle"_ns, style);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = regKey->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ::SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, (PVOID)path.get(),
+ SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::GetDesktopBackgroundColor(uint32_t* aColor) {
+ uint32_t color = ::GetSysColor(COLOR_DESKTOP);
+ *aColor =
+ (GetRValue(color) << 16) | (GetGValue(color) << 8) | GetBValue(color);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::SetDesktopBackgroundColor(uint32_t aColor) {
+ int aParameters[2] = {COLOR_BACKGROUND, COLOR_DESKTOP};
+ BYTE r = (aColor >> 16);
+ BYTE g = (aColor << 16) >> 24;
+ BYTE b = (aColor << 24) >> 24;
+ COLORREF colors[2] = {RGB(r, g, b), RGB(r, g, b)};
+
+ ::SetSysColors(sizeof(aParameters) / sizeof(int), aParameters, colors);
+
+ nsresult rv;
+ nsCOMPtr<nsIWindowsRegKey> regKey =
+ do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
+ u"Control Panel\\Colors"_ns,
+ nsIWindowsRegKey::ACCESS_SET_VALUE);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ wchar_t rgb[12];
+ _snwprintf(rgb, 12, L"%u %u %u", r, g, b);
+
+ rv = regKey->WriteStringValue(u"Background"_ns, nsDependentString(rgb));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return regKey->Close();
+}
+
+/*
+ * Writes information about a shortcut to a shortcuts log in
+ * %PROGRAMDATA%\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38.
+ * (This is the same directory used for update staging.)
+ * For more on the shortcuts log format and purpose, consult
+ * /toolkit/mozapps/installer/windows/nsis/common.nsh.
+ *
+ * The shortcuts log created or appended here is named after
+ * the currently running application and current user SID.
+ * For example: Firefox_$SID_shortcuts.ini.
+ *
+ * If it does not exist, it will be created. If it exists
+ * and a matching shortcut named already exists in the file,
+ * a new one will not be appended.
+ *
+ * In an ideal world this function would not need aShortcutsLogDir
+ * passed to it, but it is called by at least one function that runs
+ * asynchronously, and is therefore unable to use nsDirectoryService
+ * to look it up itself.
+ */
+static nsresult WriteShortcutToLog(nsIFile* aShortcutsLogDir,
+ KNOWNFOLDERID aFolderId,
+ const nsAString& aShortcutName) {
+ // the section inside the shortcuts log
+ nsAutoCString section;
+ // the shortcuts log wants "Programs" shortcuts in its "STARTMENU" section
+ if (aFolderId == FOLDERID_CommonPrograms || aFolderId == FOLDERID_Programs) {
+ section.Assign("STARTMENU");
+ } else if (aFolderId == FOLDERID_PublicDesktop ||
+ aFolderId == FOLDERID_Desktop) {
+ section.Assign("DESKTOP");
+ } else {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsIFile> shortcutsLog;
+ nsresult rv = aShortcutsLogDir->GetParent(getter_AddRefs(shortcutsLog));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString appName;
+ nsCOMPtr<nsIXULAppInfo> appInfo =
+ do_GetService("@mozilla.org/xre/app-info;1");
+ rv = appInfo->GetName(appName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ auto userSid = GetCurrentUserStringSid();
+ if (!userSid) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+
+ nsAutoString filename;
+ filename.AppendPrintf("%s_%ls_shortcuts.ini", appName.get(), userSid.get());
+ rv = shortcutsLog->Append(filename);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsINIParser parser;
+ bool fileExists = false;
+ bool shortcutsLogEntryExists = false;
+ nsAutoCString keyName, shortcutName;
+ shortcutName = NS_ConvertUTF16toUTF8(aShortcutName);
+
+ shortcutsLog->IsFile(&fileExists);
+ // if the shortcuts log exists, find either an existing matching
+ // entry, or the next available shortcut index
+ if (fileExists) {
+ rv = parser.Init(shortcutsLog);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString iniShortcut;
+ // surely we'll never need more than 10 shortcuts in one section...
+ for (int i = 0; i < 10; i++) {
+ keyName.AssignLiteral("Shortcut");
+ keyName.AppendInt(i);
+ rv = parser.GetString(section.get(), keyName.get(), iniShortcut);
+ if (NS_FAILED(rv)) {
+ // we found an unused index
+ break;
+ } else if (iniShortcut.Equals(shortcutName)) {
+ shortcutsLogEntryExists = true;
+ }
+ }
+ // otherwise, this is safe to use
+ } else {
+ keyName.AssignLiteral("Shortcut0");
+ }
+
+ if (!shortcutsLogEntryExists) {
+ parser.SetString(section.get(), keyName.get(), shortcutName.get());
+ // We write this ourselves instead of using parser->WriteToFile because
+ // the INI parser in our uninstaller needs to read this, and only supports
+ // UTF-16LE encoding. nsINIParser does not support UTF-16.
+ nsAutoCString formatted;
+ parser.WriteToString(formatted);
+ FILE* writeFile;
+ rv = shortcutsLog->OpenANSIFileDesc("w,ccs=UTF-16LE", &writeFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ConvertUTF8toUTF16 formattedUTF16(formatted);
+ if (fwrite(formattedUTF16.get(), sizeof(wchar_t), formattedUTF16.Length(),
+ writeFile) != formattedUTF16.Length()) {
+ fclose(writeFile);
+ return NS_ERROR_FAILURE;
+ }
+ fclose(writeFile);
+ }
+
+ return NS_OK;
+}
+
+static nsresult CreateShortcutImpl(
+ nsIFile* aBinary, const CopyableTArray<nsString>& aArguments,
+ const nsAString& aDescription, nsIFile* aIconFile, uint16_t aIconIndex,
+ const nsAString& aAppUserModelId, KNOWNFOLDERID aShortcutFolder,
+ const nsAString& aShortcutName, const nsString& aShortcutFile,
+ nsIFile* aShortcutsLogDir) {
+ NS_ENSURE_ARG(aBinary);
+ NS_ENSURE_ARG(aIconFile);
+
+ nsresult rv =
+ WriteShortcutToLog(aShortcutsLogDir, aShortcutFolder, aShortcutName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<IShellLinkW> link;
+ HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, getter_AddRefs(link));
+ NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
+
+ nsString path(aBinary->NativePath());
+ link->SetPath(path.get());
+
+ wchar_t workingDir[MAX_PATH + 1];
+ wcscpy_s(workingDir, MAX_PATH + 1, aBinary->NativePath().get());
+ PathRemoveFileSpecW(workingDir);
+ link->SetWorkingDirectory(workingDir);
+
+ if (!aDescription.IsEmpty()) {
+ link->SetDescription(PromiseFlatString(aDescription).get());
+ }
+
+ // TODO: Properly escape quotes in the string, see bug 1604287.
+ nsString arguments;
+ for (auto& arg : aArguments) {
+ arguments.AppendPrintf("\"%S\" ", static_cast<const wchar_t*>(arg.get()));
+ }
+
+ link->SetArguments(arguments.get());
+
+ if (aIconFile) {
+ nsString icon(aIconFile->NativePath());
+ link->SetIconLocation(icon.get(), aIconIndex);
+ }
+
+ if (!aAppUserModelId.IsEmpty()) {
+ RefPtr<IPropertyStore> propStore;
+ hr = link->QueryInterface(IID_IPropertyStore, getter_AddRefs(propStore));
+ NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
+
+ PROPVARIANT pv;
+ if (FAILED(InitPropVariantFromString(
+ PromiseFlatString(aAppUserModelId).get(), &pv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ hr = propStore->SetValue(PKEY_AppUserModel_ID, pv);
+ PropVariantClear(&pv);
+ NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
+
+ hr = propStore->Commit();
+ NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
+ }
+
+ RefPtr<IPersistFile> persist;
+ hr = link->QueryInterface(IID_IPersistFile, getter_AddRefs(persist));
+ NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
+
+ hr = persist->Save(aShortcutFile.get(), TRUE);
+ NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::CreateShortcut(
+ nsIFile* aBinary, const nsTArray<nsString>& aArguments,
+ const nsAString& aDescription, nsIFile* aIconFile, uint16_t aIconIndex,
+ const nsAString& aAppUserModelId, const nsAString& aShortcutFolder,
+ const nsAString& aShortcutName, JSContext* aCx, dom::Promise** aPromise) {
+ if (!NS_IsMainThread()) {
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+ // In an ideal world we'd probably send along nsIFile pointers
+ // here, but it's easier to determine the needed shortcuts log
+ // entry with a KNOWNFOLDERID - so we pass this along instead
+ // and let CreateShortcutImpl take care of converting it to
+ // an nsIFile.
+ KNOWNFOLDERID folderId;
+ if (aShortcutFolder.Equals(L"Programs")) {
+ folderId = FOLDERID_Programs;
+ } else if (aShortcutFolder.Equals(L"Desktop")) {
+ folderId = FOLDERID_Desktop;
+ } else {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<nsIFile> updRoot, shortcutsLogDir;
+ nsresult nsrv =
+ NS_GetSpecialDirectory(XRE_UPDATE_ROOT_DIR, getter_AddRefs(updRoot));
+ NS_ENSURE_SUCCESS(nsrv, nsrv);
+ nsrv = updRoot->GetParent(getter_AddRefs(shortcutsLogDir));
+ NS_ENSURE_SUCCESS(nsrv, nsrv);
+
+ nsCOMPtr<nsIFile> shortcutFile;
+ if (folderId == FOLDERID_Programs) {
+ nsrv = NS_GetSpecialDirectory(NS_WIN_PROGRAMS_DIR,
+ getter_AddRefs(shortcutFile));
+ } else if (folderId == FOLDERID_Desktop) {
+ nsrv =
+ NS_GetSpecialDirectory(NS_OS_DESKTOP_DIR, getter_AddRefs(shortcutFile));
+ } else {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ if (NS_FAILED(nsrv)) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ shortcutFile->Append(aShortcutName);
+
+ auto promiseHolder = MakeRefPtr<nsMainThreadPtrHolder<dom::Promise>>(
+ "CreateShortcut promise", promise);
+
+ nsCOMPtr<nsIFile> binary(aBinary);
+ nsCOMPtr<nsIFile> iconFile(aIconFile);
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "CreateShortcut",
+ [binary, aArguments = CopyableTArray<nsString>(aArguments),
+ aDescription = nsString{aDescription}, iconFile, aIconIndex,
+ aAppUserModelId = nsString{aAppUserModelId}, folderId,
+ aShortcutFolder = nsString{aShortcutFolder},
+ aShortcutName = nsString{aShortcutName}, shortcutsLogDir,
+ shortcutFile, promiseHolder = std::move(promiseHolder)] {
+ nsresult rv = NS_ERROR_FAILURE;
+ HRESULT hr = CoInitialize(nullptr);
+
+ if (SUCCEEDED(hr)) {
+ rv = CreateShortcutImpl(
+ binary.get(), aArguments, aDescription, iconFile.get(),
+ aIconIndex, aAppUserModelId, folderId, aShortcutName,
+ shortcutFile->NativePath(), shortcutsLogDir.get());
+ CoUninitialize();
+ }
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "CreateShortcut callback",
+ [rv, shortcutFile, promiseHolder = std::move(promiseHolder)] {
+ dom::Promise* promise = promiseHolder.get()->get();
+
+ if (NS_SUCCEEDED(rv)) {
+ promise->MaybeResolve(shortcutFile->NativePath());
+ } else {
+ promise->MaybeReject(rv);
+ }
+ }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+// Look for any installer-created shortcuts in the given location that match
+// the given AUMID and EXE Path. If one is found, output its path.
+//
+// NOTE: DO NOT USE if a false negative (mismatch) is unacceptable.
+// aExePath is compared directly to the path retrieved from the shortcut.
+// Due to the presence of symlinks or other filesystem issues, it's possible
+// for different paths to refer to the same file, which would cause the check
+// to fail.
+// This should rarely be an issue as we are most likely to be run from a path
+// written by the installer (shortcut, association, launch from installer),
+// which also wrote the shortcuts. But it is possible.
+//
+// aCSIDL the CSIDL of the directory to look for matching shortcuts in
+// aAUMID the AUMID to check for
+// aExePath the target exe path to check for, should be a long path where
+// possible
+// aShortcutSubstring a substring to limit which shortcuts in aCSIDL are
+// inspected for a match. Only shortcuts whose filename
+// contains this substring will be considered
+// aShortcutPath outparam, set to matching shortcut path if NS_OK is returned.
+//
+// Returns
+// NS_ERROR_FAILURE on errors before any shortcuts were loaded
+// NS_ERROR_FILE_NOT_FOUND if no shortcuts matching aShortcutSubstring exist
+// NS_ERROR_FILE_ALREADY_EXISTS if shortcuts were found but did not match
+// aAUMID or aExePath
+// NS_OK if a matching shortcut is found
+static nsresult GetMatchingShortcut(int aCSIDL, const nsAString& aAUMID,
+ const wchar_t aExePath[MAXPATHLEN],
+ const nsAString& aShortcutSubstring,
+ /* out */ nsAutoString& aShortcutPath) {
+ nsresult result = NS_ERROR_FAILURE;
+
+ wchar_t folderPath[MAX_PATH] = {};
+ HRESULT hr = SHGetFolderPathW(nullptr, aCSIDL, nullptr, SHGFP_TYPE_CURRENT,
+ folderPath);
+ if (NS_WARN_IF(FAILED(hr))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (wcscat_s(folderPath, MAX_PATH, L"\\") != 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get list of shortcuts in aCSIDL
+ nsAutoString pattern(folderPath);
+ pattern.AppendLiteral("*.lnk");
+
+ WIN32_FIND_DATAW findData = {};
+ HANDLE hFindFile = FindFirstFileW(pattern.get(), &findData);
+ if (hFindFile == INVALID_HANDLE_VALUE) {
+ Unused << NS_WARN_IF(GetLastError() != ERROR_FILE_NOT_FOUND);
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ // Past this point we don't return until the end of the function,
+ // when FindClose() is called.
+
+ // todo: improve return values here
+ do {
+ // Skip any that don't contain aShortcutSubstring
+ // This is a case sensitive comparison, but that's probably fine for
+ // the vast majority of cases -- and certainly for all the ones where
+ // a shortcut was created by the installer.
+ if (StrStrIW(findData.cFileName, aShortcutSubstring.Data()) == NULL) {
+ continue;
+ }
+
+ nsAutoString path(folderPath);
+ path.Append(findData.cFileName);
+
+ // Create a shell link object for loading the shortcut
+ RefPtr<IShellLinkW> link;
+ HRESULT hr =
+ CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, getter_AddRefs(link));
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ // Load
+ RefPtr<IPersistFile> persist;
+ hr = link->QueryInterface(IID_IPersistFile, getter_AddRefs(persist));
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ hr = persist->Load(path.get(), STGM_READ);
+ if (FAILED(hr)) {
+ if (NS_WARN_IF(hr != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))) {
+ // empty branch, result unchanged but warning issued
+ } else {
+ // If we've ever gotten past this block, result will already be
+ // NS_ERROR_FILE_ALREADY_EXISTS, which is a more accurate error
+ // than NS_ERROR_FILE_NOT_FOUND.
+ if (result != NS_ERROR_FILE_ALREADY_EXISTS) {
+ result = NS_ERROR_FILE_NOT_FOUND;
+ }
+ }
+ continue;
+ }
+ result = NS_ERROR_FILE_ALREADY_EXISTS;
+
+ // Check the AUMID
+ RefPtr<IPropertyStore> propStore;
+ hr = link->QueryInterface(IID_IPropertyStore, getter_AddRefs(propStore));
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ PROPVARIANT pv;
+ hr = propStore->GetValue(PKEY_AppUserModel_ID, &pv);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ wchar_t storedAUMID[MAX_PATH];
+ hr = PropVariantToString(pv, storedAUMID, MAX_PATH);
+ PropVariantClear(&pv);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ if (!aAUMID.Equals(storedAUMID)) {
+ continue;
+ }
+
+ // Check the exe path
+ static_assert(MAXPATHLEN == MAX_PATH);
+ wchar_t storedExePath[MAX_PATH] = {};
+ // With no flags GetPath gets a long path
+ hr = link->GetPath(storedExePath, ArrayLength(storedExePath), nullptr, 0);
+ if (FAILED(hr) || hr == S_FALSE) {
+ continue;
+ }
+ // Case insensitive path comparison
+ if (wcsnicmp(storedExePath, aExePath, MAXPATHLEN) == 0) {
+ aShortcutPath.Assign(path);
+ result = NS_OK;
+ break;
+ }
+ } while (FindNextFileW(hFindFile, &findData));
+
+ FindClose(hFindFile);
+
+ return result;
+}
+
+static nsresult FindMatchingShortcut(const nsAString& aAppUserModelId,
+ const nsAString& aShortcutSubstring,
+ const bool aPrivateBrowsing,
+ nsAutoString& aShortcutPath) {
+ wchar_t exePath[MAXPATHLEN] = {};
+ if (NS_WARN_IF(NS_FAILED(BinaryPath::GetLong(exePath)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aPrivateBrowsing) {
+ if (!PathRemoveFileSpecW(exePath)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!PathAppendW(exePath, L"private_browsing.exe")) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ int shortcutCSIDLs[] = {CSIDL_COMMON_PROGRAMS, CSIDL_PROGRAMS,
+ CSIDL_COMMON_DESKTOPDIRECTORY,
+ CSIDL_DESKTOPDIRECTORY};
+ for (int shortcutCSIDL : shortcutCSIDLs) {
+ // GetMatchingShortcut may fail when the exe path doesn't match, even
+ // if it refers to the same file. This should be rare, and the worst
+ // outcome would be failure to pin, so the risk is acceptable.
+ nsresult rv = GetMatchingShortcut(shortcutCSIDL, aAppUserModelId, exePath,
+ aShortcutSubstring, aShortcutPath);
+ if (NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FILE_NOT_FOUND;
+}
+
+static bool HasMatchingShortcutImpl(const nsAString& aAppUserModelId,
+ const bool aPrivateBrowsing,
+ const nsAutoString& aShortcutSubstring) {
+ // unused by us, but required
+ nsAutoString shortcutPath;
+ nsresult rv = FindMatchingShortcut(aAppUserModelId, aShortcutSubstring,
+ aPrivateBrowsing, shortcutPath);
+ if (SUCCEEDED(rv)) {
+ return true;
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP nsWindowsShellService::HasMatchingShortcut(
+ const nsAString& aAppUserModelId, const bool aPrivateBrowsing,
+ JSContext* aCx, dom::Promise** aPromise) {
+ if (!NS_IsMainThread()) {
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ auto promiseHolder = MakeRefPtr<nsMainThreadPtrHolder<dom::Promise>>(
+ "HasMatchingShortcut promise", promise);
+
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "HasMatchingShortcut",
+ [aAppUserModelId = nsString{aAppUserModelId}, aPrivateBrowsing,
+ promiseHolder = std::move(promiseHolder)] {
+ bool rv = false;
+ HRESULT hr = CoInitialize(nullptr);
+
+ if (SUCCEEDED(hr)) {
+ nsAutoString shortcutSubstring;
+ shortcutSubstring.AssignLiteral(MOZ_APP_DISPLAYNAME);
+ rv = HasMatchingShortcutImpl(aAppUserModelId, aPrivateBrowsing,
+ shortcutSubstring);
+ CoUninitialize();
+ }
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "HasMatchingShortcut callback",
+ [rv, promiseHolder = std::move(promiseHolder)] {
+ dom::Promise* promise = promiseHolder.get()->get();
+
+ promise->MaybeResolve(rv);
+ }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+static nsresult PinCurrentAppToTaskbarWin7(bool aCheckOnly,
+ nsAutoString aShortcutPath) {
+ nsModuleHandle shellInst(LoadLibraryW(L"shell32.dll"));
+
+ RefPtr<IShellWindows> shellWindows;
+ HRESULT hr =
+ ::CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_LOCAL_SERVER,
+ IID_IShellWindows, getter_AddRefs(shellWindows));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ // 1. Find the shell view for the desktop.
+ _variant_t loc(int(CSIDL_DESKTOP));
+ _variant_t empty;
+ long hwnd;
+ RefPtr<IDispatch> dispDesktop;
+ hr = shellWindows->FindWindowSW(&loc, &empty, SWC_DESKTOP, &hwnd,
+ SWFO_NEEDDISPATCH,
+ getter_AddRefs(dispDesktop));
+ if (FAILED(hr) || hr == S_FALSE) return NS_ERROR_FAILURE;
+
+ RefPtr<IServiceProvider> servProv;
+ hr = dispDesktop->QueryInterface(IID_IServiceProvider,
+ getter_AddRefs(servProv));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ RefPtr<IShellBrowser> browser;
+ hr = servProv->QueryService(SID_STopLevelBrowser, IID_IShellBrowser,
+ getter_AddRefs(browser));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ RefPtr<IShellView> activeShellView;
+ hr = browser->QueryActiveShellView(getter_AddRefs(activeShellView));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ // 2. Get the automation object for the desktop.
+ RefPtr<IDispatch> dispView;
+ hr = activeShellView->GetItemObject(SVGIO_BACKGROUND, IID_IDispatch,
+ getter_AddRefs(dispView));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ RefPtr<IShellFolderViewDual> folderView;
+ hr = dispView->QueryInterface(IID_IShellFolderViewDual,
+ getter_AddRefs(folderView));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ // 3. Get the interface to IShellDispatch
+ RefPtr<IDispatch> dispShell;
+ hr = folderView->get_Application(getter_AddRefs(dispShell));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ RefPtr<IShellDispatch2> shellDisp;
+ hr =
+ dispShell->QueryInterface(IID_IShellDispatch2, getter_AddRefs(shellDisp));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ wchar_t shortcutDir[MAX_PATH + 1];
+ wcscpy_s(shortcutDir, MAX_PATH + 1, aShortcutPath.get());
+ if (!PathRemoveFileSpecW(shortcutDir)) return NS_ERROR_FAILURE;
+
+ VARIANT dir;
+ dir.vt = VT_BSTR;
+ BStrPtr bstrShortcutDir = BStrPtr(SysAllocString(shortcutDir));
+ if (bstrShortcutDir.get() == NULL) return NS_ERROR_FAILURE;
+ dir.bstrVal = bstrShortcutDir.get();
+
+ RefPtr<Folder> folder;
+ hr = shellDisp->NameSpace(dir, getter_AddRefs(folder));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ wchar_t linkName[MAX_PATH + 1];
+ wcscpy_s(linkName, MAX_PATH + 1, aShortcutPath.get());
+ PathStripPathW(linkName);
+ BStrPtr bstrLinkName = BStrPtr(SysAllocString(linkName));
+ if (bstrLinkName.get() == NULL) return NS_ERROR_FAILURE;
+
+ RefPtr<FolderItem> folderItem;
+ hr = folder->ParseName(bstrLinkName.get(), getter_AddRefs(folderItem));
+ if (FAILED(hr) || !folderItem) return NS_ERROR_FAILURE;
+
+ RefPtr<FolderItemVerbs> verbs;
+ hr = folderItem->Verbs(getter_AddRefs(verbs));
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ long count;
+ hr = verbs->get_Count(&count);
+ if (FAILED(hr)) return NS_ERROR_FAILURE;
+
+ WCHAR verbName[100];
+ if (!LoadStringW(shellInst.get(), PIN_TO_TASKBAR_SHELL_VERB, verbName,
+ ARRAYSIZE(verbName))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ VARIANT v;
+ v.vt = VT_I4;
+ BStrPtr name;
+ for (long i = 0; i < count; ++i) {
+ RefPtr<FolderItemVerb> fiVerb;
+ v.lVal = i;
+ hr = verbs->Item(v, getter_AddRefs(fiVerb));
+ if (FAILED(hr)) {
+ continue;
+ }
+
+ BSTR tmpName;
+ hr = fiVerb->get_Name(&tmpName);
+ if (FAILED(hr)) {
+ continue;
+ }
+ name = BStrPtr(tmpName);
+ if (!wcscmp((WCHAR*)name.get(), verbName)) {
+ if (aCheckOnly) {
+ // we've done as much as we can without actually
+ // changing anything
+ return NS_OK;
+ }
+
+ hr = fiVerb->DoIt();
+ if (SUCCEEDED(hr)) {
+ return NS_OK;
+ }
+ }
+ }
+
+ // if we didn't return in the block above, something failed
+ return NS_ERROR_FAILURE;
+}
+
+static nsresult PinCurrentAppToTaskbarWin10(bool aCheckOnly,
+ nsAutoString aShortcutPath) {
+ // This enum is likely only used for Windows telemetry, INT_MAX is chosen to
+ // avoid confusion with existing uses.
+ enum PINNEDLISTMODIFYCALLER { PLMC_INT_MAX = INT_MAX };
+
+ // The types below, and the idea of using IPinnedList3::Modify,
+ // are thanks to Gee Law <https://geelaw.blog/entries/msedge-pins/>
+ static constexpr GUID CLSID_TaskbandPin = {
+ 0x90aa3a4e,
+ 0x1cba,
+ 0x4233,
+ {0xb8, 0xbb, 0x53, 0x57, 0x73, 0xd4, 0x84, 0x49}};
+
+ static constexpr GUID IID_IPinnedList3 = {
+ 0x0dd79ae2,
+ 0xd156,
+ 0x45d4,
+ {0x9e, 0xeb, 0x3b, 0x54, 0x97, 0x69, 0xe9, 0x40}};
+
+ struct IPinnedList3Vtbl;
+ struct IPinnedList3 {
+ IPinnedList3Vtbl* vtbl;
+ };
+
+ typedef ULONG STDMETHODCALLTYPE ReleaseFunc(IPinnedList3 * that);
+ typedef HRESULT STDMETHODCALLTYPE ModifyFunc(
+ IPinnedList3 * that, PCIDLIST_ABSOLUTE unpin, PCIDLIST_ABSOLUTE pin,
+ PINNEDLISTMODIFYCALLER caller);
+
+ struct IPinnedList3Vtbl {
+ void* QueryInterface; // 0
+ void* AddRef; // 1
+ ReleaseFunc* Release; // 2
+ void* Other[13]; // 3-15
+ ModifyFunc* Modify; // 16
+ };
+
+ struct ILFreeDeleter {
+ void operator()(LPITEMIDLIST aPtr) {
+ if (aPtr) {
+ ILFree(aPtr);
+ }
+ }
+ };
+
+ mozilla::UniquePtr<__unaligned ITEMIDLIST, ILFreeDeleter> path(
+ ILCreateFromPathW(aShortcutPath.get()));
+ if (NS_WARN_IF(!path)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ IPinnedList3* pinnedList = nullptr;
+ HRESULT hr =
+ CoCreateInstance(CLSID_TaskbandPin, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IPinnedList3, (void**)&pinnedList);
+ if (FAILED(hr) || !pinnedList) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (!aCheckOnly) {
+ hr =
+ pinnedList->vtbl->Modify(pinnedList, nullptr, path.get(), PLMC_INT_MAX);
+ }
+
+ pinnedList->vtbl->Release(pinnedList);
+
+ if (FAILED(hr)) {
+ return NS_ERROR_FAILURE;
+ } else {
+ return NS_OK;
+ }
+}
+
+static nsresult PinCurrentAppToTaskbarImpl(
+ bool aCheckOnly, bool aPrivateBrowsing, const nsAString& aAppUserModelId,
+ const nsAString& aShortcutName, const nsAString& aShortcutSubstring,
+ nsIFile* aShortcutsLogDir, nsIFile* aGreDir, nsIFile* aProgramsDir) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !NS_IsMainThread(),
+ "PinCurrentAppToTaskbarImpl should be called off main thread only");
+
+ nsAutoString shortcutPath;
+ nsresult rv = FindMatchingShortcut(aAppUserModelId, aShortcutSubstring,
+ aPrivateBrowsing, shortcutPath);
+ if (NS_FAILED(rv)) {
+ shortcutPath.Truncate();
+ }
+ if (shortcutPath.IsEmpty()) {
+ if (aCheckOnly) {
+ // Later checks rely on a shortcut already existing.
+ // We don't want to create a shortcut in check only mode
+ // so the best we can do is assume those parts will work.
+ return NS_OK;
+ }
+
+ nsAutoString linkName(aShortcutName);
+
+ nsCOMPtr<nsIFile> exeFile(aGreDir);
+ if (aPrivateBrowsing) {
+ nsAutoString pbExeStr(PRIVATE_BROWSING_BINARY);
+ nsresult rv = exeFile->Append(pbExeStr);
+ if (!NS_SUCCEEDED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ wchar_t exePath[MAXPATHLEN] = {};
+ if (NS_WARN_IF(NS_FAILED(BinaryPath::GetLong(exePath)))) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoString exeStr(exePath);
+ nsresult rv = NS_NewLocalFile(exeStr, true, getter_AddRefs(exeFile));
+ if (!NS_SUCCEEDED(rv)) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ }
+
+ nsCOMPtr<nsIFile> shortcutFile(aProgramsDir);
+ shortcutFile->Append(aShortcutName);
+ shortcutPath.Assign(shortcutFile->NativePath());
+
+ nsTArray<nsString> arguments;
+ rv = CreateShortcutImpl(exeFile, arguments, aShortcutName, exeFile,
+ // Icon indexes are defined as Resource IDs, but
+ // CreateShortcutImpl needs an index.
+ IDI_APPICON - 1, aAppUserModelId, FOLDERID_Programs,
+ linkName, shortcutFile->NativePath(),
+ aShortcutsLogDir);
+ if (!NS_SUCCEEDED(rv)) {
+ return NS_ERROR_FILE_NOT_FOUND;
+ }
+ }
+
+ if (IsWin10OrLater()) {
+ return PinCurrentAppToTaskbarWin10(aCheckOnly, shortcutPath);
+ } else {
+ return PinCurrentAppToTaskbarWin7(aCheckOnly, shortcutPath);
+ }
+}
+
+static nsresult PinCurrentAppToTaskbarAsyncImpl(bool aCheckOnly,
+ bool aPrivateBrowsing,
+ JSContext* aCx,
+ dom::Promise** aPromise) {
+ if (!NS_IsMainThread()) {
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ // First available on 1809
+ if (IsWin10OrLater() && !IsWin10Sep2018UpdateOrLater()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ nsAutoString aumid;
+ if (NS_WARN_IF(!mozilla::widget::WinTaskbar::GenerateAppUserModelID(
+ aumid, aPrivateBrowsing))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // NOTE: In the installer, non-private shortcuts are named
+ // "${BrandShortName}.lnk". This is set from MOZ_APP_DISPLAYNAME in
+ // defines.nsi.in. (Except in dev edition where it's explicitly set to
+ // "Firefox Developer Edition" in branding.nsi, which matches
+ // MOZ_APP_DISPLAYNAME in aurora/configure.sh.)
+ //
+ // If this changes, we could expand this to check shortcuts_log.ini,
+ // which records the name of the shortcuts as created by the installer.
+ //
+ // Private shortcuts are not created by the installer (they're created
+ // upon user request, ultimately by CreateShortcutImpl, and recorded in
+ // a separate shortcuts log. As with non-private shortcuts they have a known
+ // name - so there's no need to look through logs to find them.
+ nsAutoString shortcutName;
+ if (aPrivateBrowsing) {
+ nsTArray<nsCString> resIds = {
+ "branding/brand.ftl"_ns,
+ "browser/browser.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+ nsAutoCString pbStr;
+ IgnoredErrorResult rv;
+ l10n->FormatValueSync("private-browsing-shortcut-text-2"_ns, {}, pbStr, rv);
+ shortcutName.Append(NS_ConvertUTF8toUTF16(pbStr));
+ shortcutName.AppendLiteral(".lnk");
+ } else {
+ shortcutName.AppendLiteral(MOZ_APP_DISPLAYNAME ".lnk");
+ }
+
+ nsCOMPtr<nsIFile> greDir, updRoot, programsDir, shortcutsLogDir;
+ nsresult nsrv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir));
+ NS_ENSURE_SUCCESS(nsrv, nsrv);
+ nsrv = NS_GetSpecialDirectory(XRE_UPDATE_ROOT_DIR, getter_AddRefs(updRoot));
+ NS_ENSURE_SUCCESS(nsrv, nsrv);
+ rv = NS_GetSpecialDirectory(NS_WIN_PROGRAMS_DIR, getter_AddRefs(programsDir));
+ NS_ENSURE_SUCCESS(nsrv, nsrv);
+ nsrv = updRoot->GetParent(getter_AddRefs(shortcutsLogDir));
+ NS_ENSURE_SUCCESS(nsrv, nsrv);
+
+ auto promiseHolder = MakeRefPtr<nsMainThreadPtrHolder<dom::Promise>>(
+ "CheckPinCurrentAppToTaskbarAsync promise", promise);
+
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "CheckPinCurrentAppToTaskbarAsync",
+ [aCheckOnly, aPrivateBrowsing, shortcutName, aumid = nsString{aumid},
+ shortcutsLogDir, greDir, programsDir,
+ promiseHolder = std::move(promiseHolder)] {
+ nsresult rv = NS_ERROR_FAILURE;
+ HRESULT hr = CoInitialize(nullptr);
+
+ if (SUCCEEDED(hr)) {
+ nsAutoString shortcutSubstring;
+ shortcutSubstring.AssignLiteral(MOZ_APP_DISPLAYNAME);
+ rv = PinCurrentAppToTaskbarImpl(
+ aCheckOnly, aPrivateBrowsing, aumid, shortcutName,
+ shortcutSubstring, shortcutsLogDir.get(), greDir.get(),
+ programsDir.get());
+ CoUninitialize();
+ }
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "CheckPinCurrentAppToTaskbarAsync callback",
+ [rv, promiseHolder = std::move(promiseHolder)] {
+ dom::Promise* promise = promiseHolder.get()->get();
+
+ if (NS_SUCCEEDED(rv)) {
+ promise->MaybeResolveWithUndefined();
+ } else {
+ promise->MaybeReject(rv);
+ }
+ }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::PinCurrentAppToTaskbarAsync(bool aPrivateBrowsing,
+ JSContext* aCx,
+ dom::Promise** aPromise) {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1712628 tracks implementing
+ // this for MSIX packages.
+ if (widget::WinUtils::HasPackageIdentity()) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ return PinCurrentAppToTaskbarAsyncImpl(
+ /* aCheckOnly */ false, aPrivateBrowsing, aCx, aPromise);
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::CheckPinCurrentAppToTaskbarAsync(
+ bool aPrivateBrowsing, JSContext* aCx, dom::Promise** aPromise) {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1712628 tracks implementing
+ // this for MSIX packages.
+ if (widget::WinUtils::HasPackageIdentity()) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ return PinCurrentAppToTaskbarAsyncImpl(
+ /* aCheckOnly = */ true, aPrivateBrowsing, aCx, aPromise);
+}
+
+static bool IsCurrentAppPinnedToTaskbarSync(const nsAutoString& aumid) {
+ // There are two shortcut targets that we created. One always matches the
+ // binary we're running as (eg: firefox.exe). The other is the wrapper
+ // for launching in Private Browsing mode. We need to inspect shortcuts
+ // that point at either of these to accurately judge whether or not
+ // the app is pinned with the given AUMID.
+ wchar_t exePath[MAXPATHLEN] = {};
+ wchar_t pbExePath[MAXPATHLEN] = {};
+
+ if (NS_WARN_IF(NS_FAILED(BinaryPath::GetLong(exePath)))) {
+ return false;
+ }
+
+ wcscpy_s(pbExePath, MAXPATHLEN, exePath);
+ if (!PathRemoveFileSpecW(pbExePath)) {
+ return false;
+ }
+ if (!PathAppendW(pbExePath, L"private_browsing.exe")) {
+ return false;
+ }
+
+ wchar_t folderChars[MAX_PATH] = {};
+ HRESULT hr = SHGetFolderPathW(nullptr, CSIDL_APPDATA, nullptr,
+ SHGFP_TYPE_CURRENT, folderChars);
+ if (NS_WARN_IF(FAILED(hr))) {
+ return false;
+ }
+
+ nsAutoString folder;
+ folder.Assign(folderChars);
+ if (NS_WARN_IF(folder.IsEmpty())) {
+ return false;
+ }
+ if (folder[folder.Length() - 1] != '\\') {
+ folder.AppendLiteral("\\");
+ }
+ folder.AppendLiteral(
+ "Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar");
+ nsAutoString pattern;
+ pattern.Assign(folder);
+ pattern.AppendLiteral("\\*.lnk");
+
+ WIN32_FIND_DATAW findData = {};
+ HANDLE hFindFile = FindFirstFileW(pattern.get(), &findData);
+ if (hFindFile == INVALID_HANDLE_VALUE) {
+ Unused << NS_WARN_IF(GetLastError() != ERROR_FILE_NOT_FOUND);
+ return false;
+ }
+ // Past this point we don't return until the end of the function,
+ // when FindClose() is called.
+
+ // Check all shortcuts until a match is found
+ bool isPinned = false;
+ do {
+ nsAutoString fileName;
+ fileName.Assign(folder);
+ fileName.AppendLiteral("\\");
+ fileName.Append(findData.cFileName);
+
+ // Create a shell link object for loading the shortcut
+ RefPtr<IShellLinkW> link;
+ HRESULT hr =
+ CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
+ IID_IShellLinkW, getter_AddRefs(link));
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ // Load
+ RefPtr<IPersistFile> persist;
+ hr = link->QueryInterface(IID_IPersistFile, getter_AddRefs(persist));
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ hr = persist->Load(fileName.get(), STGM_READ);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ // Check the exe path
+ static_assert(MAXPATHLEN == MAX_PATH);
+ wchar_t storedExePath[MAX_PATH] = {};
+ // With no flags GetPath gets a long path
+ hr = link->GetPath(storedExePath, ArrayLength(storedExePath), nullptr, 0);
+ if (FAILED(hr) || hr == S_FALSE) {
+ continue;
+ }
+ // Case insensitive path comparison
+ // NOTE: Because this compares the path directly, it is possible to
+ // have a false negative mismatch.
+ if (wcsnicmp(storedExePath, exePath, MAXPATHLEN) == 0 ||
+ wcsnicmp(storedExePath, pbExePath, MAXPATHLEN) == 0) {
+ RefPtr<IPropertyStore> propStore;
+ hr = link->QueryInterface(IID_IPropertyStore, getter_AddRefs(propStore));
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ PROPVARIANT pv;
+ hr = propStore->GetValue(PKEY_AppUserModel_ID, &pv);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ wchar_t storedAUMID[MAX_PATH];
+ hr = PropVariantToString(pv, storedAUMID, MAX_PATH);
+ PropVariantClear(&pv);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ if (aumid.Equals(storedAUMID)) {
+ isPinned = true;
+ break;
+ }
+ }
+ } while (FindNextFileW(hFindFile, &findData));
+
+ FindClose(hFindFile);
+
+ return isPinned;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::IsCurrentAppPinnedToTaskbarAsync(
+ const nsAString& aumid, JSContext* aCx, /* out */ dom::Promise** aPromise) {
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1712628 tracks implementing
+ // this for MSIX packages.
+ if (widget::WinUtils::HasPackageIdentity()) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ if (!NS_IsMainThread()) {
+ return NS_ERROR_NOT_SAME_THREAD;
+ }
+
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ if (MOZ_UNLIKELY(rv.Failed())) {
+ return rv.StealNSResult();
+ }
+
+ // A holder to pass the promise through the background task and back to
+ // the main thread when finished.
+ auto promiseHolder = MakeRefPtr<nsMainThreadPtrHolder<dom::Promise>>(
+ "IsCurrentAppPinnedToTaskbarAsync promise", promise);
+
+ // nsAString can't be captured by a lambda because it does not have a
+ // public copy constructor
+ nsAutoString capturedAumid(aumid);
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "IsCurrentAppPinnedToTaskbarAsync",
+ [capturedAumid, promiseHolder = std::move(promiseHolder)] {
+ bool isPinned = false;
+
+ HRESULT hr = CoInitialize(nullptr);
+ if (SUCCEEDED(hr)) {
+ isPinned = IsCurrentAppPinnedToTaskbarSync(capturedAumid);
+ CoUninitialize();
+ }
+
+ // Dispatch back to the main thread to resolve the promise.
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "IsCurrentAppPinnedToTaskbarAsync callback",
+ [isPinned, promiseHolder = std::move(promiseHolder)] {
+ promiseHolder.get()->get()->MaybeResolve(isPinned);
+ }));
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsWindowsShellService::ClassifyShortcut(const nsAString& aPath,
+ nsAString& aResult) {
+ aResult.Truncate();
+
+ nsAutoString shortcutPath(PromiseFlatString(aPath));
+
+ // NOTE: On Windows 7, Start Menu pin shortcuts are stored under
+ // "<FOLDERID_User Pinned>\StartMenu", but on Windows 10 they are just normal
+ // Start Menu shortcuts. These both map to "StartMenu" for consistency,
+ // rather than having a separate "StartMenuPins" which would only apply on
+ // Win7.
+ struct {
+ KNOWNFOLDERID folderId;
+ const char16_t* postfix;
+ const char16_t* classification;
+ } folders[] = {{FOLDERID_CommonStartMenu, u"\\", u"StartMenu"},
+ {FOLDERID_StartMenu, u"\\", u"StartMenu"},
+ {FOLDERID_PublicDesktop, u"\\", u"Desktop"},
+ {FOLDERID_Desktop, u"\\", u"Desktop"},
+ {FOLDERID_UserPinned, u"\\TaskBar\\", u"Taskbar"},
+ {FOLDERID_UserPinned, u"\\StartMenu\\", u"StartMenu"}};
+
+ for (size_t i = 0; i < ArrayLength(folders); ++i) {
+ nsAutoString knownPath;
+
+ // These flags are chosen to avoid I/O, see bug 1363398.
+ DWORD flags =
+ KF_FLAG_SIMPLE_IDLIST | KF_FLAG_DONT_VERIFY | KF_FLAG_NO_ALIAS;
+ PWSTR rawPath = nullptr;
+
+ if (FAILED(SHGetKnownFolderPath(folders[i].folderId, flags, nullptr,
+ &rawPath))) {
+ continue;
+ }
+
+ knownPath = nsDependentString(rawPath);
+ CoTaskMemFree(rawPath);
+
+ knownPath.Append(folders[i].postfix);
+ // Check if the shortcut path starts with the shell folder path.
+ if (wcsnicmp(shortcutPath.get(), knownPath.get(), knownPath.Length()) ==
+ 0) {
+ aResult.Assign(folders[i].classification);
+ nsTArray<nsCString> resIds = {
+ "branding/brand.ftl"_ns,
+ "browser/browser.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+ nsAutoCString pbStr;
+ IgnoredErrorResult rv;
+ l10n->FormatValueSync("private-browsing-shortcut-text-2"_ns, {}, pbStr,
+ rv);
+ NS_ConvertUTF8toUTF16 widePbStr(pbStr);
+ if (wcsstr(shortcutPath.get(), widePbStr.get())) {
+ aResult.AppendLiteral("Private");
+ }
+ return NS_OK;
+ }
+ }
+
+ // Nothing found, aResult is already "".
+ return NS_OK;
+}
+
+nsWindowsShellService::nsWindowsShellService() {}
+
+nsWindowsShellService::~nsWindowsShellService() {}
diff --git a/browser/components/shell/nsWindowsShellService.h b/browser/components/shell/nsWindowsShellService.h
new file mode 100644
index 0000000000..6127f687bd
--- /dev/null
+++ b/browser/components/shell/nsWindowsShellService.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef nswindowsshellservice_h____
+#define nswindowsshellservice_h____
+
+#include "nscore.h"
+#include "nsString.h"
+#include "nsToolkitShellService.h"
+#include "nsIShellService.h"
+#include "nsIWindowsShellService.h"
+
+#include <windows.h>
+#include <ole2.h>
+
+class nsWindowsShellService : public nsIShellService,
+ public nsToolkitShellService,
+ public nsIWindowsShellService {
+ virtual ~nsWindowsShellService();
+
+ public:
+ nsWindowsShellService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+ NS_DECL_NSIWINDOWSSHELLSERVICE
+
+ protected:
+ nsresult LaunchControlPanelDefaultsSelectionUI();
+ nsresult LaunchControlPanelDefaultPrograms();
+ nsresult LaunchModernSettingsDialogDefaultApps();
+ nsresult InvokeHTTPOpenAsVerb();
+ nsresult LaunchHTTPHandlerPane();
+};
+
+#endif // nswindowsshellservice_h____
diff --git a/browser/components/shell/search-provider-files/README b/browser/components/shell/search-provider-files/README
new file mode 100644
index 0000000000..81ea16d9b6
--- /dev/null
+++ b/browser/components/shell/search-provider-files/README
@@ -0,0 +1,20 @@
+In order to get gnome shell search provider registered and active
+you need to install the firefox-search-provider.ini and firefox.desktop files system wide.
+
+The locations may be distro specific, for instance Fedora and Ubuntu expect
+the files at:
+
+/usr/share/gnome-shell/search-providers/firefox-search-provider.ini
+/usr/share/applications/firefox.desktop
+
+firefox.desktop is a system-wide Firefox launcher. It may come with your
+distribution or you can use this one. Update name of firefox desktop file at firefox-search-provider.ini
+according your actual file at /usr/share/applications.
+
+firefox-search-provider.ini registers Firefox as a search provider.
+When the file is correctly installed you can see Firefox as a searchable application
+at Settings -> Search at Gnome controll center.
+
+Gnome shell search provider is active only when Firefox is running. When it's active
+you can see it as org.mozilla.Firefox.SearchProvider D-Bus service.
+
diff --git a/browser/components/shell/search-provider-files/firefox-search-provider.ini b/browser/components/shell/search-provider-files/firefox-search-provider.ini
new file mode 100644
index 0000000000..3868e3d528
--- /dev/null
+++ b/browser/components/shell/search-provider-files/firefox-search-provider.ini
@@ -0,0 +1,5 @@
+[Shell Search Provider]
+DesktopId=firefox.desktop
+BusName=org.mozilla.Firefox.SearchProvider
+ObjectPath=/org/mozilla/Firefox/SearchProvider
+Version=2
diff --git a/browser/components/shell/search-provider-files/firefox.desktop b/browser/components/shell/search-provider-files/firefox.desktop
new file mode 100644
index 0000000000..118aae6b73
--- /dev/null
+++ b/browser/components/shell/search-provider-files/firefox.desktop
@@ -0,0 +1,273 @@
+[Desktop Entry]
+Version=1.0
+Name=Firefox
+GenericName=Web Browser
+GenericName[ca]=Navegador web
+GenericName[cs]=Webový prohlížeč
+GenericName[es]=Navegador web
+GenericName[fa]=مرورگر اینترنتی
+GenericName[fi]=WWW-selain
+GenericName[fr]=Navigateur Web
+GenericName[hu]=Webböngésző
+GenericName[it]=Browser Web
+GenericName[ja]=ウェブ・ブラウザ
+GenericName[ko]=웹 브라우저
+GenericName[nb]=Nettleser
+GenericName[nl]=Webbrowser
+GenericName[nn]=Nettlesar
+GenericName[no]=Nettleser
+GenericName[pl]=Przeglądarka WWW
+GenericName[pt]=Navegador Web
+GenericName[pt_BR]=Navegador Web
+GenericName[sk]=Internetový prehliadač
+GenericName[sv]=Webbläsare
+Comment=Browse the Web
+Comment[ca]=Navegueu per el web
+Comment[cs]=Prohlížení stránek World Wide Webu
+Comment[de]=Im Internet surfen
+Comment[es]=Navegue por la web
+Comment[fa]=صفحات شبکه جهانی اینترنت را مرور نمایید
+Comment[fi]=Selaa Internetin WWW-sivuja
+Comment[fr]=Navigue sur Internet
+Comment[hu]=A világháló böngészése
+Comment[it]=Esplora il web
+Comment[ja]=ウェブを閲覧します
+Comment[ko]=웹을 돌아 다닙니다
+Comment[nb]=Surf på nettet
+Comment[nl]=Verken het internet
+Comment[nn]=Surf på nettet
+Comment[no]=Surf på nettet
+Comment[pl]=Przeglądanie stron WWW
+Comment[pt]=Navegue na Internet
+Comment[pt_BR]=Navegue na Internet
+Comment[sk]=Prehliadanie internetu
+Comment[sv]=Surfa på webben
+Exec=firefox %u
+Icon=firefox
+Terminal=false
+Type=Application
+MimeType=text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https;
+StartupNotify=true
+Categories=Network;WebBrowser;
+Keywords=web;browser;internet;
+Actions=new-window;new-private-window;
+
+X-Desktop-File-Install-Version=0.24
+
+[Desktop Action new-window]
+Name=Open a New Window
+Name[ach]=Dirica manyen
+Name[af]=Nuwe venster
+Name[an]=Nueva finestra
+Name[ar]=نافذة جديدة
+Name[as]=নতুন উইন্ডো
+Name[ast]=Ventana nueva
+Name[az]=Yeni Pəncərə
+Name[be]=Новае акно
+Name[bg]=Нов прозорец
+Name[bn_BD]=নতুন উইন্ডো (N)
+Name[bn_IN]=নতুন উইন্ডো
+Name[br]=Prenestr nevez
+Name[brx]=गोदान उइन्ड'(N)
+Name[bs]=Novi prozor
+Name[ca]=Finestra nova
+Name[cak]=K'ak'a' tzuwäch
+Name[cs]=Nové okno
+Name[cy]=Ffenestr Newydd
+Name[da]=Nyt vindue
+Name[de]=Neues Fenster
+Name[dsb]=Nowe wokno
+Name[el]=Νέο παράθυρο
+Name[en_GB]=New Window
+Name[en_US]=New Window
+Name[en_ZA]=New Window
+Name[eo]=Nova fenestro
+Name[es_AR]=Nueva ventana
+Name[es_CL]=Nueva ventana
+Name[es_ES]=Nueva ventana
+Name[es_MX]=Nueva ventana
+Name[et]=Uus aken
+Name[eu]=Leiho berria
+Name[fa]=پنجره جدید‌
+Name[ff]=Henorde Hesere
+Name[fi]=Uusi ikkuna
+Name[fr]=Nouvelle fenêtre
+Name[fy_NL]=Nij finster
+Name[ga_IE]=Fuinneog Nua
+Name[gd]=Uinneag ùr
+Name[gl]=Nova xanela
+Name[gn]=Ovetã pyahu
+Name[gu_IN]=નવી વિન્ડો
+Name[he]=חלון חדש
+Name[hi_IN]=नया विंडो
+Name[hr]=Novi prozor
+Name[hsb]=Nowe wokno
+Name[hu]=Új ablak
+Name[hy_AM]=Նոր Պատուհան
+Name[id]=Jendela Baru
+Name[is]=Nýr gluggi
+Name[it]=Nuova finestra
+Name[ja]=新しいウィンドウ
+Name[ja_JP-mac]=新規ウインドウ
+Name[ka]=ახალი ფანჯარა
+Name[kk]=Жаңа терезе
+Name[km]=បង្អួច​​​ថ្មី
+Name[kn]=ಹೊಸ ಕಿಟಕಿ
+Name[ko]=새 창
+Name[kok]=नवें जनेल
+Name[ks]=نئئ وِنڈو
+Name[lij]=Neuvo barcon
+Name[lo]=ຫນ້າຕ່າງໃຫມ່
+Name[lt]=Naujas langas
+Name[ltg]=Jauns lūgs
+Name[lv]=Jauns logs
+Name[mai]=नव विंडो
+Name[mk]=Нов прозорец
+Name[ml]=പുതിയ ജാലകം
+Name[mr]=नवीन पटल
+Name[ms]=Tetingkap Baru
+Name[my]=ဝင်းဒိုးအသစ်
+Name[nb_NO]=Nytt vindu
+Name[ne_NP]=नयाँ सञ्झ्याल
+Name[nl]=Nieuw venster
+Name[nn_NO]=Nytt vindauge
+Name[or]=ନୂତନ ୱିଣ୍ଡୋ
+Name[pa_IN]=ਨਵੀਂ ਵਿੰਡੋ
+Name[pl]=Nowe okno
+Name[pt_BR]=Nova janela
+Name[pt_PT]=Nova janela
+Name[rm]=Nova fanestra
+Name[ro]=Fereastră nouă
+Name[ru]=Новое окно
+Name[sat]=नावा विंडो (N)
+Name[si]=නව කවුළුවක්
+Name[sk]=Nové okno
+Name[sl]=Novo okno
+Name[son]=Zanfun taaga
+Name[sq]=Dritare e Re
+Name[sr]=Нови прозор
+Name[sv_SE]=Nytt fönster
+Name[ta]=புதிய சாளரம்
+Name[te]=కొత్త విండో
+Name[th]=หน้าต่างใหม่
+Name[tr]=Yeni pencere
+Name[tsz]=Eraatarakua jimpani
+Name[uk]=Нове вікно
+Name[ur]=نیا دریچہ
+Name[uz]=Yangi oyna
+Name[vi]=Cửa sổ mới
+Name[wo]=Palanteer bu bees
+Name[xh]=Ifestile entsha
+Name[zh_CN]=新建窗口
+Name[zh_TW]=開新視窗
+
+
+Exec=firefox --new-window %u
+
+[Desktop Action new-private-window]
+Name=Open a New Private Window
+Name[ach]=Dirica manyen me mung
+Name[af]=Nuwe privaatvenster
+Name[an]=Nueva finestra privada
+Name[ar]=نافذة خاصة جديدة
+Name[as]=নতুন ব্যক্তিগত উইন্ডো
+Name[ast]=Ventana privada nueva
+Name[az]=Yeni Məxfi Pəncərə
+Name[be]=Новае акно адасаблення
+Name[bg]=Нов прозорец за поверително сърфиране
+Name[bn_BD]=নতুন ব্যক্তিগত উইন্ডো
+Name[bn_IN]=নতুন ব্যক্তিগত উইন্ডো
+Name[br]=Prenestr merdeiñ prevez nevez
+Name[brx]=गोदान प्राइभेट उइन्ड'
+Name[bs]=Novi privatni prozor
+Name[ca]=Finestra privada nova
+Name[cak]=K'ak'a' ichinan tzuwäch
+Name[cs]=Nové anonymní okno
+Name[cy]=Ffenestr Breifat Newydd
+Name[da]=Nyt privat vindue
+Name[de]=Neues privates Fenster
+Name[dsb]=Nowe priwatne wokno
+Name[el]=Νέο παράθυρο ιδιωτικής περιήγησης
+Name[en_GB]=New Private Window
+Name[en_US]=New Private Window
+Name[en_ZA]=New Private Window
+Name[eo]=Nova privata fenestro
+Name[es_AR]=Nueva ventana privada
+Name[es_CL]=Nueva ventana privada
+Name[es_ES]=Nueva ventana privada
+Name[es_MX]=Nueva ventana privada
+Name[et]=Uus privaatne aken
+Name[eu]=Leiho pribatu berria
+Name[fa]=پنجره ناشناس جدید
+Name[ff]=Henorde Suturo Hesere
+Name[fi]=Uusi yksityinen ikkuna
+Name[fr]=Nouvelle fenêtre de navigation privée
+Name[fy_NL]=Nij priveefinster
+Name[ga_IE]=Fuinneog Nua Phríobháideach
+Name[gd]=Uinneag phrìobhaideach ùr
+Name[gl]=Nova xanela privada
+Name[gn]=Ovetã ñemi pyahu
+Name[gu_IN]=નવી ખાનગી વિન્ડો
+Name[he]=חלון פרטי חדש
+Name[hi_IN]=नयी निजी विंडो
+Name[hr]=Novi privatni prozor
+Name[hsb]=Nowe priwatne wokno
+Name[hu]=Új privát ablak
+Name[hy_AM]=Սկսել Գաղտնի դիտարկում
+Name[id]=Jendela Mode Pribadi Baru
+Name[is]=Nýr huliðsgluggi
+Name[it]=Nuova finestra anonima
+Name[ja]=新しいプライベートウィンドウ
+Name[ja_JP-mac]=新規プライベートウインドウ
+Name[ka]=ახალი პირადი ფანჯარა
+Name[kk]=Жаңа жекелік терезе
+Name[km]=បង្អួច​ឯកជន​ថ្មី
+Name[kn]=ಹೊಸ ಖಾಸಗಿ ಕಿಟಕಿ
+Name[ko]=새 사생활 보호 모드
+Name[kok]=नवो खाजगी विंडो
+Name[ks]=نْو پرایوٹ وینڈو&amp;
+Name[lij]=Neuvo barcon privou
+Name[lo]=ເປີດຫນ້າຕ່າງສວນຕົວຂື້ນມາໃຫມ່
+Name[lt]=Naujas privataus naršymo langas
+Name[ltg]=Jauns privatais lūgs
+Name[lv]=Jauns privātais logs
+Name[mai]=नया निज विंडो (W)
+Name[mk]=Нов приватен прозорец
+Name[ml]=പുതിയ സ്വകാര്യ ജാലകം
+Name[mr]=नवीन वैयक्तिक पटल
+Name[ms]=Tetingkap Persendirian Baharu
+Name[my]=New Private Window
+Name[nb_NO]=Nytt privat vindu
+Name[ne_NP]=नयाँ निजी सञ्झ्याल
+Name[nl]=Nieuw privévenster
+Name[nn_NO]=Nytt privat vindauge
+Name[or]=ନୂତନ ବ୍ୟକ୍ତିଗତ ୱିଣ୍ଡୋ
+Name[pa_IN]=ਨਵੀਂ ਪ੍ਰਾਈਵੇਟ ਵਿੰਡੋ
+Name[pl]=Nowe okno prywatne
+Name[pt_BR]=Nova janela privativa
+Name[pt_PT]=Nova janela privada
+Name[rm]=Nova fanestra privata
+Name[ro]=Fereastră privată nouă
+Name[ru]=Новое приватное окно
+Name[sat]=नावा निजेराक् विंडो (W )
+Name[si]=නව පුද්ගලික කවුළුව (W)
+Name[sk]=Nové okno v režime Súkromné prehliadanie
+Name[sl]=Novo zasebno okno
+Name[son]=Sutura zanfun taaga
+Name[sq]=Dritare e Re Private
+Name[sr]=Нови приватан прозор
+Name[sv_SE]=Nytt privat fönster
+Name[ta]=புதிய தனிப்பட்ட சாளரம்
+Name[te]=కొత్త ఆంతరంగిక విండో
+Name[th]=หน้าต่างส่วนตัวใหม่
+Name[tr]=Yeni gizli pencere
+Name[tsz]=Juchiiti eraatarakua jimpani
+Name[uk]=Приватне вікно
+Name[ur]=نیا نجی دریچہ
+Name[uz]=Yangi maxfiy oyna
+Name[vi]=Cửa sổ riêng tư mới
+Name[wo]=Panlanteeru biir bu bees
+Name[xh]=Ifestile yangasese entsha
+Name[zh_CN]=新建隐私浏览窗口
+Name[zh_TW]=新增隱私視窗
+Exec=firefox --private-window %u
diff --git a/browser/components/shell/test/browser.ini b/browser/components/shell/test/browser.ini
new file mode 100644
index 0000000000..ab69b0835e
--- /dev/null
+++ b/browser/components/shell/test/browser.ini
@@ -0,0 +1,69 @@
+[DEFAULT]
+
+[browser_1119088.js]
+support-files =
+ mac_desktop_image.py
+skip-if =
+ os != "mac"
+ verify
+[browser_420786.js]
+skip-if = os != "linux"
+[browser_633221.js]
+skip-if = os != "linux"
+[browser_doesAppNeedPin.js]
+[browser_headless_screenshot_1.js]
+support-files =
+ head.js
+ headless.html
+skip-if =
+ os == 'win'
+ ccov
+ tsan #Bug 1429950 , Bug 1583315, Bug 1696109, Bug 1701449
+[browser_headless_screenshot_2.js]
+support-files =
+ head.js
+ headless.html
+skip-if =
+ os == 'win'
+ ccov
+ tsan #Bug 1429950 , Bug 1583315, Bug 1696109, Bug 1701449
+[browser_headless_screenshot_3.js]
+support-files =
+ head.js
+ headless.html
+skip-if =
+ os == 'win'
+ ccov
+ tsan #Bug 1429950 , Bug 1583315, Bug 1696109, Bug 1701449
+[browser_headless_screenshot_4.js]
+support-files =
+ head.js
+ headless.html
+skip-if =
+ os == 'win'
+ ccov
+ tsan #Bug 1429950 , Bug 1583315, Bug 1696109, Bug 1701449
+[browser_headless_screenshot_cross_origin.js]
+support-files =
+ head.js
+ headless_cross_origin.html
+ headless_iframe.html
+skip-if =
+ os == 'win'
+ ccov
+ tsan #Bug 1429950 , Bug 1583315, Bug 1696109, Bug 1701449
+[browser_headless_screenshot_redirect.js]
+support-files =
+ head.js
+ headless.html
+ headless_redirect.html
+ headless_redirect.html^headers^
+skip-if =
+ os == 'win'
+ ccov
+ tsan #Bug 1429950 , Bug 1583315, Bug 1696109, Bug 1701449
+[browser_setDefaultBrowser.js]
+[browser_setDefaultPDFHandler.js]
+run-if = os == "win" && os_version != "6.1"
+reason = Test is Windows 10+.
+[browser_setDesktopBackgroundPreview.js]
diff --git a/browser/components/shell/test/browser_1119088.js b/browser/components/shell/test/browser_1119088.js
new file mode 100644
index 0000000000..bc0995fe51
--- /dev/null
+++ b/browser/components/shell/test/browser_1119088.js
@@ -0,0 +1,167 @@
+// Where we save the desktop background to (~/Pictures).
+const NS_OSX_PICTURE_DOCUMENTS_DIR = "Pct";
+
+// Paths used to run the CLI command (python script) that is used to
+// 1) check the desktop background image matches what we set it to via
+// nsIShellService::setDesktopBackground() and
+// 2) revert the desktop background image to the OS default
+const kPythonPath = "/usr/bin/python";
+const kDesktopCheckerScriptPath =
+ "browser/browser/components/shell/test/mac_desktop_image.py";
+const kDefaultBackgroundImage_10_14 =
+ "/Library/Desktop Pictures/Solid Colors/Teal.png";
+const kDefaultBackgroundImage_10_15 =
+ "/System/Library/Desktop Pictures/Solid Colors/Teal.png";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+function getPythonExecutableFile() {
+ let python = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ python.initWithPath(kPythonPath);
+ return python;
+}
+
+function createProcess() {
+ return Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+}
+
+// Use a CLI command to set the desktop background to |imagePath|. Returns the
+// exit code of the CLI command which reflects whether or not the background
+// image was successfully set. Returns 0 on success.
+function setDesktopBackgroundCLI(imagePath) {
+ let setBackgroundProcess = createProcess();
+ setBackgroundProcess.init(getPythonExecutableFile());
+ let args = [
+ kDesktopCheckerScriptPath,
+ "--verbose",
+ "--set-background-image",
+ imagePath,
+ ];
+ setBackgroundProcess.run(true, args, args.length);
+ return setBackgroundProcess.exitValue;
+}
+
+// Check the desktop background is |imagePath| using a CLI command.
+// Returns the exit code of the CLI command which reflects whether or not
+// the provided image path matches the path of the current desktop background
+// image. A return value of 0 indicates success/match.
+function checkDesktopBackgroundCLI(imagePath) {
+ let checkBackgroundProcess = createProcess();
+ checkBackgroundProcess.init(getPythonExecutableFile());
+ let args = [
+ kDesktopCheckerScriptPath,
+ "--verbose",
+ "--check-background-image",
+ imagePath,
+ ];
+ checkBackgroundProcess.run(true, args, args.length);
+ return checkBackgroundProcess.exitValue;
+}
+
+// Use the python script to set/check the desktop background is |imagePath|
+function setAndCheckDesktopBackgroundCLI(imagePath) {
+ Assert.ok(FileUtils.File(imagePath).exists(), `${imagePath} exists`);
+
+ let setExitCode = setDesktopBackgroundCLI(imagePath);
+ Assert.ok(setExitCode == 0, `Setting background via CLI to ${imagePath}`);
+
+ let checkExitCode = checkDesktopBackgroundCLI(imagePath);
+ Assert.ok(checkExitCode == 0, `Checking background via CLI is ${imagePath}`);
+}
+
+// Restore the automation default background image. i.e., the default used
+// in the automated test environment, not the OS default.
+function restoreDefaultBackground() {
+ let defaultBackgroundPath;
+ if (AppConstants.isPlatformAndVersionAtLeast("macosx", 19)) {
+ defaultBackgroundPath = kDefaultBackgroundImage_10_15;
+ } else {
+ defaultBackgroundPath = kDefaultBackgroundImage_10_14;
+ }
+ setAndCheckDesktopBackgroundCLI(defaultBackgroundPath);
+}
+
+/**
+ * Tests "Set As Desktop Background" platform implementation on macOS.
+ *
+ * Sets the desktop background image to the browser logo from the about:logo
+ * page and verifies it was set successfully. Setting the desktop background
+ * (which uses the nsIShellService::setDesktopBackground() interface method)
+ * downloads the image to ~/Pictures using a unique file name and sets the
+ * desktop background to the downloaded file leaving the download in place.
+ * After setDesktopBackground() is called, the test uses a python script to
+ * validate that the current desktop background is in fact set to the
+ * downloaded logo.
+ */
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logo",
+ },
+ async browser => {
+ let dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ Ci.nsIDirectoryServiceProvider
+ );
+ let uuidGenerator = Services.uuid;
+ let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIShellService
+ );
+
+ // Ensure we are starting with the default background. Log a
+ // failure if we can not set the background to the default, but
+ // ignore the case where the background is not already set as that
+ // that may be due to a previous test failure.
+ restoreDefaultBackground();
+
+ // Generate a UUID (with non-alphanumberic characters removed) to build
+ // up a filename for the desktop background. Use a UUID to distinguish
+ // between runs so we won't be confused by images that were not properly
+ // cleaned up after previous runs.
+ let uuid = uuidGenerator.generateUUID().toString().replace(/\W/g, "");
+
+ // Set the background image path to be $HOME/Pictures/<UUID>.png.
+ // nsIShellService.setDesktopBackground() downloads the image to this
+ // path and then sets it as the desktop background image, leaving the
+ // image in place.
+ let backgroundImage = dirSvc.getFile(NS_OSX_PICTURE_DOCUMENTS_DIR, {});
+ backgroundImage.append(uuid + ".png");
+ if (backgroundImage.exists()) {
+ backgroundImage.remove(false);
+ }
+
+ // For simplicity, we're going to reach in and access the image on the
+ // page directly, which means the page shouldn't be running in a remote
+ // browser. Thankfully, about:logo runs in the parent process for now.
+ Assert.ok(
+ !gBrowser.selectedBrowser.isRemoteBrowser,
+ "image can be accessed synchronously from the parent process"
+ );
+ let image = gBrowser.selectedBrowser.contentDocument.images[0];
+
+ info(`Setting/saving desktop background to ${backgroundImage.path}`);
+
+ // Saves the file in ~/Pictures
+ shellSvc.setDesktopBackground(image, 0, backgroundImage.leafName);
+
+ await BrowserTestUtils.waitForCondition(() => backgroundImage.exists());
+ info(`${backgroundImage.path} downloaded`);
+ Assert.ok(
+ FileUtils.File(backgroundImage.path).exists(),
+ `${backgroundImage.path} exists`
+ );
+
+ // Check that the desktop background image is the image we set above.
+ let exitCode = checkDesktopBackgroundCLI(backgroundImage.path);
+ Assert.ok(exitCode == 0, `background should be ${backgroundImage.path}`);
+
+ // Restore the background image to the Mac default.
+ restoreDefaultBackground();
+
+ // We no longer need the downloaded image.
+ backgroundImage.remove(false);
+ }
+ );
+});
diff --git a/browser/components/shell/test/browser_420786.js b/browser/components/shell/test/browser_420786.js
new file mode 100644
index 0000000000..025cd87943
--- /dev/null
+++ b/browser/components/shell/test/browser_420786.js
@@ -0,0 +1,105 @@
+const DG_BACKGROUND = "/desktop/gnome/background";
+const DG_IMAGE_KEY = DG_BACKGROUND + "/picture_filename";
+const DG_OPTION_KEY = DG_BACKGROUND + "/picture_options";
+const DG_DRAW_BG_KEY = DG_BACKGROUND + "/draw_background";
+
+const GS_BG_SCHEMA = "org.gnome.desktop.background";
+const GS_IMAGE_KEY = "picture-uri";
+const GS_OPTION_KEY = "picture-options";
+const GS_DRAW_BG_KEY = "draw-background";
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logo",
+ },
+ browser => {
+ var brandName = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+ var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ Ci.nsIDirectoryServiceProvider
+ );
+ var homeDir = dirSvc.getFile("Home", {});
+
+ var wpFile = homeDir.clone();
+ wpFile.append(brandName + "_wallpaper.png");
+
+ // Backup the existing wallpaper so that this test doesn't change the user's
+ // settings.
+ var wpFileBackup = homeDir.clone();
+ wpFileBackup.append(brandName + "_wallpaper.png.backup");
+
+ if (wpFileBackup.exists()) {
+ wpFileBackup.remove(false);
+ }
+
+ if (wpFile.exists()) {
+ wpFile.copyTo(null, wpFileBackup.leafName);
+ }
+
+ var shell = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIShellService
+ );
+
+ // For simplicity, we're going to reach in and access the image on the
+ // page directly, which means the page shouldn't be running in a remote
+ // browser. Thankfully, about:logo runs in the parent process for now.
+ Assert.ok(
+ !gBrowser.selectedBrowser.isRemoteBrowser,
+ "image can be accessed synchronously from the parent process"
+ );
+
+ var image = content.document.images[0];
+
+ let checkWallpaper, restoreSettings;
+ try {
+ // Try via GSettings first
+ const gsettings = Cc["@mozilla.org/gsettings-service;1"]
+ .getService(Ci.nsIGSettingsService)
+ .getCollectionForSchema(GS_BG_SCHEMA);
+
+ const prevImage = gsettings.getString(GS_IMAGE_KEY);
+ const prevOption = gsettings.getString(GS_OPTION_KEY);
+ const prevDrawBG = gsettings.getBoolean(GS_DRAW_BG_KEY);
+
+ checkWallpaper = function (position, expectedGSettingsPosition) {
+ shell.setDesktopBackground(image, position, "");
+ ok(wpFile.exists(), "Wallpaper was written to disk");
+ is(
+ gsettings.getString(GS_IMAGE_KEY),
+ encodeURI("file://" + wpFile.path),
+ "Wallpaper file GSettings key is correct"
+ );
+ is(
+ gsettings.getString(GS_OPTION_KEY),
+ expectedGSettingsPosition,
+ "Wallpaper position GSettings key is correct"
+ );
+ };
+
+ restoreSettings = function () {
+ gsettings.setString(GS_IMAGE_KEY, prevImage);
+ gsettings.setString(GS_OPTION_KEY, prevOption);
+ gsettings.setBoolean(GS_DRAW_BG_KEY, prevDrawBG);
+ };
+ } catch (e) {}
+
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_TILE, "wallpaper");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_STRETCH, "stretched");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_CENTER, "centered");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_FILL, "zoom");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_FIT, "scaled");
+ checkWallpaper(Ci.nsIShellService.BACKGROUND_SPAN, "spanned");
+
+ restoreSettings();
+
+ // Restore files
+ if (wpFileBackup.exists()) {
+ wpFileBackup.moveTo(null, wpFile.leafName);
+ }
+ }
+ );
+});
diff --git a/browser/components/shell/test/browser_633221.js b/browser/components/shell/test/browser_633221.js
new file mode 100644
index 0000000000..0bea31728a
--- /dev/null
+++ b/browser/components/shell/test/browser_633221.js
@@ -0,0 +1,11 @@
+function test() {
+ ShellService.setDefaultBrowser(true, false);
+ ok(
+ ShellService.isDefaultBrowser(true, false),
+ "we got here and are the default browser"
+ );
+ ok(
+ ShellService.isDefaultBrowser(true, true),
+ "we got here and are the default browser"
+ );
+}
diff --git a/browser/components/shell/test/browser_doesAppNeedPin.js b/browser/components/shell/test/browser_doesAppNeedPin.js
new file mode 100644
index 0000000000..e5dc25faf8
--- /dev/null
+++ b/browser/components/shell/test/browser_doesAppNeedPin.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+});
+
+registerCleanupFunction(() => {
+ ExperimentAPI._store._deleteForTests("shellService");
+});
+
+let defaultValue;
+add_task(async function default_need() {
+ defaultValue = await ShellService.doesAppNeedPin();
+
+ Assert.ok(defaultValue !== undefined, "Got a default app need pin value");
+});
+
+add_task(async function remote_disable() {
+ if (defaultValue === false) {
+ info("Default pin already false, so nothing to test");
+ return;
+ }
+
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.shellService.featureId,
+ value: { disablePin: true, enabled: true },
+ });
+
+ Assert.equal(
+ await ShellService.doesAppNeedPin(),
+ false,
+ "Pinning disabled via nimbus"
+ );
+
+ await doCleanup();
+});
+
+add_task(async function restore_default() {
+ if (defaultValue === undefined) {
+ info("No default pin value set, so nothing to test");
+ return;
+ }
+
+ ExperimentAPI._store._deleteForTests("shellService");
+
+ Assert.equal(
+ await ShellService.doesAppNeedPin(),
+ defaultValue,
+ "Pinning restored to original"
+ );
+});
diff --git a/browser/components/shell/test/browser_headless_screenshot_1.js b/browser/components/shell/test/browser_headless_screenshot_1.js
new file mode 100644
index 0000000000..b5f84f0f66
--- /dev/null
+++ b/browser/components/shell/test/browser_headless_screenshot_1.js
@@ -0,0 +1,74 @@
+"use strict";
+
+add_task(async function () {
+ // Test all four basic variations of the "screenshot" argument
+ // when a file path is specified.
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ `-screenshot=${screenshotPath}`,
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "--screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ `--screenshot=${screenshotPath}`,
+ ],
+ screenshotPath
+ );
+
+ // Test when the requested URL redirects
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless_redirect.html",
+ "-screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+
+ // Test with additional command options
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ screenshotPath,
+ "-attach-console",
+ ],
+ screenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-attach-console",
+ "-screenshot",
+ screenshotPath,
+ "-headless",
+ ],
+ screenshotPath
+ );
+});
diff --git a/browser/components/shell/test/browser_headless_screenshot_2.js b/browser/components/shell/test/browser_headless_screenshot_2.js
new file mode 100644
index 0000000000..871895b45d
--- /dev/null
+++ b/browser/components/shell/test/browser_headless_screenshot_2.js
@@ -0,0 +1,48 @@
+"use strict";
+add_task(async function () {
+ const cwdScreenshotPath = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ "screenshot.png"
+ );
+
+ // Test variations of the "screenshot" argument when a file path
+ // isn't specified.
+ await testFileCreationPositive(
+ [
+ "-screenshot",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "--screenshot",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "--screenshot",
+ ],
+ cwdScreenshotPath
+ );
+
+ // Test with additional command options
+ await testFileCreationPositive(
+ [
+ "--screenshot",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-attach-console",
+ ],
+ cwdScreenshotPath
+ );
+});
diff --git a/browser/components/shell/test/browser_headless_screenshot_3.js b/browser/components/shell/test/browser_headless_screenshot_3.js
new file mode 100644
index 0000000000..b55635652d
--- /dev/null
+++ b/browser/components/shell/test/browser_headless_screenshot_3.js
@@ -0,0 +1,59 @@
+"use strict";
+
+add_task(async function () {
+ const cwdScreenshotPath = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ "screenshot.png"
+ );
+
+ // Test invalid URL arguments (either no argument or too many arguments).
+ await testFileCreationNegative(["-screenshot"], cwdScreenshotPath);
+ await testFileCreationNegative(
+ [
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "http://mochi.test:8888/headless.html",
+ "-screenshot",
+ ],
+ cwdScreenshotPath
+ );
+
+ // Test all four basic variations of the "window-size" argument.
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size",
+ "800",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size=800",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "--window-size",
+ "800",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "--window-size=800",
+ ],
+ cwdScreenshotPath
+ );
+});
diff --git a/browser/components/shell/test/browser_headless_screenshot_4.js b/browser/components/shell/test/browser_headless_screenshot_4.js
new file mode 100644
index 0000000000..4b93ba516e
--- /dev/null
+++ b/browser/components/shell/test/browser_headless_screenshot_4.js
@@ -0,0 +1,31 @@
+"use strict";
+
+add_task(async function () {
+ const cwdScreenshotPath = PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ "screenshot.png"
+ );
+ // Test other variations of the "window-size" argument.
+ await testWindowSizePositive(800, 600);
+ await testWindowSizePositive(1234);
+ await testFileCreationNegative(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size",
+ "hello",
+ ],
+ cwdScreenshotPath
+ );
+ await testFileCreationNegative(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ "-window-size",
+ "800,",
+ ],
+ cwdScreenshotPath
+ );
+});
diff --git a/browser/components/shell/test/browser_headless_screenshot_cross_origin.js b/browser/components/shell/test/browser_headless_screenshot_cross_origin.js
new file mode 100644
index 0000000000..9547773581
--- /dev/null
+++ b/browser/components/shell/test/browser_headless_screenshot_cross_origin.js
@@ -0,0 +1,9 @@
+"use strict";
+
+add_task(async function () {
+ // Test cross origin iframes work.
+ await testGreen(
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless_cross_origin.html",
+ screenshotPath
+ );
+});
diff --git a/browser/components/shell/test/browser_headless_screenshot_redirect.js b/browser/components/shell/test/browser_headless_screenshot_redirect.js
new file mode 100644
index 0000000000..c50b847b62
--- /dev/null
+++ b/browser/components/shell/test/browser_headless_screenshot_redirect.js
@@ -0,0 +1,14 @@
+"use strict";
+
+add_task(async function () {
+ // Test when the requested URL redirects
+ await testFileCreationPositive(
+ [
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless_redirect.html",
+ "-screenshot",
+ screenshotPath,
+ ],
+ screenshotPath
+ );
+});
diff --git a/browser/components/shell/test/browser_setDefaultBrowser.js b/browser/components/shell/test/browser_setDefaultBrowser.js
new file mode 100644
index 0000000000..ecf7fcb303
--- /dev/null
+++ b/browser/components/shell/test/browser_setDefaultBrowser.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const _handleWDBAResultStub = sinon
+ .stub(ShellService, "_handleWDBAResult")
+ .callsFake(async () => {
+ throw new Error("from _handleWDBAResultStub");
+ });
+
+const _callExternalDefaultBrowserAgentStub = sinon
+ .stub(ShellService, "_callExternalDefaultBrowserAgent")
+ .callsFake(async () => ({
+ async wait() {
+ return { exitCode: 0 };
+ },
+ }));
+
+const _userChoiceImpossibleTelemetryResultStub = sinon
+ .stub(ShellService, "_userChoiceImpossibleTelemetryResult")
+ .callsFake(() => null);
+
+const userChoiceStub = sinon
+ .stub(ShellService, "setAsDefaultUserChoice")
+ .resolves();
+const setDefaultStub = sinon.stub();
+const shellStub = sinon
+ .stub(ShellService, "shellService")
+ .value({ setDefaultBrowser: setDefaultStub });
+
+registerCleanupFunction(() => {
+ _handleWDBAResultStub.restore();
+ _callExternalDefaultBrowserAgentStub.restore();
+ _userChoiceImpossibleTelemetryResultStub.restore();
+ userChoiceStub.restore();
+ shellStub.restore();
+
+ ExperimentAPI._store._deleteForTests("shellService");
+});
+
+let defaultUserChoice;
+add_task(async function need_user_choice() {
+ ShellService.setDefaultBrowser();
+ defaultUserChoice = userChoiceStub.called;
+
+ Assert.ok(
+ defaultUserChoice !== undefined,
+ "Decided which default browser method to use"
+ );
+ Assert.equal(
+ setDefaultStub.notCalled,
+ defaultUserChoice,
+ "Only one default behavior was used"
+ );
+});
+
+add_task(async function remote_disable() {
+ if (defaultUserChoice === false) {
+ info("Default behavior already not user choice, so nothing to test");
+ return;
+ }
+
+ userChoiceStub.resetHistory();
+ setDefaultStub.resetHistory();
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: false,
+ enabled: true,
+ },
+ });
+
+ ShellService.setDefaultBrowser();
+
+ Assert.ok(
+ userChoiceStub.notCalled,
+ "Set default with user choice disabled via nimbus"
+ );
+ Assert.ok(setDefaultStub.called, "Used plain set default insteead");
+
+ await doCleanup();
+});
+
+add_task(async function restore_default() {
+ if (defaultUserChoice === undefined) {
+ info("No default user choice behavior set, so nothing to test");
+ return;
+ }
+
+ userChoiceStub.resetHistory();
+ setDefaultStub.resetHistory();
+ ExperimentAPI._store._deleteForTests("shellService");
+
+ ShellService.setDefaultBrowser();
+
+ Assert.equal(
+ userChoiceStub.called,
+ defaultUserChoice,
+ "Set default with user choice restored to original"
+ );
+ Assert.equal(
+ setDefaultStub.notCalled,
+ defaultUserChoice,
+ "Plain set default behavior restored to original"
+ );
+});
+
+add_task(async function ensure_fallback() {
+ if (!AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+ info("Nothing to test on non-Windows");
+ return;
+ }
+
+ let userChoicePromise = Promise.resolve();
+ userChoiceStub.callsFake(function (...args) {
+ return (userChoicePromise = userChoiceStub.wrappedMethod.apply(this, args));
+ });
+ userChoiceStub.resetHistory();
+ setDefaultStub.resetHistory();
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandler: false,
+ enabled: true,
+ },
+ });
+
+ ShellService.setDefaultBrowser();
+
+ Assert.ok(userChoiceStub.called, "Set default with user choice called");
+
+ let thrown = false;
+ await userChoicePromise.catch(() => (thrown = true));
+
+ Assert.ok(thrown, "Set default with user choice threw an error");
+ Assert.ok(setDefaultStub.called, "Fallbacked to plain set default");
+
+ await doCleanup();
+});
diff --git a/browser/components/shell/test/browser_setDefaultPDFHandler.js b/browser/components/shell/test/browser_setDefaultPDFHandler.js
new file mode 100644
index 0000000000..3314a4cf21
--- /dev/null
+++ b/browser/components/shell/test/browser_setDefaultPDFHandler.js
@@ -0,0 +1,291 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
+ ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "XreDirProvider",
+ "@mozilla.org/xre/directory-provider;1",
+ "nsIXREDirProvider"
+);
+
+const _callExternalDefaultBrowserAgentStub = sinon
+ .stub(ShellService, "_callExternalDefaultBrowserAgent")
+ .callsFake(async () => ({
+ async wait() {
+ return { exitCode: 0 };
+ },
+ }));
+
+const _userChoiceImpossibleTelemetryResultStub = sinon
+ .stub(ShellService, "_userChoiceImpossibleTelemetryResult")
+ .callsFake(() => null);
+
+// Ensure we don't fall back to a real implementation.
+const setDefaultStub = sinon.stub();
+// We'll dynamically update this as needed during the tests.
+const queryCurrentDefaultHandlerForStub = sinon.stub();
+const shellStub = sinon.stub(ShellService, "shellService").value({
+ setDefaultBrowser: setDefaultStub,
+ queryCurrentDefaultHandlerFor: queryCurrentDefaultHandlerForStub,
+});
+
+registerCleanupFunction(() => {
+ _callExternalDefaultBrowserAgentStub.restore();
+ _userChoiceImpossibleTelemetryResultStub.restore();
+ shellStub.restore();
+
+ ExperimentAPI._store._deleteForTests("shellService");
+});
+
+add_task(async function ready() {
+ await ExperimentAPI.ready();
+});
+
+// Everything here is Windows 10+.
+Assert.ok(
+ AppConstants.isPlatformAndVersionAtLeast("win", "10"),
+ "Windows version 10+"
+);
+
+add_task(async function remoteEnableWithPDF() {
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandlerOnlyReplaceBrowsers: false,
+ setDefaultPDFHandler: true,
+ enabled: true,
+ },
+ });
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ true
+ );
+
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+ ShellService.setDefaultBrowser();
+
+ const aumi = XreDirProvider.getInstallHash();
+ Assert.ok(_callExternalDefaultBrowserAgentStub.called);
+ Assert.deepEqual(_callExternalDefaultBrowserAgentStub.firstCall.args, [
+ {
+ arguments: [
+ "set-default-browser-user-choice",
+ aumi,
+ ".pdf",
+ "FirefoxPDF",
+ ],
+ },
+ ]);
+
+ await doCleanup();
+});
+
+add_task(async function remoteEnableWithPDF_testOnlyReplaceBrowsers() {
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandlerOnlyReplaceBrowsers: true,
+ setDefaultPDFHandler: true,
+ enabled: true,
+ },
+ });
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable(
+ "setDefaultPDFHandlerOnlyReplaceBrowsers"
+ ),
+ true
+ );
+
+ const aumi = XreDirProvider.getInstallHash();
+
+ // We'll take the default from a missing association or a known browser.
+ for (let progId of ["", "MSEdgePDF"]) {
+ queryCurrentDefaultHandlerForStub.callsFake(() => progId);
+
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+ ShellService.setDefaultBrowser();
+
+ Assert.ok(_callExternalDefaultBrowserAgentStub.called);
+ Assert.deepEqual(
+ _callExternalDefaultBrowserAgentStub.firstCall.args,
+ [
+ {
+ arguments: [
+ "set-default-browser-user-choice",
+ aumi,
+ ".pdf",
+ "FirefoxPDF",
+ ],
+ },
+ ],
+ `Will take default from missing association or known browser with ProgID '${progId}'`
+ );
+ }
+
+ // But not from a non-browser.
+ queryCurrentDefaultHandlerForStub.callsFake(() => "Acrobat.Document.DC");
+
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+ ShellService.setDefaultBrowser();
+
+ Assert.ok(_callExternalDefaultBrowserAgentStub.called);
+ Assert.deepEqual(
+ _callExternalDefaultBrowserAgentStub.firstCall.args,
+ [{ arguments: ["set-default-browser-user-choice", aumi] }],
+ `Will not take default from non-browser`
+ );
+
+ await doCleanup();
+});
+
+add_task(async function remoteEnableWithoutPDF() {
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: true,
+ setDefaultPDFHandler: false,
+ enabled: true,
+ },
+ });
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ true
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ false
+ );
+
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+ ShellService.setDefaultBrowser();
+
+ const aumi = XreDirProvider.getInstallHash();
+ Assert.ok(_callExternalDefaultBrowserAgentStub.called);
+ Assert.deepEqual(_callExternalDefaultBrowserAgentStub.firstCall.args, [
+ { arguments: ["set-default-browser-user-choice", aumi] },
+ ]);
+
+ await doCleanup();
+});
+
+add_task(async function remoteDisable() {
+ let doCleanup = await ExperimentFakes.enrollWithRollout({
+ featureId: NimbusFeatures.shellService.featureId,
+ value: {
+ setDefaultBrowserUserChoice: false,
+ setDefaultPDFHandler: true,
+ enabled: false,
+ },
+ });
+
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultBrowserUserChoice"),
+ false
+ );
+ Assert.equal(
+ NimbusFeatures.shellService.getVariable("setDefaultPDFHandler"),
+ true
+ );
+
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+ ShellService.setDefaultBrowser();
+
+ Assert.ok(_callExternalDefaultBrowserAgentStub.notCalled);
+ Assert.ok(setDefaultStub.called);
+
+ await doCleanup();
+});
+
+add_task(async function test_setAsDefaultPDFHandler_knownBrowser() {
+ const sandbox = sinon.createSandbox();
+
+ const aumi = XreDirProvider.getInstallHash();
+ const expectedArguments = [
+ "set-default-extension-handlers-user-choice",
+ aumi,
+ ".pdf",
+ "FirefoxPDF",
+ ];
+
+ try {
+ const pdfHandlerResult = { registered: true, knownBrowser: true };
+ sandbox
+ .stub(ShellService, "getDefaultPDFHandler")
+ .returns(pdfHandlerResult);
+
+ info("Testing setAsDefaultPDFHandler(true) when knownBrowser = true");
+ ShellService.setAsDefaultPDFHandler(true);
+ Assert.ok(
+ _callExternalDefaultBrowserAgentStub.called,
+ "Called default browser agent"
+ );
+ Assert.deepEqual(
+ _callExternalDefaultBrowserAgentStub.firstCall.args,
+ [{ arguments: expectedArguments }],
+ "Called default browser agent with expected arguments"
+ );
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+
+ info("Testing setAsDefaultPDFHandler(false) when knownBrowser = true");
+ ShellService.setAsDefaultPDFHandler(false);
+ Assert.ok(
+ _callExternalDefaultBrowserAgentStub.called,
+ "Called default browser agent"
+ );
+ Assert.deepEqual(
+ _callExternalDefaultBrowserAgentStub.firstCall.args,
+ [{ arguments: expectedArguments }],
+ "Called default browser agent with expected arguments"
+ );
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+
+ pdfHandlerResult.knownBrowser = false;
+
+ info("Testing setAsDefaultPDFHandler(true) when knownBrowser = false");
+ ShellService.setAsDefaultPDFHandler(true);
+ Assert.ok(
+ _callExternalDefaultBrowserAgentStub.notCalled,
+ "Did not call default browser agent"
+ );
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+
+ info("Testing setAsDefaultPDFHandler(false) when knownBrowser = false");
+ ShellService.setAsDefaultPDFHandler(false);
+ Assert.ok(
+ _callExternalDefaultBrowserAgentStub.called,
+ "Called default browser agent"
+ );
+ Assert.deepEqual(
+ _callExternalDefaultBrowserAgentStub.firstCall.args,
+ [{ arguments: expectedArguments }],
+ "Called default browser agent with expected arguments"
+ );
+ _callExternalDefaultBrowserAgentStub.resetHistory();
+ } finally {
+ sandbox.restore();
+ }
+});
diff --git a/browser/components/shell/test/browser_setDesktopBackgroundPreview.js b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js
new file mode 100644
index 0000000000..b2dbe13db8
--- /dev/null
+++ b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check whether the preview image for setDesktopBackground is rendered
+ * correctly, without stretching
+ */
+
+add_task(async function () {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:logo",
+ },
+ async browser => {
+ const dialogLoad = BrowserTestUtils.domWindowOpened(null, async win => {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ Assert.equal(
+ win.document.documentElement.getAttribute("windowtype"),
+ "Shell:SetDesktopBackground",
+ "Opened correct window"
+ );
+ return true;
+ });
+
+ const image = content.document.images[0];
+ EventUtils.synthesizeMouseAtCenter(image, { type: "contextmenu" });
+
+ const menu = document.getElementById("contentAreaContextMenu");
+ await BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden");
+
+ const menuItem = document.getElementById("context-setDesktopBackground");
+ try {
+ menu.activateItem(menuItem);
+ } catch (ex) {
+ ok(
+ menuItem.hidden,
+ "should only fail to activate when menu item is hidden"
+ );
+ ok(
+ !ShellService.canSetDesktopBackground,
+ "Should only hide when not able to set the desktop background"
+ );
+ is(
+ AppConstants.platform,
+ "linux",
+ "Should always be able to set desktop background on non-linux platforms"
+ );
+ todo(false, "Skipping test on this configuration");
+
+ menu.hidePopup();
+ await menuClosed;
+ return;
+ }
+
+ await menuClosed;
+
+ const win = await dialogLoad;
+
+ /* setDesktopBackground.js does a setTimeout to wait for correct
+ dimensions. If we don't wait here we could read the preview dimensions
+ before they're changed to match the screen */
+ await TestUtils.waitForTick();
+
+ const canvas = win.document.getElementById("screen");
+ const screenRatio = screen.width / screen.height;
+ const previewRatio = canvas.clientWidth / canvas.clientHeight;
+
+ info(`Screen dimensions are ${screen.width}x${screen.height}`);
+ info(`Screen's raw ratio is ${screenRatio}`);
+ info(
+ `Preview dimensions are ${canvas.clientWidth}x${canvas.clientHeight}`
+ );
+ info(`Preview's raw ratio is ${previewRatio}`);
+
+ Assert.ok(
+ previewRatio < screenRatio + 0.01 && previewRatio > screenRatio - 0.01,
+ "Preview's aspect ratio is within ±.01 of screen's"
+ );
+
+ win.close();
+
+ await menuClosed;
+ }
+ );
+});
diff --git a/browser/components/shell/test/head.js b/browser/components/shell/test/head.js
new file mode 100644
index 0000000000..d2114941e4
--- /dev/null
+++ b/browser/components/shell/test/head.js
@@ -0,0 +1,159 @@
+"use strict";
+
+const { Subprocess } = ChromeUtils.importESModule(
+ "resource://gre/modules/Subprocess.sys.mjs"
+);
+
+const TEMP_DIR = Services.dirsvc.get("TmpD", Ci.nsIFile).path;
+
+const screenshotPath = PathUtils.join(TEMP_DIR, "headless_test_screenshot.png");
+
+async function runFirefox(args) {
+ const XRE_EXECUTABLE_FILE = "XREExeF";
+ const firefoxExe = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).path;
+ const NS_APP_PREFS_50_FILE = "PrefF";
+ const mochiPrefsFile = Services.dirsvc.get(NS_APP_PREFS_50_FILE, Ci.nsIFile);
+ const mochiPrefsPath = mochiPrefsFile.path;
+ const mochiPrefsName = mochiPrefsFile.leafName;
+ const profilePath = PathUtils.join(
+ TEMP_DIR,
+ "headless_test_screenshot_profile"
+ );
+ const prefsPath = PathUtils.join(profilePath, mochiPrefsName);
+ const firefoxArgs = ["-profile", profilePath, "-no-remote"];
+
+ await IOUtils.makeDirectory(profilePath);
+ await IOUtils.copy(mochiPrefsPath, prefsPath);
+ let proc = await Subprocess.call({
+ command: firefoxExe,
+ arguments: firefoxArgs.concat(args),
+ // Disable leak detection to avoid intermittent failure bug 1331152.
+ environmentAppend: true,
+ environment: {
+ ASAN_OPTIONS:
+ "detect_leaks=0:quarantine_size=50331648:malloc_context_size=5",
+ // Don't enable Marionette.
+ MOZ_MARIONETTE: null,
+ },
+ });
+ let stdout;
+ while ((stdout = await proc.stdout.readString())) {
+ dump(`>>> ${stdout}\n`);
+ }
+ let { exitCode } = await proc.wait();
+ is(exitCode, 0, "Firefox process should exit with code 0");
+ await IOUtils.remove(profilePath, { recursive: true });
+}
+
+async function testFileCreationPositive(args, path) {
+ await runFirefox(args);
+
+ let saved = IOUtils.exists(path);
+ ok(saved, "A screenshot should be saved as " + path);
+ if (!saved) {
+ return;
+ }
+
+ let info = await IOUtils.stat(path);
+ ok(info.size > 0, "Screenshot should not be an empty file");
+ await IOUtils.remove(path);
+}
+
+async function testFileCreationNegative(args, path) {
+ await runFirefox(args);
+
+ let saved = await IOUtils.exists(path);
+ ok(!saved, "A screenshot should not be saved");
+ await IOUtils.remove(path);
+}
+
+async function testWindowSizePositive(width, height) {
+ let size = String(width);
+ if (height) {
+ size += "," + height;
+ }
+
+ await runFirefox([
+ "-url",
+ "http://mochi.test:8888/browser/browser/components/shell/test/headless.html",
+ "-screenshot",
+ screenshotPath,
+ "-window-size",
+ size,
+ ]);
+
+ let saved = await IOUtils.exists(screenshotPath);
+ ok(saved, "A screenshot should be saved in the tmp directory");
+ if (!saved) {
+ return;
+ }
+
+ let data = await IOUtils.read(screenshotPath);
+ await new Promise((resolve, reject) => {
+ let blob = new Blob([data], { type: "image/png" });
+ let reader = new FileReader();
+ reader.onloadend = function () {
+ let screenshot = new Image();
+ screenshot.onload = function () {
+ is(
+ screenshot.width,
+ width,
+ "Screenshot should be " + width + " pixels wide"
+ );
+ if (height) {
+ is(
+ screenshot.height,
+ height,
+ "Screenshot should be " + height + " pixels tall"
+ );
+ }
+ resolve();
+ };
+ screenshot.src = reader.result;
+ };
+ reader.readAsDataURL(blob);
+ });
+ await IOUtils.remove(screenshotPath);
+}
+
+async function testGreen(url, path) {
+ await runFirefox(["-url", url, `--screenshot=${path}`]);
+
+ let saved = await IOUtils.exists(path);
+ ok(saved, "A screenshot should be saved in the tmp directory");
+ if (!saved) {
+ return;
+ }
+
+ let data = await IOUtils.read(path);
+ let image = await new Promise((resolve, reject) => {
+ let blob = new Blob([data], { type: "image/png" });
+ let reader = new FileReader();
+ reader.onloadend = function () {
+ let screenshot = new Image();
+ screenshot.onload = function () {
+ resolve(screenshot);
+ };
+ screenshot.src = reader.result;
+ };
+ reader.readAsDataURL(blob);
+ });
+ let canvas = document.createElement("canvas");
+ canvas.width = image.naturalWidth;
+ canvas.height = image.naturalHeight;
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(image, 0, 0);
+ let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ let rgba = imageData.data;
+
+ let found = false;
+ for (let i = 0; i < rgba.length; i += 4) {
+ if (rgba[i] === 0 && rgba[i + 1] === 255 && rgba[i + 2] === 0) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, "There should be a green pixel in the screenshot.");
+
+ await IOUtils.remove(path);
+}
diff --git a/browser/components/shell/test/headless.html b/browser/components/shell/test/headless.html
new file mode 100644
index 0000000000..bbde895077
--- /dev/null
+++ b/browser/components/shell/test/headless.html
@@ -0,0 +1,6 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body style="background-color: rgb(0, 255, 0); color: rgb(0, 0, 255)">
+Hi
+</body>
+</html>
diff --git a/browser/components/shell/test/headless_cross_origin.html b/browser/components/shell/test/headless_cross_origin.html
new file mode 100644
index 0000000000..3bb09aa5d8
--- /dev/null
+++ b/browser/components/shell/test/headless_cross_origin.html
@@ -0,0 +1,7 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body>
+<iframe width="300" height="200" src="http://example.com/browser/browser/components/shell/test/headless_iframe.html"></iframe>
+Hi
+</body>
+</html>
diff --git a/browser/components/shell/test/headless_iframe.html b/browser/components/shell/test/headless_iframe.html
new file mode 100644
index 0000000000..f318cbe0f3
--- /dev/null
+++ b/browser/components/shell/test/headless_iframe.html
@@ -0,0 +1,6 @@
+<html>
+<head><meta content="text/html; charset=utf-8" http-equiv="Content-Type"></head>
+<body style="background-color: rgb(0, 255, 0);">
+Hi
+</body>
+</html>
diff --git a/browser/components/shell/test/headless_redirect.html b/browser/components/shell/test/headless_redirect.html
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/browser/components/shell/test/headless_redirect.html
diff --git a/browser/components/shell/test/headless_redirect.html^headers^ b/browser/components/shell/test/headless_redirect.html^headers^
new file mode 100644
index 0000000000..1d7db20ea5
--- /dev/null
+++ b/browser/components/shell/test/headless_redirect.html^headers^
@@ -0,0 +1,2 @@
+HTTP 302 Moved Temporarily
+Location: headless.html \ No newline at end of file
diff --git a/browser/components/shell/test/mac_desktop_image.py b/browser/components/shell/test/mac_desktop_image.py
new file mode 100755
index 0000000000..e3ccc77190
--- /dev/null
+++ b/browser/components/shell/test/mac_desktop_image.py
@@ -0,0 +1,168 @@
+#!/usr/bin/python
+# 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/. */
+
+"""
+mac_desktop_image.py
+
+Mac-specific utility to get/set the desktop background image or check that
+the current background image path matches a provided path.
+
+Depends on Objective-C python binding imports which are in the python import
+paths by default when using macOS's /usr/bin/python.
+
+Includes generous amount of logging to aid debugging for use in automated tests.
+"""
+
+import argparse
+import logging
+import os
+import sys
+
+#
+# These Objective-C bindings imports are included in the import path by default
+# for the Mac-bundled python installed in /usr/bin/python. They're needed to
+# call the Objective-C API's to set and retrieve the current desktop background
+# image.
+#
+from AppKit import NSScreen, NSWorkspace
+from Cocoa import NSURL
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Utility to print, set, or "
+ + "check the path to image being used as "
+ + "the desktop background image. By "
+ + "default, prints the path to the "
+ + "current desktop background image."
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="print verbose debugging information",
+ default=False,
+ )
+ group = parser.add_mutually_exclusive_group()
+ group.add_argument(
+ "-s",
+ "--set-background-image",
+ dest="newBackgroundImagePath",
+ required=False,
+ help="path to the new background image to set. A zero "
+ + "exit code indicates no errors occurred.",
+ default=None,
+ )
+ group.add_argument(
+ "-c",
+ "--check-background-image",
+ dest="checkBackgroundImagePath",
+ required=False,
+ help="check if the provided background image path "
+ + "matches the provided path. A zero exit code "
+ + "indicates the paths match.",
+ default=None,
+ )
+ args = parser.parse_args()
+
+ # Using logging for verbose output
+ if args.verbose:
+ logging.basicConfig(level=logging.DEBUG)
+ else:
+ logging.basicConfig(level=logging.CRITICAL)
+ logger = logging.getLogger("desktopImage")
+
+ # Print what we're going to do
+ if args.checkBackgroundImagePath is not None:
+ logger.debug(
+ "checking provided desktop image %s matches current "
+ "image" % args.checkBackgroundImagePath
+ )
+ elif args.newBackgroundImagePath is not None:
+ logger.debug("setting image to %s " % args.newBackgroundImagePath)
+ else:
+ logger.debug("retrieving desktop image path")
+
+ focussedScreen = NSScreen.mainScreen()
+ if not focussedScreen:
+ raise RuntimeError("mainScreen error")
+
+ ws = NSWorkspace.sharedWorkspace()
+ if not ws:
+ raise RuntimeError("sharedWorkspace error")
+
+ # If we're just checking the image path, check it and then return.
+ # A successful exit code (0) indicates the paths match.
+ if args.checkBackgroundImagePath is not None:
+ # Get existing desktop image path and resolve it
+ existingImageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
+ existingImagePath = existingImageURL.path()
+ existingImagePathReal = os.path.realpath(existingImagePath)
+ logger.debug("existing desktop image: %s" % existingImagePath)
+ logger.debug("existing desktop image realpath: %s" % existingImagePath)
+
+ # Resolve the path we're going to check
+ checkImagePathReal = os.path.realpath(args.checkBackgroundImagePath)
+ logger.debug("check desktop image: %s" % args.checkBackgroundImagePath)
+ logger.debug("check desktop image realpath: %s" % checkImagePathReal)
+
+ if existingImagePathReal == checkImagePathReal:
+ print("desktop image path matches provided path")
+ return True
+
+ print("desktop image path does NOT match provided path")
+ return False
+
+ # Log the current desktop image
+ if args.verbose:
+ existingImageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
+ logger.debug("existing desktop image: %s" % existingImageURL.path())
+
+ # Set the desktop image
+ if args.newBackgroundImagePath is not None:
+ newImagePath = args.newBackgroundImagePath
+ if not os.path.exists(newImagePath):
+ logger.critical("%s does not exist" % newImagePath)
+ return False
+ if not os.access(newImagePath, os.R_OK):
+ logger.critical("%s is not readable" % newImagePath)
+ return False
+
+ logger.debug("new desktop image to set: %s" % newImagePath)
+ newImageURL = NSURL.fileURLWithPath_(newImagePath)
+ logger.debug("new desktop image URL to set: %s" % newImageURL)
+
+ status = False
+ (status, error) = ws.setDesktopImageURL_forScreen_options_error_(
+ newImageURL, focussedScreen, None, None
+ )
+ if not status:
+ raise RuntimeError("setDesktopImageURL error")
+
+ # Print the current desktop image
+ imageURL = getCurrentDesktopImageURL(focussedScreen, ws, logger)
+ imagePath = imageURL.path()
+ imagePathReal = os.path.realpath(imagePath)
+ logger.debug("updated desktop image URL: %s" % imageURL)
+ logger.debug("updated desktop image path: %s" % imagePath)
+ logger.debug("updated desktop image path (resolved): %s" % imagePathReal)
+ print(imagePathReal)
+ return True
+
+
+def getCurrentDesktopImageURL(focussedScreen, workspace, logger):
+ imageURL = workspace.desktopImageURLForScreen_(focussedScreen)
+ if not imageURL:
+ raise RuntimeError("desktopImageURLForScreen returned invalid URL")
+ if not imageURL.isFileURL():
+ logger.warning("desktop image URL is not a file URL")
+ return imageURL
+
+
+if __name__ == "__main__":
+ if not main():
+ sys.exit(1)
+ else:
+ sys.exit(0)
diff --git a/browser/components/shell/test/unit/test_macOS_showSecurityPreferences.js b/browser/components/shell/test/unit/test_macOS_showSecurityPreferences.js
new file mode 100644
index 0000000000..df3f4a5918
--- /dev/null
+++ b/browser/components/shell/test/unit/test_macOS_showSecurityPreferences.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the macOS ShowSecurityPreferences shell service method.
+ */
+
+function killSystemPreferences() {
+ let killallFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ killallFile.initWithPath("/usr/bin/killall");
+ let sysPrefsArg = ["System Preferences"];
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(killallFile);
+ process.run(true, sysPrefsArg, 1);
+ return process.exitValue;
+}
+
+add_setup(async function () {
+ info("Ensure System Preferences isn't already running");
+ killSystemPreferences();
+});
+
+add_task(async function test_prefsOpen() {
+ let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIMacShellService
+ );
+ shellSvc.showSecurityPreferences("Privacy_AllFiles");
+
+ equal(killSystemPreferences(), 0, "Ensure System Preferences was started");
+});
diff --git a/browser/components/shell/test/unit/xpcshell.ini b/browser/components/shell/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..383dcb3475
--- /dev/null
+++ b/browser/components/shell/test/unit/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+skip-if = toolkit == 'android' # bug 1730213
+firefox-appdir = browser
+
+[test_macOS_showSecurityPreferences.js]
+skip-if = toolkit != "cocoa"