From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- browser/components/shell/HeadlessShell.sys.mjs | 262 +++ browser/components/shell/ScreenshotChild.sys.mjs | 31 + browser/components/shell/ShellService.sys.mjs | 572 ++++++ browser/components/shell/WindowsDefaultBrowser.cpp | 162 ++ browser/components/shell/WindowsDefaultBrowser.h | 20 + browser/components/shell/WindowsUserChoice.cpp | 422 +++++ browser/components/shell/WindowsUserChoice.h | 103 ++ .../shell/content/setDesktopBackground.js | 252 +++ .../shell/content/setDesktopBackground.xhtml | 88 + browser/components/shell/jar.mn | 7 + browser/components/shell/moz.build | 92 + .../components/shell/nsGNOMEShellDBusHelper.cpp | 535 ++++++ browser/components/shell/nsGNOMEShellDBusHelper.h | 37 + .../shell/nsGNOMEShellSearchProvider.cpp | 442 +++++ .../components/shell/nsGNOMEShellSearchProvider.h | 139 ++ browser/components/shell/nsGNOMEShellService.cpp | 506 ++++++ browser/components/shell/nsGNOMEShellService.h | 46 + browser/components/shell/nsIGNOMEShellService.idl | 23 + browser/components/shell/nsIMacShellService.idl | 27 + browser/components/shell/nsIShellService.idl | 69 + .../components/shell/nsIWindowsShellService.idl | 187 ++ browser/components/shell/nsMacShellService.cpp | 281 +++ browser/components/shell/nsMacShellService.h | 33 + browser/components/shell/nsShellService.h | 11 + browser/components/shell/nsToolkitShellService.h | 21 + browser/components/shell/nsWindowsShellService.cpp | 1845 ++++++++++++++++++++ browser/components/shell/nsWindowsShellService.h | 38 + .../components/shell/search-provider-files/README | 20 + .../firefox-search-provider.ini | 5 + .../shell/search-provider-files/firefox.desktop | 273 +++ browser/components/shell/test/browser.ini | 69 + browser/components/shell/test/browser_1119088.js | 167 ++ browser/components/shell/test/browser_420786.js | 105 ++ browser/components/shell/test/browser_633221.js | 11 + .../shell/test/browser_doesAppNeedPin.js | 54 + .../shell/test/browser_headless_screenshot_1.js | 74 + .../shell/test/browser_headless_screenshot_2.js | 48 + .../shell/test/browser_headless_screenshot_3.js | 59 + .../shell/test/browser_headless_screenshot_4.js | 31 + .../browser_headless_screenshot_cross_origin.js | 9 + .../test/browser_headless_screenshot_redirect.js | 14 + .../shell/test/browser_setDefaultBrowser.js | 146 ++ .../shell/test/browser_setDefaultPDFHandler.js | 291 +++ .../test/browser_setDesktopBackgroundPreview.js | 87 + browser/components/shell/test/head.js | 159 ++ browser/components/shell/test/headless.html | 6 + .../shell/test/headless_cross_origin.html | 7 + browser/components/shell/test/headless_iframe.html | 6 + .../components/shell/test/headless_redirect.html | 0 .../shell/test/headless_redirect.html^headers^ | 2 + browser/components/shell/test/mac_desktop_image.py | 168 ++ .../unit/test_macOS_showSecurityPreferences.js | 30 + browser/components/shell/test/unit/xpcshell.ini | 6 + 53 files changed, 8098 insertions(+) create mode 100644 browser/components/shell/HeadlessShell.sys.mjs create mode 100644 browser/components/shell/ScreenshotChild.sys.mjs create mode 100644 browser/components/shell/ShellService.sys.mjs create mode 100644 browser/components/shell/WindowsDefaultBrowser.cpp create mode 100644 browser/components/shell/WindowsDefaultBrowser.h create mode 100644 browser/components/shell/WindowsUserChoice.cpp create mode 100644 browser/components/shell/WindowsUserChoice.h create mode 100644 browser/components/shell/content/setDesktopBackground.js create mode 100644 browser/components/shell/content/setDesktopBackground.xhtml create mode 100644 browser/components/shell/jar.mn create mode 100644 browser/components/shell/moz.build create mode 100644 browser/components/shell/nsGNOMEShellDBusHelper.cpp create mode 100644 browser/components/shell/nsGNOMEShellDBusHelper.h create mode 100644 browser/components/shell/nsGNOMEShellSearchProvider.cpp create mode 100644 browser/components/shell/nsGNOMEShellSearchProvider.h create mode 100644 browser/components/shell/nsGNOMEShellService.cpp create mode 100644 browser/components/shell/nsGNOMEShellService.h create mode 100644 browser/components/shell/nsIGNOMEShellService.idl create mode 100644 browser/components/shell/nsIMacShellService.idl create mode 100644 browser/components/shell/nsIShellService.idl create mode 100644 browser/components/shell/nsIWindowsShellService.idl create mode 100644 browser/components/shell/nsMacShellService.cpp create mode 100644 browser/components/shell/nsMacShellService.h create mode 100644 browser/components/shell/nsShellService.h create mode 100644 browser/components/shell/nsToolkitShellService.h create mode 100644 browser/components/shell/nsWindowsShellService.cpp create mode 100644 browser/components/shell/nsWindowsShellService.h create mode 100644 browser/components/shell/search-provider-files/README create mode 100644 browser/components/shell/search-provider-files/firefox-search-provider.ini create mode 100644 browser/components/shell/search-provider-files/firefox.desktop create mode 100644 browser/components/shell/test/browser.ini create mode 100644 browser/components/shell/test/browser_1119088.js create mode 100644 browser/components/shell/test/browser_420786.js create mode 100644 browser/components/shell/test/browser_633221.js create mode 100644 browser/components/shell/test/browser_doesAppNeedPin.js create mode 100644 browser/components/shell/test/browser_headless_screenshot_1.js create mode 100644 browser/components/shell/test/browser_headless_screenshot_2.js create mode 100644 browser/components/shell/test/browser_headless_screenshot_3.js create mode 100644 browser/components/shell/test/browser_headless_screenshot_4.js create mode 100644 browser/components/shell/test/browser_headless_screenshot_cross_origin.js create mode 100644 browser/components/shell/test/browser_headless_screenshot_redirect.js create mode 100644 browser/components/shell/test/browser_setDefaultBrowser.js create mode 100644 browser/components/shell/test/browser_setDefaultPDFHandler.js create mode 100644 browser/components/shell/test/browser_setDesktopBackgroundPreview.js create mode 100644 browser/components/shell/test/head.js create mode 100644 browser/components/shell/test/headless.html create mode 100644 browser/components/shell/test/headless_cross_origin.html create mode 100644 browser/components/shell/test/headless_iframe.html create mode 100644 browser/components/shell/test/headless_redirect.html create mode 100644 browser/components/shell/test/headless_redirect.html^headers^ create mode 100755 browser/components/shell/test/mac_desktop_image.py create mode 100644 browser/components/shell/test/unit/test_macOS_showSecurityPreferences.js create mode 100644 browser/components/shell/test/unit/xpcshell.ini (limited to 'browser/components/shell') 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 + +#include +#include +#include +#include + +#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& aAppRegName) { + mozilla::UniquePtr appDirStr; + bool success = GetInstallDirectory(appDirStr); + if (!success) { + return success; + } + + uint64_t hash = CityHash64(reinterpret_cast(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(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 appRegName; + GetAppRegName(appRegName); + int bufferSize = _scwprintf(paramFormat, appRegName.get()); + ++bufferSize; // Extra byte for terminating null + mozilla::UniquePtr params = + mozilla::MakeUnique(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& 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 + * - based on a PureBasic version by LMongrain + * + * - AssocHashGen by "halfmeasuresdisabled", see bug 1225660 and + * + * - SetUserFTA changelog + * + */ + +#include +#include // for ConvertSidToStringSidW +#include // for CryptoAPI base64 +#include // for CNG MD5 +#include // for NT_SUCCESS() + +#include "mozilla/ArrayUtils.h" +#include "mozilla/UniquePtr.h" +#include "nsWindowsHelpers.h" + +#include "WindowsUserChoice.h" + +using namespace mozilla; + +UniquePtr 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(userSize); + if (!::GetTokenInformation(processToken.get(), TokenUser, userBytes.get(), + userSize, &userSize)) { + return nullptr; + } + + wchar_t* rawSid = nullptr; + if (!::ConvertSidToStringSidW( + reinterpret_cast(userBytes.get())->User.Sid, &rawSid)) { + return nullptr; + } + UniquePtr sid(rawSid); + + // Copy instead of passing UniquePtr back to + // the caller. + int sidLen = ::lstrlenW(sid.get()) + 1; + auto outSid = MakeUnique(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 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(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 CNG_MD5(const unsigned char* bytes, ULONG bytesLen) { + constexpr ULONG MD5_BYTES = 16; + constexpr ULONG MD5_DWORDS = MD5_BYTES / sizeof(DWORD); + UniquePtr 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(bytes), + bytesLen, 0))) { + hash = MakeUnique(MD5_DWORDS); + if (!NT_SUCCESS(::BCryptFinishHash( + hHash, reinterpret_cast(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 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(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 HashString(const wchar_t* inputString) { + auto inputBytes = reinterpret_cast(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(hash), + sizeof(hash)); +} + +UniquePtr 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(keyPathLen); + _snwprintf_s(keyPath.get(), keyPathLen, _TRUNCATE, keyPathFmt, aExt); + + return keyPath; +} + +UniquePtr 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 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 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 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(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 + +#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 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 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 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 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 @@ + + +# 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/. + + + + + + + + + + + + +#ifdef XP_MACOSX +#include ../../../base/content/macWindow.inc.xhtml +#else +