diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /browser/components/shell | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/shell')
55 files changed, 8432 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..c4af0be7de --- /dev/null +++ b/browser/components/shell/ShellService.sys.mjs @@ -0,0 +1,499 @@ +/* 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", +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "XreDirProvider", + "@mozilla.org/xre/directory-provider;1", + "nsIXREDirProvider" +); + +ChromeUtils.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; + }, + + /** + * 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; + } + + 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; + }, + + /* + * 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. + "AppXd4nrz8ff68srnhf9t5a8sbjyar1cr723", // Another pre-Blink Edge identifier. See Bug 1858729. + "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"); + + 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 extraFileExtensions = []; + if ( + lazy.NimbusFeatures.shellService.getVariable("setDefaultPDFHandler") + ) { + if (this._shouldSetDefaultPDFHandler()) { + lazy.log.info("Setting Firefox as default PDF handler"); + extraFileExtensions.push(".pdf", "FirefoxPDF"); + } else { + lazy.log.info("Not setting Firefox as default PDF handler"); + } + } + try { + await this.defaultAgent.setDefaultBrowserUserChoiceAsync( + aumi, + extraFileExtensions + ); + } catch (err) { + telemetryResult = "ErrOther"; + this._handleWDBAResult(err.result || Cr.NS_ERROR_FAILURE); + } + 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"); + } + + let telemetryResult = "ErrOther"; + + try { + const aumi = lazy.XreDirProvider.getInstallHash(); + try { + this.defaultAgent.setDefaultExtensionHandlersUserChoice(aumi, [ + ".pdf", + "FirefoxPDF", + ]); + } catch (err) { + telemetryResult = "ErrOther"; + this._handleWDBAResult(err.result || Cr.NS_ERROR_FAILURE); + } + 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. + async setDefaultBrowser(forAllUsers) { + // On Windows, our best chance is to set UserChoice, so try that first. + if ( + AppConstants.platform == "win" && + lazy.NimbusFeatures.shellService.getVariable( + "setDefaultBrowserUserChoice" + ) + ) { + try { + await this.setAsDefaultUserChoice(); + return; + } catch (err) { + lazy.log.warn( + "Error thrown during setAsDefaultUserChoice. Full exception:", + err + ); + + // intentionally fall through to setting via the non-user choice pathway on error + } + } + + this.shellService.setDefaultBrowser(forAllUsers); + }, + + async setAsDefault() { + let setAsDefaultError = false; + try { + await ShellService.setDefaultBrowser(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.platform == "win") { + 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); + } + } + }, + + _handleWDBAResult(exitCode) { + if (exitCode != Cr.NS_OK) { + const telemetryResult = + new Map([ + [Cr.NS_ERROR_WDBA_NO_PROGID, "ErrExeProgID"], + [Cr.NS_ERROR_WDBA_HASH_CHECK, "ErrExeHash"], + [Cr.NS_ERROR_WDBA_REJECTED, "ErrExeRejected"], + [Cr.NS_ERROR_WDBA_BUILD, "ErrBuild"], + ]).get(exitCode) ?? "ErrExeOther"; + + throw new WDBAError(exitCode, telemetryResult); + } + }, +}; + +XPCOMUtils.defineLazyServiceGetters(ShellServiceInternal, { + defaultAgent: ["@mozilla.org/default-agent;1", "nsIDefaultAgent"], + 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..4e73e9d022 --- /dev/null +++ b/browser/components/shell/WindowsDefaultBrowser.cpp @@ -0,0 +1,205 @@ +/* -*- 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" + +// This must be before any other includes that might include shlobj.h +#define INITGUID +#include <shlobj.h> + +#include <lm.h> +#include <shellapi.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; +} + +static bool IsAppRegistered(HKEY rootKey, const wchar_t* appRegName) { + const wchar_t* keyPath = L"Software\\RegisteredApplications"; + + DWORD size = sizeof(uint32_t); + LSTATUS ls = RegGetValueW(rootKey, keyPath, appRegName, RRF_RT_ANY, nullptr, + nullptr, &size); + return ls == ERROR_SUCCESS; +} + +static bool LaunchMsSettingsProtocol() { + mozilla::UniquePtr<wchar_t[]> params = nullptr; + if (mozilla::HasPackageIdentity()) { + mozilla::UniquePtr<wchar_t[]> packageFamilyName = + mozilla::GetPackageFamilyName(); + if (packageFamilyName) { + const wchar_t* paramFormat = + L"ms-settings:defaultapps?registeredAUMID=%s!App"; + int bufferSize = _scwprintf(paramFormat, packageFamilyName.get()); + ++bufferSize; // Extra byte for terminating null + params = mozilla::MakeUnique<wchar_t[]>(bufferSize); + _snwprintf_s(params.get(), bufferSize, _TRUNCATE, paramFormat, + packageFamilyName.get()); + } + } + if (!params) { + mozilla::UniquePtr<wchar_t[]> appRegName; + GetAppRegName(appRegName); + const wchar_t* paramFormat = + IsAppRegistered(HKEY_CURRENT_USER, appRegName.get()) || + !IsAppRegistered(HKEY_LOCAL_MACHINE, appRegName.get()) + ? L"ms-settings:defaultapps?registeredAppUser=%s" + : L"ms-settings:defaultapps?registeredAppMachine=%s"; + int bufferSize = _scwprintf(paramFormat, appRegName.get()); + ++bufferSize; // Extra byte for terminating null + params = mozilla::MakeUnique<wchar_t[]>(bufferSize); + _snwprintf_s(params.get(), bufferSize, _TRUNCATE, paramFormat, + appRegName.get()); + } + + SHELLEXECUTEINFOW seinfo = {sizeof(seinfo)}; + seinfo.lpFile = params.get(); + seinfo.nShow = SW_SHOWNORMAL; + return ShellExecuteExW(&seinfo); +} + +bool LaunchModernSettingsDialogDefaultApps() { + if (mozilla::IsWin11OrLater()) { + return LaunchMsSettingsProtocol(); + } + + if (!mozilla::IsWindows10BuildOrLater(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..baa3e7286e --- /dev/null +++ b/browser/components/shell/WindowsUserChoice.cpp @@ -0,0 +1,474 @@ +/* -*- 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 <appmodel.h> // for GetPackageFamilyName +#include <sddl.h> // for ConvertSidToStringSidW +#include <wincrypt.h> // for CryptoAPI base64 +#include <bcrypt.h> // for CNG MD5 +#include <winternl.h> // for NT_SUCCESS() + +#include "nsDebug.h" +#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; +} + +nsresult GetMsixProgId(const wchar_t* assoc, UniquePtr<wchar_t[]>& aProgId) { + // Retrieve the registry path to the package from registry path: + // clang-format off + // HKEY_CLASSES_ROOT\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\[Package Full Name]\App\Capabilities\[FileAssociations | URLAssociations]\[File | URL] + // clang-format on + + UINT32 pfnLen = 0; + LONG rv = GetCurrentPackageFullName(&pfnLen, nullptr); + NS_ENSURE_TRUE(rv != APPMODEL_ERROR_NO_PACKAGE, NS_ERROR_FAILURE); + + auto pfn = mozilla::MakeUnique<wchar_t[]>(pfnLen); + rv = GetCurrentPackageFullName(&pfnLen, pfn.get()); + NS_ENSURE_TRUE(rv == ERROR_SUCCESS, NS_ERROR_FAILURE); + + const wchar_t* assocSuffix; + if (assoc[0] == L'.') { + // File association. + assocSuffix = LR"(App\Capabilities\FileAssociations)"; + } else { + // URL association. + assocSuffix = LR"(App\Capabilities\URLAssociations)"; + } + + const wchar_t* assocPathFmt = + LR"(Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages\%s\%s)"; + int assocPathLen = _scwprintf(assocPathFmt, pfn.get(), assocSuffix); + assocPathLen += 1; // _scwprintf does not include the terminator + + auto assocPath = MakeUnique<wchar_t[]>(assocPathLen); + _snwprintf_s(assocPath.get(), assocPathLen, _TRUNCATE, assocPathFmt, + pfn.get(), assocSuffix); + + LSTATUS ls; + + // Retrieve the package association's ProgID, always in the form `AppX[32 hash + // characters]`. + const size_t appxProgIdLen = 37; + auto progId = MakeUnique<wchar_t[]>(appxProgIdLen); + DWORD progIdLen = appxProgIdLen * sizeof(wchar_t); + ls = ::RegGetValueW(HKEY_CLASSES_ROOT, assocPath.get(), assoc, RRF_RT_REG_SZ, + nullptr, (LPBYTE)progId.get(), &progIdLen); + if (ls != ERROR_SUCCESS) { + return NS_ERROR_WDBA_NO_PROGID; + } + + aProgId.swap(progId); + + return NS_OK; +} diff --git a/browser/components/shell/WindowsUserChoice.h b/browser/components/shell/WindowsUserChoice.h new file mode 100644 index 0000000000..ae83e093e4 --- /dev/null +++ b/browser/components/shell/WindowsUserChoice.h @@ -0,0 +1,118 @@ +/* -*- 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 "ErrorList.h" // for nsresult +#include "mozilla/UniquePtr.h" +#include "nsString.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); + +/* + * Get the ProgID registered by Windows for the given association. + * + * The MSIX `AppManifest.xml` declares supported protocols and file + * type associations. Upon installation, Windows generates + * corresponding ProgIDs for them, of the form `AppX*`. This function + * retrieves those generated ProgIDs (from the Windows registry). + * + * @return ProgID. + */ +nsresult GetMsixProgId(const wchar_t* assoc, + mozilla::UniquePtr<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..7448a3e076 --- /dev/null +++ b/browser/components/shell/content/setDesktopBackground.js @@ -0,0 +1,249 @@ +/* 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) { + // Hide span option on single monitor systems. + 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..57db807ac1 --- /dev/null +++ b/browser/components/shell/content/setDesktopBackground.xhtml @@ -0,0 +1,91 @@ +<?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/. + +<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="stylesheet" href="chrome://global/skin/global.css" /> + <html:link + rel="stylesheet" + href="chrome://browser/skin/setDesktopBackground.css" + /> + + <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..fe70623907 --- /dev/null +++ b/browser/components/shell/moz.build @@ -0,0 +1,89 @@ +# -*- 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.toml"] +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.toml"] + +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"] + +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..2349cf8cc2 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellDBusHelper.cpp @@ -0,0 +1,397 @@ +/* -*- 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; +} + +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(" "); + } + } +} + +// GetInitialResultSet :: (as) → (as) +// GetSubsearchResultSet :: (as,as) → (as) +void DBusHandleResultSet(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, bool aInitialSearch, + GDBusMethodInvocation* aReply) { + // Inital search has params (as), any following one has (as,as) and we want + // the second string array. + fprintf(stderr, "%s\n", g_variant_get_type_string(aParameters)); + RefPtr<GVariant> variant = dont_AddRef( + g_variant_get_child_value(aParameters, aInitialSearch ? 0 : 1)); + const char** stringArray = g_variant_get_strv(variant, nullptr); + if (!stringArray) { + g_dbus_method_invocation_return_error( + aReply, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Wrong params!"); + return; + } + + aSearchResult->SetReply(aReply); + nsAutoCString searchTerm; + ConcatArray(searchTerm, stringArray); + aSearchResult->SetSearchTerm(searchTerm.get()); + GetGNOMEShellHistoryService()->QueryHistory(aSearchResult); + // DBus reply will be send asynchronously by + // nsGNOMEShellHistorySearchResult::SendDBusSearchResultReply() + // when GetGNOMEShellHistoryService() has the results. + + g_free(stringArray); +} + +/* + "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(GVariantBuilder* aBuilder, GnomeHistoryIcon* aIcon) { + GVariantBuilder b; + g_variant_builder_init(&b, G_VARIANT_TYPE("(iiibiiay)")); + g_variant_builder_add_value(&b, g_variant_new_int32(aIcon->GetWidth())); + g_variant_builder_add_value(&b, g_variant_new_int32(aIcon->GetHeight())); + g_variant_builder_add_value(&b, g_variant_new_int32(aIcon->GetWidth() * 4)); + g_variant_builder_add_value(&b, g_variant_new_boolean(true)); + g_variant_builder_add_value(&b, g_variant_new_int32(8)); + g_variant_builder_add_value(&b, g_variant_new_int32(4)); + g_variant_builder_add_value( + &b, g_variant_new_fixed_array(G_VARIANT_TYPE("y"), aIcon->GetData(), + aIcon->GetWidth() * aIcon->GetHeight() * 4, + sizeof(char))); + g_variant_builder_add(aBuilder, "{sv}", "icon-data", + g_variant_builder_end(&b)); +} + +/* 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 already_AddRefed<GVariant> DBusAppendResultID( + nsGNOMEShellHistorySearchResult* aSearchResult, const char* aID) { + nsCOMPtr<nsINavHistoryContainerResultNode> container = + aSearchResult->GetSearchResultContainer(); + + int index = DBusGetIndexFromIDKey(aID); + nsCOMPtr<nsINavHistoryResultNode> child; + container->GetChild(index, getter_AddRefs(child)); + nsAutoCString title; + if (!child || NS_FAILED(child->GetTitle(title))) { + return nullptr; + } + + if (title.IsEmpty()) { + if (NS_FAILED(child->GetUri(title)) || title.IsEmpty()) { + return nullptr; + } + } + + GVariantBuilder b; + g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); + + const char* titleStr = title.get(); + g_variant_builder_add(&b, "{sv}", "id", g_variant_new_string(aID)); + g_variant_builder_add(&b, "{sv}", "name", g_variant_new_string(titleStr)); + + GnomeHistoryIcon* icon = aSearchResult->GetHistoryIcon(index); + if (icon) { + DBusAppendIcon(&b, icon); + } else { + g_variant_builder_add(&b, "{sv}", "gicon", + g_variant_new_string("text-html")); + } + return dont_AddRef(g_variant_ref_sink(g_variant_builder_end(&b))); +} + +// Search the web for: "searchTerm" to the DBUS reply. +static already_AddRefed<GVariant> DBusAppendSearchID(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 nullptr; + } + + GVariantBuilder b; + g_variant_builder_init(&b, G_VARIANT_TYPE("a{sv}")); + g_variant_builder_add(&b, "{sv}", "id", + g_variant_new_string(KEYWORD_SEARCH_STRING)); + + // Extract ssssss part from aID + nsAutoCString searchTerm(aID + KEYWORD_SEARCH_STRING_LEN + 1); + nsAutoCString gnomeSearchTitle; + if (GetGnomeSearchTitle(searchTerm.get(), gnomeSearchTitle)) { + g_variant_builder_add(&b, "{sv}", "name", + g_variant_new_string(gnomeSearchTitle.get())); + // TODO: When running on flatpak/snap we may need to use + // icon like org.mozilla.Firefox or so. + g_variant_builder_add(&b, "{sv}", "gicon", g_variant_new_string("firefox")); + } + + return dont_AddRef(g_variant_ref_sink(g_variant_builder_end(&b))); +} + +// GetResultMetas :: (as) → (aa{sv}) +void DBusHandleResultMetas( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, GDBusMethodInvocation* aReply) { + RefPtr<GVariant> variant = + dont_AddRef(g_variant_get_child_value(aParameters, 0)); + gsize elements; + const char** stringArray = g_variant_get_strv(variant, &elements); + if (!stringArray) { + g_dbus_method_invocation_return_error( + aReply, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Wrong params!"); + return; + } + + GVariantBuilder b; + g_variant_builder_init(&b, G_VARIANT_TYPE("aa{sv}")); + for (gsize i = 0; i < elements; i++) { + RefPtr<GVariant> value; + if (strncmp(stringArray[i], KEYWORD_SEARCH_STRING, + KEYWORD_SEARCH_STRING_LEN) == 0) { + value = DBusAppendSearchID(stringArray[i]); + } else { + value = DBusAppendResultID(aSearchResult, stringArray[i]); + } + if (value) { + g_variant_builder_add_value(&b, value); + } + } + + GVariant* v = g_variant_builder_end(&b); + g_dbus_method_invocation_return_value(aReply, g_variant_new_tuple(&v, 1)); + + g_free(stringArray); +} // 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); +} + +// ActivateResult :: (s,as,u) → () +void DBusActivateResult(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, GDBusMethodInvocation* aReply) { + const char* resultID; + + // aParameters is "(s,as,u)" type + RefPtr<GVariant> r = dont_AddRef(g_variant_get_child_value(aParameters, 0)); + if (!(resultID = g_variant_get_string(r, nullptr))) { + g_dbus_method_invocation_return_error( + aReply, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Wrong params!"); + return; + } + RefPtr<GVariant> t = dont_AddRef(g_variant_get_child_value(aParameters, 2)); + uint32_t timestamp = g_variant_get_uint32(t); + + ActivateResultID(aSearchResult, resultID, timestamp); + g_dbus_method_invocation_return_value(aReply, nullptr); +} + +// LaunchSearch :: (as,u) → () +void DBusLaunchSearch(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, GDBusMethodInvocation* aReply) { + RefPtr<GVariant> variant = + dont_AddRef(g_variant_get_child_value(aParameters, 1)); + if (!variant) { + g_dbus_method_invocation_return_error( + aReply, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Wrong params!"); + return; + } + DBusLaunchWithAllResults(aSearchResult, g_variant_get_uint32(variant)); + g_dbus_method_invocation_return_value(aReply, nullptr); +} + +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..ac879ef992 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellDBusHelper.h @@ -0,0 +1,35 @@ +/* -*- 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 <gio/gio.h> +#include "nsINavHistoryService.h" + +#define MAX_SEARCH_RESULTS_NUM 9 +#define KEYWORD_SEARCH_STRING "special:search" +#define KEYWORD_SEARCH_STRING_LEN 14 + +class nsGNOMEShellHistorySearchResult; + +void DBusHandleResultSet(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, bool aInitialSearch, + GDBusMethodInvocation* aReply); +void DBusHandleResultMetas( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, GDBusMethodInvocation* aReply); +void DBusActivateResult(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, GDBusMethodInvocation* aReply); +void DBusLaunchSearch(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + GVariant* aParameters, GDBusMethodInvocation* aReply); +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..b894254fce --- /dev/null +++ b/browser/components/shell/nsGNOMEShellSearchProvider.cpp @@ -0,0 +1,508 @@ +/* -*- 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 "mozilla/GUniquePtr.h" +#include "mozilla/UniquePtrExtensions.h" + +#include "imgIContainer.h" +#include "imgITools.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +// Mozilla has old GIO version in build roots +#define G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE GBusNameOwnerFlags(1 << 2) + +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.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"; + +class AsyncFaviconDataReady final : public nsIFaviconDataCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIFAVICONDATACALLBACK + + AsyncFaviconDataReady(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + int aIconIndex, int aTimeStamp) + : mSearchResult(std::move(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; +} + +void nsGNOMEShellSearchProvider::HandleSearchResultSet( + GVariant* aParameters, GDBusMethodInvocation* aInvocation, + 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(). + DBusHandleResultSet(newSearch.forget(), aParameters, aInitialSearch, + aInvocation); +} + +void nsGNOMEShellSearchProvider::HandleResultMetas( + GVariant* aParameters, GDBusMethodInvocation* aInvocation) { + if (mSearchResult) { + DBusHandleResultMetas(mSearchResult, aParameters, aInvocation); + } +} + +void nsGNOMEShellSearchProvider::ActivateResult( + GVariant* aParameters, GDBusMethodInvocation* aInvocation) { + if (mSearchResult) { + DBusActivateResult(mSearchResult, aParameters, aInvocation); + } +} + +void nsGNOMEShellSearchProvider::LaunchSearch( + GVariant* aParameters, GDBusMethodInvocation* aInvocation) { + if (mSearchResult) { + DBusLaunchSearch(mSearchResult, aParameters, aInvocation); + } +} + +static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aMethodName, GVariant* aParameters, + GDBusMethodInvocation* aInvocation, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + + if (strcmp("org.gnome.Shell.SearchProvider2", aInterfaceName) == 0) { + if (strcmp("GetInitialResultSet", aMethodName) == 0) { + static_cast<nsGNOMEShellSearchProvider*>(aUserData) + ->HandleSearchResultSet(aParameters, aInvocation, + /* aInitialSearch */ true); + } else if (strcmp("GetSubsearchResultSet", aMethodName) == 0) { + static_cast<nsGNOMEShellSearchProvider*>(aUserData) + ->HandleSearchResultSet(aParameters, aInvocation, + /* aInitialSearch */ false); + } else if (strcmp("GetResultMetas", aMethodName) == 0) { + static_cast<nsGNOMEShellSearchProvider*>(aUserData)->HandleResultMetas( + aParameters, aInvocation); + } else if (strcmp("ActivateResult", aMethodName) == 0) { + static_cast<nsGNOMEShellSearchProvider*>(aUserData)->ActivateResult( + aParameters, aInvocation); + } else if (strcmp("LaunchSearch", aMethodName) == 0) { + static_cast<nsGNOMEShellSearchProvider*>(aUserData)->LaunchSearch( + aParameters, aInvocation); + } else { + g_warning( + "nsGNOMEShellSearchProvider: HandleMethodCall() wrong method %s", + aMethodName); + } + } +} + +static GVariant* HandleGetProperty(GDBusConnection* aConnection, + const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aPropertyName, GError** aError, + gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s:%s setting is not supported", aInterfaceName, aPropertyName); + return nullptr; +} + +static gboolean HandleSetProperty(GDBusConnection* aConnection, + const gchar* aSender, + const gchar* aObjectPath, + const gchar* aInterfaceName, + const gchar* aPropertyName, GVariant* aValue, + GError** aError, gpointer aUserData) { + MOZ_ASSERT(aUserData); + MOZ_ASSERT(NS_IsMainThread()); + g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED, + "%s:%s setting is not supported", aInterfaceName, aPropertyName); + return false; +} + +static const GDBusInterfaceVTable gInterfaceVTable = { + HandleMethodCall, HandleGetProperty, HandleSetProperty}; + +void nsGNOMEShellSearchProvider::OnBusAcquired(GDBusConnection* aConnection) { + GUniquePtr<GError> error; + mIntrospectionData = dont_AddRef(g_dbus_node_info_new_for_xml( + introspect_template, getter_Transfers(error))); + if (!mIntrospectionData) { + g_warning( + "nsGNOMEShellSearchProvider: g_dbus_node_info_new_for_xml() failed! %s", + error->message); + return; + } + + mRegistrationId = g_dbus_connection_register_object( + aConnection, GetDBusObjectPath(), mIntrospectionData->interfaces[0], + &gInterfaceVTable, this, /* user_data */ + nullptr, /* user_data_free_func */ + getter_Transfers(error)); /* GError** */ + + if (mRegistrationId == 0) { + g_warning( + "nsGNOMEShellSearchProvider: g_dbus_connection_register_object() " + "failed! %s", + error->message); + return; + } +} + +void nsGNOMEShellSearchProvider::OnNameAcquired(GDBusConnection* aConnection) { + mConnection = aConnection; +} + +void nsGNOMEShellSearchProvider::OnNameLost(GDBusConnection* aConnection) { + mConnection = nullptr; + if (!mRegistrationId) { + return; + } + if (g_dbus_connection_unregister_object(aConnection, mRegistrationId)) { + mRegistrationId = 0; + } +} + +nsresult nsGNOMEShellSearchProvider::Startup() { + if (mDBusID) { + // We're already connected so we don't need to reconnect + return NS_ERROR_ALREADY_INITIALIZED; + } + + mDBusID = g_bus_own_name( + G_BUS_TYPE_SESSION, GetDBusBusName(), G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, + [](GDBusConnection* aConnection, const gchar*, + gpointer aUserData) -> void { + static_cast<nsGNOMEShellSearchProvider*>(aUserData)->OnBusAcquired( + aConnection); + }, + [](GDBusConnection* aConnection, const gchar*, + gpointer aUserData) -> void { + static_cast<nsGNOMEShellSearchProvider*>(aUserData)->OnNameAcquired( + aConnection); + }, + [](GDBusConnection* aConnection, const gchar*, + gpointer aUserData) -> void { + static_cast<nsGNOMEShellSearchProvider*>(aUserData)->OnNameLost( + aConnection); + }, + this, nullptr); + + if (!mDBusID) { + g_warning("nsGNOMEShellSearchProvider: g_bus_own_name() failed!"); + return NS_ERROR_FAILURE; + } + + mSearchResultTimeStamp = 0; + return NS_OK; +} + +void nsGNOMEShellSearchProvider::Shutdown() { + OnNameLost(mConnection); + if (mDBusID) { + g_bus_unown_name(mDBusID); + mDBusID = 0; + } + mIntrospectionData = 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()); +} + +// Send (as) rearch result reply +void nsGNOMEShellHistorySearchResult::HandleSearchResultReply() { + MOZ_ASSERT(mReply); + + GVariantBuilder b; + g_variant_builder_init(&b, G_VARIANT_TYPE("as")); + + uint32_t childCount = 0; + nsresult rv = mHistResultContainer->GetChildCount(&childCount); + 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; + rv = 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); + + g_variant_builder_add(&b, "s", idKey.get()); + } + } + + nsPrintfCString searchString("%s:%s", KEYWORD_SEARCH_STRING, + mSearchTerm.get()); + g_variant_builder_add(&b, "s", searchString.get()); + + GVariant* v = g_variant_builder_end(&b); + g_dbus_method_invocation_return_value(mReply, g_variant_new_tuple(&v, 1)); + 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..ce215183fc --- /dev/null +++ b/browser/components/shell/nsGNOMEShellSearchProvider.h @@ -0,0 +1,153 @@ +/* -*- 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 <gio/gio.h> + +#include "mozilla/RefPtr.h" +#include "mozilla/GRefPtr.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, + GDBusConnection* aConnection, int aTimeStamp) + : mSearchProvider(aSearchProvider), + mConnection(aConnection), + mTimeStamp(aTimeStamp){}; + + void SetReply(RefPtr<GDBusMethodInvocation> aReply) { + mReply = std::move(aReply); + } + void SetSearchTerm(const char* aSearchTerm) { + mSearchTerm = nsAutoCString(aSearchTerm); + } + GDBusConnection* 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; + RefPtr<GDBusMethodInvocation> mReply; + GDBusConnection* mConnection = nullptr; + 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(); + + void UnregisterDBusInterface(GDBusConnection* aConnection); + + bool SetSearchResult(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult); + + void HandleSearchResultSet(GVariant* aParameters, + GDBusMethodInvocation* aInvocation, + bool aInitialSearch); + void HandleResultMetas(GVariant* aParameters, + GDBusMethodInvocation* aInvocation); + void ActivateResult(GVariant* aParameters, + GDBusMethodInvocation* aInvocation); + void LaunchSearch(GVariant* aParameters, GDBusMethodInvocation* aInvocation); + + void OnBusAcquired(GDBusConnection* aConnection); + void OnNameAcquired(GDBusConnection* aConnection); + void OnNameLost(GDBusConnection* aConnection); + + private: + // The connection is owned by DBus library + uint mDBusID = 0; + uint mRegistrationId = 0; + GDBusConnection* mConnection = nullptr; + RefPtr<GDBusNodeInfo> mIntrospectionData; + + 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..987e8ae78b --- /dev/null +++ b/browser/components/shell/nsGNOMEShellService.cpp @@ -0,0 +1,502 @@ +/* -*- 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 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) { + appInfo->SetAsDefaultForURIScheme( + nsDependentCString(appProtocols[i].name)); + } + + // set handler for .html and xhtml files and MIME types: + // 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..414cab5ca8 --- /dev/null +++ b/browser/components/shell/nsIMacShellService.idl @@ -0,0 +1,42 @@ +/* -*- 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); + + /* + * Searches for any available handlers for the given protocol and returns them in an + * array in the form [[displayName1, handlerPath1], [displayName2, handlerPath2], etc.] + * Can return an empty array if no handlers are found. + * + * @param protocol The protocol to search for handlers for (e.g. "http", "mailto" etc.) + * @return The full list of handler paths and names. + * + * @throws NS_ERROR_ILLEGAL_VALUE if the protocol cannot be resolved as a string. + * @throws NS_ERROR_MALFORMED_URI if the protocol cannot be resolved to a URI. + * @throws NS_ERROR_NOT_AVAILABLE if a list of handlers fails to generate. + */ + + Array<Array<AString> > getAvailableApplicationsForProtocol(in ACString protocol); +}; diff --git a/browser/components/shell/nsIShellService.idl b/browser/components/shell/nsIShellService.idl new file mode 100644 index 0000000000..c52db439b8 --- /dev/null +++ b/browser/components/shell/nsIShellService.idl @@ -0,0 +1,66 @@ +/* -*- 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 aForAllUsers Whether or not Firefox should attempt + * to become the default browser for all + * users on a multi-user system. + */ + void setDefaultBrowser(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..13c824f39c --- /dev/null +++ b/browser/components/shell/nsIWindowsShellService.idl @@ -0,0 +1,288 @@ +/* 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); + + /* + * Searches the %USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup + * folder and returns an array with the path of all shortcuts with a target matching the + * current Firefox install location. The AUMID isn't required here as we are only looking + * for the currently running binary, whether that's firefox.exe or the private browsing + * proxy executable. + * + * It is possible to return an empty array if no shortcuts are found. + * + * @return An array of paths for all launch on login shortcuts.s + * + * @throws NS_ERROR_ABORT + * if instance cannot be created. + * @throws NS_ERROR_FILE_NOT_FOUND + * if %USERPROFILE%\AppData\Roaming\ cannot be opened. + * @throws NS_ERROR_FAILURE + * if the executable file cannot be found. + * @throws NS_ERROR_FILE_UNRECOGNIZED_PATH + * if the executable file cannot be converted into a string. + */ + + Array<AString> getLaunchOnLoginShortcuts(); + + /* + * 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 will definitely fail on an OS before Windows 10 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); + + /* + * Similar to createShortcut except it removes most of the checking in that + * function that ensures we are pinning a Firefox executable instead allowing + * any shortcut to be pinned. + * + * This function should not be called unless it is certain that it's + * necessary given how few checks there are within. + * @param aShortcutPath + * A path to the .lnk file that should be pinned to the taskbar. + * @throws NS_ERROR_FAILURE + * If the COM service could not be initialized + * @throws NS_ERROR_FILE_NOT_FOUND + * If aShortcutPath cannot be found + * @throws NS_ERROR_NOT_AVAILABLE + * If the taskbar pinning service cannot be initialized + * @throws NS_ERROR_FILE_ACCESS_DENIED + * If the taskbar pins cannot be modified + */ + void pinShortcutToTaskbar(in AString aShortcutPath); + + /* + * This function is a counterpart to pinShortcutToTaskbar and allows + * the unpinning of any shortcut, including non-Firefox executables, + * without the checks of createShortcut. + * + * This function should not be called unless it is certain that it's + * necessary given how few checks there are within. + * @param aShortcutPath + * A path to the .lnk file that should be pinned to the taskbar. + * @throws NS_ERROR_FAILURE + * If the COM service could not be initialized + * @throws NS_ERROR_FILE_NOT_FOUND + * If aShortcutPath cannot be found + * @throws NS_ERROR_NOT_AVAILABLE + * If the taskbar pinning service cannot be initialized + * @throws NS_ERROR_FILE_ACCESS_DENIED + * If the taskbar pins cannot be modified + */ + void unpinShortcutFromTaskbar(in AString aShortcutPath); + + + /** + * Gets the full path of a taskbar tab shortcut. + * + * @param aShortcutName + * The name of the taskbar tab shortcut, excluding the file extension. + * @throws NS_ERROR_FAILURE + * If the user token cannot be found. + * @throws NS_ERROR_ABORT + * If converting the sid to a string fails. + * + * @return The full path of the taskbar tab shortcut + */ + AString getTaskbarTabShortcutPath(in AString aShortcutName); + + /* + * Searches the %USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start + * Menu\Programs folder and returns an array with the path of all + * shortcuts with a target matching the current Firefox install location + * and the -taskbar-tab argument. + * + * It is possible to return an empty array if no shortcuts are found. + * + * @return An array of paths for all taskbar tab shortcuts. + * + * @throws NS_ERROR_NOT_IMPLEMENTED + * if run on a MinGW compilation + * @throws NS_ERROR_ABORT + * if instance cannot be created. + * @throws NS_ERROR_FILE_NOT_FOUND + * if %USERPROFILE%\AppData\Roaming\ cannot be opened. + * @throws NS_ERROR_FAILURE + * if the executable file cannot be found. + * @throws NS_ERROR_FILE_UNRECOGNIZED_PATH + * if the executable file cannot be converted into a string. + */ + Array<AString> getTaskbarTabPins(); + + /* + * 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..aeced1db8d --- /dev/null +++ b/browser/components/shell/nsMacShellService.cpp @@ -0,0 +1,346 @@ +/* -*- 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 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 (::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; +} + +nsString ConvertCFStringToNSString(CFStringRef aSrc) { + nsString aDest; + auto len = ::CFStringGetLength(aSrc); + aDest.SetLength(len); + ::CFStringGetCharacters(aSrc, ::CFRangeMake(0, len), + (UniChar*)aDest.BeginWriting()); + return aDest; +} + +NS_IMETHODIMP +nsMacShellService::GetAvailableApplicationsForProtocol( + const nsACString& protocol, nsTArray<nsTArray<nsString>>& aHandlerPaths) { + class CFTypeRefAutoDeleter { + public: + explicit CFTypeRefAutoDeleter(CFTypeRef ref) : mRef(ref) {} + ~CFTypeRefAutoDeleter() { + if (mRef != NULL) ::CFRelease(mRef); + } + + private: + CFTypeRef mRef; + }; + + aHandlerPaths.Clear(); + nsCString protocolSep = protocol + "://"_ns; + CFStringRef cfProtocol = ::CFStringCreateWithBytes( + kCFAllocatorDefault, (const UInt8*)protocolSep.BeginReading(), + protocolSep.Length(), kCFStringEncodingUTF8, false); + CFTypeRefAutoDeleter cfProtocolAuto((CFTypeRef)cfProtocol); + if (cfProtocol == NULL) { + return NS_ERROR_ILLEGAL_VALUE; + } + CFURLRef protocolURL = + ::CFURLCreateWithString(kCFAllocatorDefault, cfProtocol, NULL); + CFTypeRefAutoDeleter cfProtocolURLAuto((CFTypeRef)protocolURL); + if (protocolURL == NULL) { + return NS_ERROR_MALFORMED_URI; + } + CFArrayRef appURLs = ::LSCopyApplicationURLsForURL(protocolURL, kLSRolesAll); + CFTypeRefAutoDeleter cfAppURLsAuto((CFTypeRef)appURLs); + if (appURLs == NULL) { + return NS_ERROR_NOT_AVAILABLE; + } + for (CFIndex i = 0; i < ::CFArrayGetCount(appURLs); i++) { + CFURLRef appURL = (CFURLRef)::CFArrayGetValueAtIndex(appURLs, i); + CFBundleRef appBundle = ::CFBundleCreate(kCFAllocatorDefault, appURL); + CFTypeRefAutoDeleter cfAppBundleAuto((CFTypeRef)appBundle); + if (appBundle == NULL) { + continue; + } + CFDictionaryRef appInfo = ::CFBundleGetInfoDictionary(appBundle); + if (appInfo == NULL) { + continue; + } + CFStringRef displayName = + (CFStringRef)::CFDictionaryGetValue(appInfo, kCFBundleNameKey); + if (displayName == NULL) { + continue; + } + CFStringRef appPath = ::CFURLGetString(appURL); + nsTArray<nsString> handlerPath = {ConvertCFStringToNSString(displayName), + ConvertCFStringToNSString(appPath)}; + aHandlerPaths.AppendElement(handlerPath.Clone()); + } + return NS_OK; +} 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..86c7694bf8 --- /dev/null +++ b/browser/components/shell/nsWindowsShellService.cpp @@ -0,0 +1,1954 @@ +/* -*- 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 "nsIMIMEService.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 "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 <windows.h> +#include <shellapi.h> +#include <strsafe.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 <knownfolders.h> +#include "WinUtils.h" +#include "mozilla/widget/WinTaskbar.h" + +#include <mbstring.h> + +#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 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, + ®isteredApp); + 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, + ®isteredApp); + 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; +} + +NS_IMETHODIMP +nsWindowsShellService::CheckAllProgIDsExist(bool* aResult) { + *aResult = false; + nsAutoString aumid; + if (!mozilla::widget::WinTaskbar::GetAppUserModelID(aumid)) { + return NS_OK; + } + + if (widget::WinUtils::HasPackageIdentity()) { + UniquePtr<wchar_t[]> extraProgID; + nsresult rv; + bool result = true; + + // "FirefoxURL". + rv = GetMsixProgId(L"https", extraProgID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + result = result && CheckProgIDExists(extraProgID.get()); + + // "FirefoxHTML". + rv = GetMsixProgId(L".htm", extraProgID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + result = result && CheckProgIDExists(extraProgID.get()); + + // "FirefoxPDF". + rv = GetMsixProgId(L".pdf", extraProgID); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + result = result && CheckProgIDExists(extraProgID.get()); + + *aResult = result; + } else { + *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; +} + +NS_IMETHODIMP +nsWindowsShellService::SetDefaultBrowser(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)) { + rv = LaunchModernSettingsDialogDefaultApps(); + // 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; +} + +NS_IMETHODIMP +nsWindowsShellService::GetLaunchOnLoginShortcuts( + nsTArray<nsString>& aShortcutPaths) { + aShortcutPaths.Clear(); + + // Get AppData\\Roaming folder using a known folder ID + RefPtr<IKnownFolderManager> fManager; + RefPtr<IKnownFolder> roamingAppData; + LPWSTR roamingAppDataW; + nsString roamingAppDataNS; + HRESULT hr = + CoCreateInstance(CLSID_KnownFolderManager, nullptr, CLSCTX_INPROC_SERVER, + IID_IKnownFolderManager, getter_AddRefs(fManager)); + if (FAILED(hr)) { + return NS_ERROR_ABORT; + } + fManager->GetFolder(FOLDERID_RoamingAppData, + roamingAppData.StartAssignment()); + hr = roamingAppData->GetPath(0, &roamingAppDataW); + if (FAILED(hr)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + // Append startup folder to AppData\\Roaming + roamingAppDataNS.Assign(roamingAppDataW); + CoTaskMemFree(roamingAppDataW); + nsString startupFolder = + roamingAppDataNS + + u"\\Microsoft\\Windows\\Start Menu\\Programs\\Startup"_ns; + nsString startupFolderWildcard = startupFolder + u"\\*.lnk"_ns; + + // Get known path for binary file for later comparison with shortcuts. + // Returns lowercase file path which should be fine for Windows as all + // directories and files are case-insensitive by default. + RefPtr<nsIFile> binFile; + nsString binPath; + nsresult rv = XRE_GetBinaryPath(binFile.StartAssignment()); + if (FAILED(rv)) { + return NS_ERROR_FAILURE; + } + rv = binFile->GetPath(binPath); + if (FAILED(rv)) { + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + } + + // Check for if first file exists with a shortcut extension (.lnk) + WIN32_FIND_DATAW ffd; + HANDLE fileHandle = INVALID_HANDLE_VALUE; + fileHandle = FindFirstFileW(startupFolderWildcard.get(), &ffd); + if (fileHandle == INVALID_HANDLE_VALUE) { + // This means that no files were found in the folder which + // doesn't imply an error. Most of the time the user won't + // have any shortcuts here. + return NS_OK; + } + + do { + // Extract shortcut target path from every + // shortcut in the startup folder. + nsString fileName(ffd.cFileName); + RefPtr<IShellLinkW> link; + RefPtr<IPersistFile> ppf; + nsString target; + target.SetLength(MAX_PATH); + CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, getter_AddRefs(link)); + hr = link->QueryInterface(IID_IPersistFile, getter_AddRefs(ppf)); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + nsString filePath = startupFolder + u"\\"_ns + fileName; + hr = ppf->Load(filePath.get(), STGM_READ); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + hr = link->GetPath(target.get(), MAX_PATH, nullptr, 0); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + + // If shortcut target matches known binary file value + // then add the path to the shortcut as a valid + // startup shortcut. This has to be a substring search as + // the user could have added unknown command line arguments + // to the shortcut. + if (_wcsnicmp(target.get(), binPath.get(), binPath.Length()) == 0) { + aShortcutPaths.AppendElement(filePath); + } + } while (FindNextFile(fileHandle, &ffd) != 0); + FindClose(fileHandle); + 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 bool IsCurrentAppPinnedToTaskbarSync(const nsAString& 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; +} + +static nsresult ManageShortcutTaskbarPins(bool aCheckOnly, bool aPinType, + const nsAString& 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(nsString(aShortcutPath).get())); + if (NS_WARN_IF(!path)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + IPinnedList3* pinnedList = nullptr; + HRESULT hr = CoCreateInstance(CLSID_TaskbandPin, NULL, CLSCTX_INPROC_SERVER, + IID_IPinnedList3, (void**)&pinnedList); + if (FAILED(hr) || !pinnedList) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!aCheckOnly) { + hr = pinnedList->vtbl->Modify(pinnedList, aPinType ? NULL : path.get(), + aPinType ? path.get() : NULL, PLMC_INT_MAX); + } + + pinnedList->vtbl->Release(pinnedList); + + if (FAILED(hr)) { + return NS_ERROR_FILE_ACCESS_DENIED; + } + return NS_OK; +} + +NS_IMETHODIMP +nsWindowsShellService::PinShortcutToTaskbar(const nsAString& aShortcutPath) { + const bool pinType = true; // true means pin + const bool runInTestMode = false; + return ManageShortcutTaskbarPins(runInTestMode, pinType, aShortcutPath); +} + +NS_IMETHODIMP +nsWindowsShellService::UnpinShortcutFromTaskbar( + const nsAString& aShortcutPath) { + const bool pinType = false; // false means unpin + const bool runInTestMode = false; + return ManageShortcutTaskbarPins(runInTestMode, pinType, aShortcutPath); +} + +// Ensure that the supplied name doesn't have invalid characters. +static void ValidateFilename(nsAString& aFilename) { + nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1"); + if (NS_WARN_IF(!mimeService)) { + aFilename.Truncate(); + return; + } + + uint32_t flags = nsIMIMEService::VALIDATE_SANITIZE_ONLY | + nsIMIMEService::VALIDATE_DONT_COLLAPSE_WHITESPACE; + + nsAutoString outFilename; + mimeService->ValidateFileNameForSaving(aFilename, EmptyCString(), flags, + outFilename); + aFilename = outFilename; +} + +NS_IMETHODIMP +nsWindowsShellService::GetTaskbarTabShortcutPath(const nsAString& aShortcutName, + nsAString& aRetPath) { + nsAutoString sanitizedShortcutName(aShortcutName); + ValidateFilename(sanitizedShortcutName); + if (sanitizedShortcutName != aShortcutName) { + return NS_ERROR_FILE_INVALID_PATH; + } + + // The taskbar tab shortcut will always be in + // %APPDATA%\Microsoft\Windows\Start Menu\Programs + RefPtr<IKnownFolderManager> fManager; + RefPtr<IKnownFolder> progFolder; + LPWSTR progFolderW; + nsString progFolderNS; + HRESULT hr = + CoCreateInstance(CLSID_KnownFolderManager, nullptr, CLSCTX_INPROC_SERVER, + IID_IKnownFolderManager, getter_AddRefs(fManager)); + if (NS_WARN_IF(FAILED(hr))) { + return NS_ERROR_ABORT; + } + fManager->GetFolder(FOLDERID_Programs, progFolder.StartAssignment()); + hr = progFolder->GetPath(0, &progFolderW); + if (FAILED(hr)) { + return NS_ERROR_FILE_NOT_FOUND; + } + progFolderNS.Assign(progFolderW); + aRetPath = progFolderNS + u"\\"_ns + aShortcutName + u".lnk"_ns; + return NS_OK; +} + +NS_IMETHODIMP +nsWindowsShellService::GetTaskbarTabPins(nsTArray<nsString>& aShortcutPaths) { +#ifdef __MINGW32__ + return NS_ERROR_NOT_IMPLEMENTED; +#else + aShortcutPaths.Clear(); + + // Get AppData\\Roaming folder using a known folder ID + RefPtr<IKnownFolderManager> fManager; + RefPtr<IKnownFolder> roamingAppData; + LPWSTR roamingAppDataW; + nsString roamingAppDataNS; + HRESULT hr = + CoCreateInstance(CLSID_KnownFolderManager, nullptr, CLSCTX_INPROC_SERVER, + IID_IKnownFolderManager, getter_AddRefs(fManager)); + if (NS_WARN_IF(FAILED(hr))) { + return NS_ERROR_ABORT; + } + fManager->GetFolder(FOLDERID_RoamingAppData, + roamingAppData.StartAssignment()); + hr = roamingAppData->GetPath(0, &roamingAppDataW); + if (FAILED(hr)) { + return NS_ERROR_FILE_NOT_FOUND; + } + + // Append taskbar pins folder to AppData\\Roaming + roamingAppDataNS.Assign(roamingAppDataW); + CoTaskMemFree(roamingAppDataW); + nsString taskbarFolder = + roamingAppDataNS + u"\\Microsoft\\Windows\\Start Menu\\Programs"_ns; + nsString taskbarFolderWildcard = taskbarFolder + u"\\*.lnk"_ns; + + // Get known path for binary file for later comparison with shortcuts. + // Returns lowercase file path which should be fine for Windows as all + // directories and files are case-insensitive by default. + RefPtr<nsIFile> binFile; + nsString binPath; + nsresult rv = XRE_GetBinaryPath(binFile.StartAssignment()); + if (NS_WARN_IF(FAILED(rv))) { + return NS_ERROR_FAILURE; + } + rv = binFile->GetPath(binPath); + if (NS_WARN_IF(FAILED(rv))) { + return NS_ERROR_FILE_UNRECOGNIZED_PATH; + } + + // Check for if first file exists with a shortcut extension (.lnk) + WIN32_FIND_DATAW ffd; + HANDLE fileHandle = INVALID_HANDLE_VALUE; + fileHandle = FindFirstFileW(taskbarFolderWildcard.get(), &ffd); + if (fileHandle == INVALID_HANDLE_VALUE) { + // This means that no files were found in the folder which + // doesn't imply an error. + return NS_OK; + } + + do { + // Extract shortcut target path from every + // shortcut in the taskbar pins folder. + nsString fileName(ffd.cFileName); + RefPtr<IShellLinkW> link; + RefPtr<IPropertyStore> pps; + nsString target; + target.SetLength(MAX_PATH); + hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, getter_AddRefs(link)); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + nsString filePath = taskbarFolder + u"\\"_ns + fileName; + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + + // After loading shortcut, search through arguments to find if + // it is a taskbar tab shortcut. + hr = SHGetPropertyStoreFromParsingName(filePath.get(), nullptr, + GPS_READWRITE, IID_IPropertyStore, + getter_AddRefs(pps)); + if (NS_WARN_IF(FAILED(hr)) || pps == nullptr) { + continue; + } + PROPVARIANT propVar; + PropVariantInit(&propVar); + auto cleanupPropVariant = + MakeScopeExit([&] { PropVariantClear(&propVar); }); + // Get the PKEY_Link_Arguments property + hr = pps->GetValue(PKEY_Link_Arguments, &propVar); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + // Check if the argument matches + if (!(propVar.vt == VT_LPWSTR && propVar.pwszVal != nullptr && + wcsstr(propVar.pwszVal, L"-taskbar-tab") != nullptr)) { + continue; + } + + hr = link->GetPath(target.get(), MAX_PATH, nullptr, 0); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + + // If shortcut target matches known binary file value + // then add the path to the shortcut as a valid + // shortcut. This has to be a substring search as + // the user could have added unknown command line arguments + // to the shortcut. + if (_wcsnicmp(target.get(), binPath.get(), binPath.Length()) == 0) { + aShortcutPaths.AppendElement(filePath); + } + } while (FindNextFile(fileHandle, &ffd) != 0); + FindClose(fileHandle); + return NS_OK; +#endif +} + +static nsresult PinCurrentAppToTaskbarWin10(bool aCheckOnly, + const nsAString& aAppUserModelId, + nsAutoString aShortcutPath) { + // The behavior here is identical if we're only checking or if we try to pin + // but the app is already pinned so we update the variable accordingly. + if (!aCheckOnly) { + aCheckOnly = IsCurrentAppPinnedToTaskbarSync(aAppUserModelId); + } + const bool pinType = true; // true means pin + return ManageShortcutTaskbarPins(aCheckOnly, pinType, aShortcutPath); +} + +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; + } + } + + return PinCurrentAppToTaskbarWin10(aCheckOnly, aAppUserModelId, 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 (!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); +} + +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..8d7ebee66b --- /dev/null +++ b/browser/components/shell/nsWindowsShellService.h @@ -0,0 +1,35 @@ +/* -*- 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 LaunchModernSettingsDialogDefaultApps(); +}; + +#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..459e012f2a --- /dev/null +++ b/browser/components/shell/search-provider-files/README @@ -0,0 +1,24 @@ +In order to get gnome shell search provider registered and active +you need to install the org.mozilla.firefox.search-provider.ini, +org.mozilla.firefox.SearchProvider.service 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/org.mozilla.firefox.search-provider.ini +/usr/share/dbus-1/services/org.mozilla.firefox.SearchProvider.service +/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 org.mozilla.firefox.search-provider.ini +according your actual file at /usr/share/applications. + +org.mozilla.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. + +org.mozilla.firefox.SearchProvider.service file makes DBus search provider service +activatable. Without it thw service is broken, see mzbz#1851393. + +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.desktop b/browser/components/shell/search-provider-files/firefox.desktop new file mode 100644 index 0000000000..575290acdd --- /dev/null +++ b/browser/components/shell/search-provider-files/firefox.desktop @@ -0,0 +1,274 @@ +[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; +DBusActivatable=true + +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]=نْو پرایوٹ وینڈو& +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/search-provider-files/org.mozilla.firefox.SearchProvider.service b/browser/components/shell/search-provider-files/org.mozilla.firefox.SearchProvider.service new file mode 100644 index 0000000000..967b990cc0 --- /dev/null +++ b/browser/components/shell/search-provider-files/org.mozilla.firefox.SearchProvider.service @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.mozilla.firefox.SearchProvider +Exec=/usr/bin/firefox diff --git a/browser/components/shell/search-provider-files/org.mozilla.firefox.search-provider.ini b/browser/components/shell/search-provider-files/org.mozilla.firefox.search-provider.ini new file mode 100644 index 0000000000..b2a0460948 --- /dev/null +++ b/browser/components/shell/search-provider-files/org.mozilla.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/test/browser.toml b/browser/components/shell/test/browser.toml new file mode 100644 index 0000000000..a15747d5eb --- /dev/null +++ b/browser/components/shell/test/browser.toml @@ -0,0 +1,95 @@ +[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_pinning.js"] +run-if = ["os == 'win'"] + +["browser_setDefaultBrowser.js"] + +["browser_setDefaultPDFHandler.js"] +run-if = ["os == 'win'"] + +["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..dbc66e2864 --- /dev/null +++ b/browser/components/shell/test/browser_633221.js @@ -0,0 +1,11 @@ +function test() { + ShellService.setDefaultBrowser(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_pinning.js b/browser/components/shell/test/browser_pinning.js new file mode 100644 index 0000000000..efdbc36239 --- /dev/null +++ b/browser/components/shell/test/browser_pinning.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_getTaskbarTabShortcutPath() { + // no exception generated for the following + ShellService.getTaskbarTabShortcutPath("working"); + ShellService.getTaskbarTabShortcutPath("workingPath.ink"); + + // confirm that multiple spaces aren't collapsed + ShellService.getTaskbarTabShortcutPath("working path.ink"); + + const invalidFilenames = [ + // These are reserved characters that can't be in names + "?", + ":", + "<", + ">", + "|", + '"', + "/", + "*", + "\t", + "\r", + "\n", + + // No path manipulation allowed + "..\\something", + ".\\something", + ".something", + + // Windows doesn't allow filenames ending in period or a space + "something.", + "something ", + + // The following are special reserved names + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + ]; + + for (const invalidFilename of invalidFilenames) { + Assert.throws( + () => { + ShellService.getTaskbarTabShortcutPath(invalidFilename); + }, + /NS_ERROR_FILE_INVALID_PATH/, + invalidFilename + + " is an invalid filename; getTaskbarTabShortcutPath should have failed with it as a parameter." + ); + } +}); diff --git a/browser/components/shell/test/browser_setDefaultBrowser.js b/browser/components/shell/test/browser_setDefaultBrowser.js new file mode 100644 index 0000000000..9a927d15b0 --- /dev/null +++ b/browser/components/shell/test/browser_setDefaultBrowser.js @@ -0,0 +1,142 @@ +/* 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 setDefaultBrowserUserChoiceStub = async () => { + throw Components.Exception("", Cr.NS_ERROR_WDBA_NO_PROGID); +}; + +const defaultAgentStub = sinon + .stub(ShellService, "defaultAgent") + .value({ setDefaultBrowserUserChoiceAsync: setDefaultBrowserUserChoiceStub }); + +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(() => { + defaultAgentStub.restore(); + _userChoiceImpossibleTelemetryResultStub.restore(); + userChoiceStub.restore(); + shellStub.restore(); + + ExperimentAPI._store._deleteForTests("shellService"); +}); + +let defaultUserChoice; +add_task(async function need_user_choice() { + await 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, + }, + }); + + await 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"); + + await 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.platform != "win") { + 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, + }, + }); + + await ShellService.setDefaultBrowser(); + + Assert.ok(userChoiceStub.called, "Set default with user choice called"); + + let message = ""; + await userChoicePromise.catch(err => (message = err.message || "")); + + Assert.ok( + message.includes("ErrExeProgID"), + "Set default with user choice threw an expected 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..cfe7a580a1 --- /dev/null +++ b/browser/components/shell/test/browser_setDefaultPDFHandler.js @@ -0,0 +1,269 @@ +/* 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 setDefaultBrowserUserChoiceStub = sinon.stub(); +const setDefaultExtensionHandlersUserChoiceStub = sinon + .stub() + .callsFake(() => Promise.resolve()); + +const defaultAgentStub = sinon.stub(ShellService, "defaultAgent").value({ + setDefaultBrowserUserChoiceAsync: setDefaultBrowserUserChoiceStub, + setDefaultExtensionHandlersUserChoice: + setDefaultExtensionHandlersUserChoiceStub, +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "XreDirProvider", + "@mozilla.org/xre/directory-provider;1", + "nsIXREDirProvider" +); + +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(() => { + defaultAgentStub.restore(); + _userChoiceImpossibleTelemetryResultStub.restore(); + shellStub.restore(); + + ExperimentAPI._store._deleteForTests("shellService"); +}); + +add_task(async function ready() { + await ExperimentAPI.ready(); +}); + +// Everything here is Windows. +Assert.ok(AppConstants.platform == "win", "Platform is Windows"); + +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 + ); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + const aumi = XreDirProvider.getInstallHash(); + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [ + 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); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual( + setDefaultBrowserUserChoiceStub.firstCall.args, + [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"); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual( + setDefaultBrowserUserChoiceStub.firstCall.args, + [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 + ); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + const aumi = XreDirProvider.getInstallHash(); + Assert.ok(setDefaultBrowserUserChoiceStub.called); + Assert.deepEqual(setDefaultBrowserUserChoiceStub.firstCall.args, [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 + ); + + setDefaultBrowserUserChoiceStub.resetHistory(); + await ShellService.setDefaultBrowser(); + + Assert.ok(setDefaultBrowserUserChoiceStub.notCalled); + Assert.ok(setDefaultStub.called); + + await doCleanup(); +}); + +add_task(async function test_setAsDefaultPDFHandler_knownBrowser() { + const sandbox = sinon.createSandbox(); + + const aumi = XreDirProvider.getInstallHash(); + const expectedArguments = [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( + setDefaultExtensionHandlersUserChoiceStub.called, + "Called default browser agent" + ); + Assert.deepEqual( + setDefaultExtensionHandlersUserChoiceStub.firstCall.args, + expectedArguments, + "Called default browser agent with expected arguments" + ); + setDefaultExtensionHandlersUserChoiceStub.resetHistory(); + + info("Testing setAsDefaultPDFHandler(false) when knownBrowser = true"); + ShellService.setAsDefaultPDFHandler(false); + Assert.ok( + setDefaultExtensionHandlersUserChoiceStub.called, + "Called default browser agent" + ); + Assert.deepEqual( + setDefaultExtensionHandlersUserChoiceStub.firstCall.args, + expectedArguments, + "Called default browser agent with expected arguments" + ); + setDefaultExtensionHandlersUserChoiceStub.resetHistory(); + + pdfHandlerResult.knownBrowser = false; + + info("Testing setAsDefaultPDFHandler(true) when knownBrowser = false"); + ShellService.setAsDefaultPDFHandler(true); + Assert.ok( + setDefaultExtensionHandlersUserChoiceStub.notCalled, + "Did not call default browser agent" + ); + setDefaultExtensionHandlersUserChoiceStub.resetHistory(); + + info("Testing setAsDefaultPDFHandler(false) when knownBrowser = false"); + ShellService.setAsDefaultPDFHandler(false); + Assert.ok( + setDefaultExtensionHandlersUserChoiceStub.called, + "Called default browser agent" + ); + Assert.deepEqual( + setDefaultExtensionHandlersUserChoiceStub.firstCall.args, + expectedArguments, + "Called default browser agent with expected arguments" + ); + setDefaultExtensionHandlersUserChoiceStub.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..db1f8811fd --- /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); + Assert.greater(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.toml b/browser/components/shell/test/unit/xpcshell.toml new file mode 100644 index 0000000000..200b895ca3 --- /dev/null +++ b/browser/components/shell/test/unit/xpcshell.toml @@ -0,0 +1,6 @@ +[DEFAULT] +skip-if = ["os == 'android'"] # bug 1730213 +firefox-appdir = "browser" + +["test_macOS_showSecurityPreferences.js"] +run-if = ["os == 'mac'"] |