diff options
Diffstat (limited to 'browser/components/shell')
42 files changed, 4950 insertions, 0 deletions
diff --git a/browser/components/shell/HeadlessShell.jsm b/browser/components/shell/HeadlessShell.jsm new file mode 100644 index 0000000000..533e0efd0d --- /dev/null +++ b/browser/components/shell/HeadlessShell.jsm @@ -0,0 +1,231 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["HeadlessShell", "ScreenshotParent"]; + +const { E10SUtils } = ChromeUtils.import( + "resource://gre/modules/E10SUtils.jsm" +); +const { HiddenFrame } = ChromeUtils.import( + "resource://gre/modules/HiddenFrame.jsm" +); +const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// Refrences to the progress listeners to keep them from being gc'ed +// before they are called. +const progressListeners = new Set(); + +class ScreenshotParent extends JSWindowActorParent { + takeScreenshot(params) { + return this.sendQuery("TakeScreenshot", params); + } +} + +ChromeUtils.registerWindowActor("Screenshot", { + parent: { + moduleURI: "resource:///modules/HeadlessShell.jsm", + }, + child: { + moduleURI: "resource:///modules/ScreenshotChild.jsm", + messages: ["TakeScreenshot"], + }, +}); + +function loadContentWindow(browser, url) { + let uri; + try { + uri = Services.io.newURI(url); + } catch (e) { + let msg = `Invalid URL passed to loadContentWindow(): ${url}`; + Cu.reportError(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.spec, 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;` + ); + doc.documentElement.appendChild(browser); + + await loadContentWindow(browser, url); + + let actor = browser.browsingContext.currentWindowGlobal.getActor( + "Screenshot" + ); + let blob = await actor.takeScreenshot({ + fullWidth, + fullHeight, + }); + + let reader = await new Promise(resolve => { + let fr = new FileReader(); + fr.onloadend = () => resolve(fr); + fr.readAsArrayBuffer(blob); + }); + + await OS.File.writeAtomic(path, new Uint8Array(reader.result), { + flush: true, + }); + dump("Screenshot saved to: " + path + "\n"); + } catch (e) { + dump("Failure taking screenshot: " + e + "\n"); + } finally { + if (frame) { + frame.destroy(); + } + } +} + +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; + } + } + + // Only command line argument left should be `screenshot` + // There could still be URLs however + try { + var path = cmdLine.handleFlagWithParam("screenshot", true); + if (!cmdLine.length && !URLlist.length) { + URLlist.push(path); // Assume the user wanted to specify a URL + path = OS.Path.join(cmdLine.workingDirectory.path, "screenshot.png"); + } + } catch (e) { + path = OS.Path.join(cmdLine.workingDirectory.path, "screenshot.png"); + cmdLine.handleFlag("screenshot", true); // Remove `screenshot` + } + + for (let i = 0; i < cmdLine.length; ++i) { + URLlist.push(cmdLine.getArgument(i)); // Assume that all remaining arguments are URLs + } + + 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.jsm b/browser/components/shell/ScreenshotChild.jsm new file mode 100644 index 0000000000..18e18b2d2d --- /dev/null +++ b/browser/components/shell/ScreenshotChild.jsm @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["ScreenshotChild"]; + +class ScreenshotChild extends JSWindowActorChild { + receiveMessage(message) { + if (message.name == "TakeScreenshot") { + return this.takeScreenshot(message.data); + } + return null; + } + + async takeScreenshot(params) { + if (this.document.readyState != "complete") { + await new Promise(resolve => + this.contentWindow.addEventListener("load", resolve, { once: true }) + ); + } + + let { fullWidth, fullHeight } = params; + let { contentWindow } = this; + + let canvas = contentWindow.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "html:canvas" + ); + let context = canvas.getContext("2d"); + let width = contentWindow.innerWidth; + let height = contentWindow.innerHeight; + if (fullWidth) { + width += contentWindow.scrollMaxX - contentWindow.scrollMinX; + } + if (fullHeight) { + height += contentWindow.scrollMaxY - contentWindow.scrollMinY; + } + + canvas.width = width; + canvas.height = height; + context.drawWindow( + contentWindow, + 0, + 0, + width, + height, + "rgb(255, 255, 255)" + ); + + return new Promise(resolve => canvas.toBlob(resolve)); + } +} diff --git a/browser/components/shell/ShellService.jsm b/browser/components/shell/ShellService.jsm new file mode 100644 index 0000000000..5a1377abea --- /dev/null +++ b/browser/components/shell/ShellService.jsm @@ -0,0 +1,170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["ShellService"]; + +const { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "WindowsRegistry", + "resource://gre/modules/WindowsRegistry.jsm" +); + +/** + * Internal functionality to save and restore the docShell.allow* properties. + */ +let ShellServiceInternal = { + /** + * Used to determine whether or not to offer "Set as desktop background" + * functionality. Even if shell service is available it is not + * guaranteed that it is able to set the background for every desktop + * which is especially true for Linux with its many different desktop + * environments. + */ + get canSetDesktopBackground() { + if (AppConstants.platform == "win" || AppConstants.platform == "macosx") { + return true; + } + + if (AppConstants.platform == "linux") { + if (this.shellService) { + let linuxShellService = this.shellService.QueryInterface( + Ci.nsIGNOMEShellService + ); + return linuxShellService.canSetDesktopBackground; + } + } + + return false; + }, + + isDefaultBrowserOptOut() { + if (AppConstants.platform == "win") { + let optOutValue = WindowsRegistry.readRegKey( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "Software\\Mozilla\\Firefox", + "DefaultBrowserOptOut" + ); + WindowsRegistry.removeRegKey( + Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "Software\\Mozilla\\Firefox", + "DefaultBrowserOptOut" + ); + if (optOutValue == "True") { + Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", false); + return true; + } + } + return false; + }, + + /** + * Used to determine whether or not to show a "Set Default Browser" + * query dialog. This attribute is true if the application is starting + * up and "browser.shell.checkDefaultBrowser" is true, otherwise it + * is false. + */ + _checkedThisSession: false, + get shouldCheckDefaultBrowser() { + // If we've already checked, the browser has been started and this is a + // new window open, and we don't want to check again. + if (this._checkedThisSession) { + return false; + } + + if (!Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser")) { + return false; + } + + if (this.isDefaultBrowserOptOut()) { + return false; + } + + return true; + }, + + set shouldCheckDefaultBrowser(shouldCheck) { + Services.prefs.setBoolPref( + "browser.shell.checkDefaultBrowser", + !!shouldCheck + ); + }, + + isDefaultBrowser(startupCheck, forAllTypes) { + // If this is the first browser window, maintain internal state that we've + // checked this session (so that subsequent window opens don't show the + // default browser dialog). + if (startupCheck) { + this._checkedThisSession = true; + } + if (this.shellService) { + return this.shellService.isDefaultBrowser(forAllTypes); + } + return false; + }, + + setAsDefault() { + let claimAllTypes = true; + let setAsDefaultError = false; + if (AppConstants.platform == "win") { + try { + // In Windows 8+, the UI for selecting default protocol is much + // nicer than the UI for setting file type associations. So we + // only show the protocol association screen on Windows 8+. + // Windows 8 is version 6.2. + let version = Services.sysinfo.getProperty("version"); + claimAllTypes = parseFloat(version) < 6.2; + } catch (ex) {} + } + try { + ShellService.setDefaultBrowser(claimAllTypes, false); + } catch (ex) { + setAsDefaultError = true; + Cu.reportError(ex); + } + // Here BROWSER_IS_USER_DEFAULT and BROWSER_SET_USER_DEFAULT_ERROR appear + // to be inverse of each other, but that is only because this function is + // called when the browser is set as the default. During startup we record + // the BROWSER_IS_USER_DEFAULT value without recording BROWSER_SET_USER_DEFAULT_ERROR. + Services.telemetry + .getHistogramById("BROWSER_IS_USER_DEFAULT") + .add(!setAsDefaultError); + Services.telemetry + .getHistogramById("BROWSER_SET_DEFAULT_ERROR") + .add(setAsDefaultError); + }, +}; + +XPCOMUtils.defineLazyServiceGetter( + ShellServiceInternal, + "shellService", + "@mozilla.org/browser/shell-service;1", + Ci.nsIShellService +); + +/** + * The external API exported by this module. + */ +var ShellService = new Proxy(ShellServiceInternal, { + get(target, name) { + if (name in target) { + return target[name]; + } + if (target.shellService) { + return target.shellService[name]; + } + Services.console.logStringMessage( + `${name} not found in ShellService: ${target.shellService}` + ); + return undefined; + }, +}); diff --git a/browser/components/shell/WindowsDefaultBrowser.cpp b/browser/components/shell/WindowsDefaultBrowser.cpp new file mode 100644 index 0000000000..00e9e645e6 --- /dev/null +++ b/browser/components/shell/WindowsDefaultBrowser.cpp @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file exists so that LaunchModernSettingsDialogDefaultApps can be called + * without linking to libxul. + */ +#include "WindowsDefaultBrowser.h" + +#include "city.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +// Needed for access to IApplicationActivationManager +// This must be before any other includes that might include shlobj.h +#ifdef _WIN32_WINNT +# undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 +#define INITGUID +#undef NTDDI_VERSION +#define NTDDI_VERSION NTDDI_WIN8 +#include <shlobj.h> + +#include <lm.h> +#include <shlwapi.h> +#include <wchar.h> +#include <windows.h> + +#define APP_REG_NAME_BASE L"Firefox-" + +static bool IsWindowsLogonConnected() { + WCHAR userName[UNLEN + 1]; + DWORD size = mozilla::ArrayLength(userName); + if (!GetUserNameW(userName, &size)) { + return false; + } + + LPUSER_INFO_24 info; + if (NetUserGetInfo(nullptr, userName, 24, (LPBYTE*)&info) != NERR_Success) { + return false; + } + bool connected = info->usri24_internet_identity; + NetApiBufferFree(info); + + return connected; +} + +static bool SettingsAppBelievesConnected() { + const wchar_t* keyPath = L"SOFTWARE\\Microsoft\\Windows\\Shell\\Associations"; + const wchar_t* valueName = L"IsConnectedAtLogon"; + + uint32_t value = 0; + DWORD size = sizeof(uint32_t); + LSTATUS ls = RegGetValueW(HKEY_CURRENT_USER, keyPath, valueName, RRF_RT_ANY, + nullptr, &value, &size); + if (ls != ERROR_SUCCESS) { + return false; + } + + return !!value; +} + +// Generates the install directory without a trailing path separator. +bool GetInstallDirectory(mozilla::UniquePtr<wchar_t[]>& installPath) { + installPath = mozilla::GetFullBinaryPath(); + // It's not safe to use PathRemoveFileSpecW with strings longer than MAX_PATH + // (including null terminator). + if (wcslen(installPath.get()) >= MAX_PATH) { + return false; + } + PathRemoveFileSpecW(installPath.get()); + return true; +} + +bool GetAppRegName(mozilla::UniquePtr<wchar_t[]>& aAppRegName) { + mozilla::UniquePtr<wchar_t[]> appDirStr; + bool success = GetInstallDirectory(appDirStr); + if (!success) { + return success; + } + + uint64_t hash = CityHash64(reinterpret_cast<char*>(appDirStr.get()), + wcslen(appDirStr.get()) * sizeof(wchar_t)); + + const wchar_t* format = L"%s%I64X"; + int bufferSize = _scwprintf(format, APP_REG_NAME_BASE, hash); + ++bufferSize; // Extra byte for terminating null + aAppRegName = mozilla::MakeUnique<wchar_t[]>(bufferSize); + + _snwprintf_s(aAppRegName.get(), bufferSize, _TRUNCATE, format, + APP_REG_NAME_BASE, hash); + + return success; +} + +bool LaunchControlPanelDefaultPrograms() { + // Build the path control.exe path safely + WCHAR controlEXEPath[MAX_PATH + 1] = {'\0'}; + if (!GetSystemDirectoryW(controlEXEPath, MAX_PATH)) { + return false; + } + LPCWSTR controlEXE = L"control.exe"; + if (wcslen(controlEXEPath) + wcslen(controlEXE) >= MAX_PATH) { + return false; + } + if (!PathAppendW(controlEXEPath, controlEXE)) { + return false; + } + + const wchar_t* paramFormat = + L"control.exe /name Microsoft.DefaultPrograms " + L"/page pageDefaultProgram\\pageAdvancedSettings?pszAppName=%s"; + mozilla::UniquePtr<wchar_t[]> appRegName; + GetAppRegName(appRegName); + int bufferSize = _scwprintf(paramFormat, appRegName.get()); + ++bufferSize; // Extra byte for terminating null + mozilla::UniquePtr<wchar_t[]> params = + mozilla::MakeUnique<wchar_t[]>(bufferSize); + _snwprintf_s(params.get(), bufferSize, _TRUNCATE, paramFormat, + appRegName.get()); + + STARTUPINFOW si = {sizeof(si), 0}; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_SHOWDEFAULT; + PROCESS_INFORMATION pi = {0}; + if (!CreateProcessW(controlEXEPath, params.get(), nullptr, nullptr, FALSE, 0, + nullptr, nullptr, &si, &pi)) { + return false; + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return true; +} + +bool LaunchModernSettingsDialogDefaultApps() { + if (!mozilla::IsWindowsBuildOrLater(14965) && !IsWindowsLogonConnected() && + SettingsAppBelievesConnected()) { + // Use the classic Control Panel to work around a bug of older + // builds of Windows 10. + return LaunchControlPanelDefaultPrograms(); + } + + IApplicationActivationManager* pActivator; + HRESULT hr = CoCreateInstance( + CLSID_ApplicationActivationManager, nullptr, CLSCTX_INPROC, + IID_IApplicationActivationManager, (void**)&pActivator); + + if (SUCCEEDED(hr)) { + DWORD pid; + hr = pActivator->ActivateApplication( + L"windows.immersivecontrolpanel_cw5n1h2txyewy" + L"!microsoft.windows.immersivecontrolpanel", + L"page=SettingsPageAppsDefaults", AO_NONE, &pid); + if (SUCCEEDED(hr)) { + // Do not check error because we could at least open + // the "Default apps" setting. + pActivator->ActivateApplication( + L"windows.immersivecontrolpanel_cw5n1h2txyewy" + L"!microsoft.windows.immersivecontrolpanel", + L"page=SettingsPageAppsDefaults" + L"&target=SystemSettings_DefaultApps_Browser", + AO_NONE, &pid); + } + pActivator->Release(); + return SUCCEEDED(hr); + } + return true; +} diff --git a/browser/components/shell/WindowsDefaultBrowser.h b/browser/components/shell/WindowsDefaultBrowser.h new file mode 100644 index 0000000000..cfdc4e0f48 --- /dev/null +++ b/browser/components/shell/WindowsDefaultBrowser.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/. */ + +/** + * This file exists so that LaunchModernSettingsDialogDefaultApps can be called + * without linking to libxul. + */ + +#ifndef windowsdefaultbrowser_h____ +#define windowsdefaultbrowser_h____ + +#include "mozilla/UniquePtr.h" + +bool GetInstallDirectory(mozilla::UniquePtr<wchar_t[]>& installPath); +bool GetAppRegName(mozilla::UniquePtr<wchar_t[]>& aAppRegName); +bool LaunchControlPanelDefaultPrograms(); +bool LaunchModernSettingsDialogDefaultApps(); + +#endif // windowsdefaultbrowser_h____ diff --git a/browser/components/shell/content/setDesktopBackground.js b/browser/components/shell/content/setDesktopBackground.js new file mode 100644 index 0000000000..2ecf125206 --- /dev/null +++ b/browser/components/shell/content/setDesktopBackground.js @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../base/content/utilityOverlay.js */ + +var gSetBackground = { + _position: AppConstants.platform == "macosx" ? "STRETCH" : "", + _backgroundColor: AppConstants.platform != "macosx" ? 0 : undefined, + _screenWidth: 0, + _screenHeight: 0, + _image: null, + _canvas: null, + _imageName: null, + + get _shell() { + return Cc["@mozilla.org/browser/shell-service;1"].getService( + Ci.nsIShellService + ); + }, + + load() { + this._canvas = document.getElementById("screen"); + this._screenWidth = screen.width; + this._screenHeight = screen.height; + // Cap ratio to 4 so the dialog width doesn't get ridiculous. Highest + // regular screens seem to be 32:9 (3.56) according to Wikipedia. + let screenRatio = Math.min(this._screenWidth / this._screenHeight, 4); + this._canvas.width = this._canvas.height * screenRatio; + document.getElementById("preview-unavailable").style.width = + this._canvas.width + "px"; + + if (AppConstants.platform == "macosx") { + document + .getElementById("SetDesktopBackgroundDialog") + .getButton("accept").hidden = true; + } else { + let multiMonitors = false; + if (AppConstants.platform == "linux") { + // getMonitors only ever returns the primary monitor on Linux, so just + // always show the option + multiMonitors = true; + } else { + const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo); + const monitors = gfxInfo.getMonitors(); + multiMonitors = monitors.length > 1; + } + + if ( + !multiMonitors || + AppConstants.isPlatformAndVersionAtMost("win", 6.1) + ) { + // Hide span option if < Win8 since that's when it was introduced. + document.getElementById("spanPosition").hidden = true; + } + } + + document.addEventListener("dialogaccept", function() { + gSetBackground.setDesktopBackground(); + }); + // make sure that the correct dimensions will be used + setTimeout( + function(self) { + self.init(window.arguments[0], window.arguments[1]); + }, + 0, + this + ); + }, + + init(aImage, aImageName) { + this._image = aImage; + this._imageName = aImageName; + + // set the size of the coordinate space + this._canvas.width = this._canvas.clientWidth; + this._canvas.height = this._canvas.clientHeight; + + var ctx = this._canvas.getContext("2d"); + ctx.scale( + this._canvas.clientWidth / this._screenWidth, + this._canvas.clientHeight / this._screenHeight + ); + + if (AppConstants.platform != "macosx") { + this._initColor(); + } else { + // Make sure to reset the button state in case the user has already + // set an image as their desktop background. + var setDesktopBackground = document.getElementById( + "setDesktopBackground" + ); + setDesktopBackground.hidden = false; + var bundle = document.getElementById("backgroundBundle"); + setDesktopBackground.label = bundle.getString("DesktopBackgroundSet"); + setDesktopBackground.disabled = false; + + document.getElementById("showDesktopPreferences").hidden = true; + } + this.updatePosition(); + }, + + setDesktopBackground() { + if (AppConstants.platform != "macosx") { + Services.xulStore.persist( + document.getElementById("menuPosition"), + "value" + ); + this._shell.desktopBackgroundColor = this._hexStringToLong( + this._backgroundColor + ); + } else { + Services.obs.addObserver(this, "shell:desktop-background-changed"); + + var bundle = document.getElementById("backgroundBundle"); + var setDesktopBackground = document.getElementById( + "setDesktopBackground" + ); + setDesktopBackground.disabled = true; + setDesktopBackground.label = bundle.getString( + "DesktopBackgroundDownloading" + ); + } + this._shell.setDesktopBackground( + this._image, + Ci.nsIShellService["BACKGROUND_" + this._position], + this._imageName + ); + }, + + updatePosition() { + var ctx = this._canvas.getContext("2d"); + ctx.clearRect(0, 0, this._screenWidth, this._screenHeight); + document.getElementById("preview-unavailable").hidden = true; + + if (AppConstants.platform != "macosx") { + this._position = document.getElementById("menuPosition").value; + } + + switch (this._position) { + case "TILE": + ctx.save(); + ctx.fillStyle = ctx.createPattern(this._image, "repeat"); + ctx.fillRect(0, 0, this._screenWidth, this._screenHeight); + ctx.restore(); + break; + case "STRETCH": + ctx.drawImage(this._image, 0, 0, this._screenWidth, this._screenHeight); + break; + case "CENTER": { + let x = (this._screenWidth - this._image.naturalWidth) / 2; + let y = (this._screenHeight - this._image.naturalHeight) / 2; + ctx.drawImage(this._image, x, y); + break; + } + case "FILL": { + // Try maxing width first, overflow height. + let widthRatio = this._screenWidth / this._image.naturalWidth; + let width = this._image.naturalWidth * widthRatio; + let height = this._image.naturalHeight * widthRatio; + if (height < this._screenHeight) { + // Height less than screen, max height and overflow width. + let heightRatio = this._screenHeight / this._image.naturalHeight; + width = this._image.naturalWidth * heightRatio; + height = this._image.naturalHeight * heightRatio; + } + let x = (this._screenWidth - width) / 2; + let y = (this._screenHeight - height) / 2; + ctx.drawImage(this._image, x, y, width, height); + break; + } + case "FIT": { + // Try maxing width first, top and bottom borders. + let widthRatio = this._screenWidth / this._image.naturalWidth; + let width = this._image.naturalWidth * widthRatio; + let height = this._image.naturalHeight * widthRatio; + let x = 0; + let y = (this._screenHeight - height) / 2; + if (height > this._screenHeight) { + // Height overflow, maximise height, side borders. + let heightRatio = this._screenHeight / this._image.naturalHeight; + width = this._image.naturalWidth * heightRatio; + height = this._image.naturalHeight * heightRatio; + x = (this._screenWidth - width) / 2; + y = 0; + } + ctx.drawImage(this._image, x, y, width, height); + break; + } + case "SPAN": { + document.getElementById("preview-unavailable").hidden = false; + ctx.fillStyle = "#222"; + ctx.fillRect(0, 0, this._screenWidth, this._screenHeight); + ctx.stroke(); + } + } + }, +}; + +if (AppConstants.platform != "macosx") { + gSetBackground._initColor = function() { + var color = this._shell.desktopBackgroundColor; + + const rMask = 4294901760; + const gMask = 65280; + const bMask = 255; + var r = (color & rMask) >> 16; + var g = (color & gMask) >> 8; + var b = color & bMask; + this.updateColor(this._rgbToHex(r, g, b)); + + var colorpicker = document.getElementById("desktopColor"); + colorpicker.value = this._backgroundColor; + }; + + gSetBackground.updateColor = function(aColor) { + this._backgroundColor = aColor; + this._canvas.style.backgroundColor = aColor; + }; + + // Converts a color string in the format "#RRGGBB" to an integer. + gSetBackground._hexStringToLong = function(aString) { + return ( + (parseInt(aString.substring(1, 3), 16) << 16) | + (parseInt(aString.substring(3, 5), 16) << 8) | + parseInt(aString.substring(5, 7), 16) + ); + }; + + gSetBackground._rgbToHex = function(aR, aG, aB) { + return ( + "#" + + [aR, aG, aB] + .map(aInt => aInt.toString(16).replace(/^(.)$/, "0$1")) + .join("") + .toUpperCase() + ); + }; +} else { + gSetBackground.observe = function(aSubject, aTopic, aData) { + if (aTopic == "shell:desktop-background-changed") { + document.getElementById("setDesktopBackground").hidden = true; + document.getElementById("showDesktopPreferences").hidden = false; + + Services.obs.removeObserver(this, "shell:desktop-background-changed"); + } + }; + + gSetBackground.showDesktopPrefs = function() { + this._shell.QueryInterface(Ci.nsIMacShellService).showDesktopPreferences(); + }; +} diff --git a/browser/components/shell/content/setDesktopBackground.xhtml b/browser/components/shell/content/setDesktopBackground.xhtml new file mode 100644 index 0000000000..53cd586a3c --- /dev/null +++ b/browser/components/shell/content/setDesktopBackground.xhtml @@ -0,0 +1,95 @@ +<?xml version="1.0"?> <!-- -*- Mode: HTML -*- --> + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/setDesktopBackground.css" type="text/css"?> + + +<!DOCTYPE dialog [ +#ifdef XP_MACOSX +#include ../../../base/content/browser-doctype.inc +#endif +]> + +<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="width: 30em;"> + +<linkset> + <html:link rel="localization" href="browser/setDesktopBackground.ftl"/> +</linkset> + +<dialog id="SetDesktopBackgroundDialog" +#ifndef XP_MACOSX + buttons="accept,cancel" +#else + buttons="accept" +#endif + buttonidaccept="set-desktop-background-accept"> + +#ifdef XP_MACOSX +#include ../../../base/content/macWindow.inc.xhtml +#else + <script src="chrome://browser/content/utilityOverlay.js"/> +#endif + + <stringbundle id="backgroundBundle" + src="chrome://browser/locale/shellservice.properties"/> + <script src="chrome://browser/content/setDesktopBackground.js"/> + <script src="chrome://global/content/contentAreaUtils.js"/> + +#ifndef XP_MACOSX + <hbox align="center"> + <label data-l10n-id="set-background-position"/> + <menulist id="menuPosition" + oncommand="gSetBackground.updatePosition();"> + <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..eec5f0a879 --- /dev/null +++ b/browser/components/shell/moz.build @@ -0,0 +1,79 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# For BinaryPath::GetLong for Windows +LOCAL_INCLUDES += ["/xpcom/build"] + +BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +JAR_MANIFESTS += ["jar.mn"] + +XPIDL_SOURCES += [ + "nsIShellService.idl", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + XPIDL_SOURCES += [ + "nsIMacShellService.idl", + ] + + SOURCES += [ + "nsMacShellService.cpp", + ] + + LOCAL_INCLUDES += [ + # For CocoaFileUtils + "/xpcom/io" + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + XPIDL_SOURCES += [ + "nsIGNOMEShellService.idl", + ] + + SOURCES += [ + "nsGNOMEShellService.cpp", + ] + if CONFIG["MOZ_ENABLE_DBUS"]: + SOURCES += [ + "nsGNOMEShellDBusHelper.cpp", + "nsGNOMEShellSearchProvider.cpp", + ] + include("/ipc/chromium/chromium-config.mozbuild") + +elif CONFIG["OS_ARCH"] == "WINNT": + XPIDL_SOURCES += [ + "nsIWindowsShellService.idl", + ] + SOURCES += [ + "nsWindowsShellService.cpp", + "WindowsDefaultBrowser.cpp", + ] + LOCAL_INCLUDES += [ + "../../../other-licenses/nsis/Contrib/CityHash/cityhash", + ] + +XPIDL_MODULE = "shellservice" + +if SOURCES: + FINAL_LIBRARY = "browsercomps" + +EXTRA_JS_MODULES += [ + "HeadlessShell.jsm", + "ScreenshotChild.jsm", + "ShellService.jsm", +] + +for var in ("MOZ_APP_NAME", "MOZ_APP_VERSION"): + DEFINES[var] = '"%s"' % CONFIG[var] + +CXXFLAGS += CONFIG["TK_CFLAGS"] +if CONFIG["MOZ_ENABLE_DBUS"]: + CXXFLAGS += CONFIG["MOZ_DBUS_GLIB_CFLAGS"] + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Shell Integration") diff --git a/browser/components/shell/nsGNOMEShellDBusHelper.cpp b/browser/components/shell/nsGNOMEShellDBusHelper.cpp new file mode 100644 index 0000000000..35fbba6a21 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellDBusHelper.cpp @@ -0,0 +1,494 @@ +/* -*- 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 "nsPrintfCString.h" +#include "RemoteUtils.h" +#include "nsServiceManagerUtils.h" + +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("gnomeSearchProviderSearch", formatStrings, + gnomeSearchTitle); + AppendUTF16toUTF8(gnomeSearchTitle, aGnomeSearchTitle); + return true; +} + +static const char* introspect_template = + "<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection " + "1.0//EN\"\n" + "\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n" + "<node>\n" + " <interface name=\"org.freedesktop.DBus.Introspectable\">\n" + " <method name=\"Introspect\">\n" + " <arg name=\"data\" direction=\"out\" type=\"s\"/>\n" + " </method>\n" + " </interface>\n" + " <interface name=\"org.gnome.Shell.SearchProvider2\">\n" + " <method name=\"GetInitialResultSet\">\n" + " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n" + " <arg type=\"as\" name=\"results\" direction=\"out\" />\n" + " </method>\n" + " <method name=\"GetSubsearchResultSet\">\n" + " <arg type=\"as\" name=\"previous_results\" direction=\"in\" />\n" + " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n" + " <arg type=\"as\" name=\"results\" direction=\"out\" />\n" + " </method>\n" + " <method name=\"GetResultMetas\">\n" + " <arg type=\"as\" name=\"identifiers\" direction=\"in\" />\n" + " <arg type=\"aa{sv}\" name=\"metas\" direction=\"out\" />\n" + " </method>\n" + " <method name=\"ActivateResult\">\n" + " <arg type=\"s\" name=\"identifier\" direction=\"in\" />\n" + " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n" + " <arg type=\"u\" name=\"timestamp\" direction=\"in\" />\n" + " </method>\n" + " <method name=\"LaunchSearch\">\n" + " <arg type=\"as\" name=\"terms\" direction=\"in\" />\n" + " <arg type=\"u\" name=\"timestamp\" direction=\"in\" />\n" + " </method>\n" + "</interface>\n" + "</node>\n"; + +DBusHandlerResult DBusIntrospect(DBusConnection* aConnection, + DBusMessage* aMsg) { + DBusMessage* reply; + + reply = dbus_message_new_method_return(aMsg); + if (!reply) { + return DBUS_HANDLER_RESULT_NEED_MEMORY; + } + + dbus_message_append_args(reply, DBUS_TYPE_STRING, &introspect_template, + DBUS_TYPE_INVALID); + + dbus_connection_send(aConnection, reply, nullptr); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +int DBusGetIndexFromIDKey(const char* aIDKey) { + // ID is NN:URL where NN is index to our current history + // result container. + char tmp[] = {aIDKey[0], aIDKey[1], '\0'}; + return atoi(tmp); +} + +DBusHandlerResult DBusHandleInitialResultSet( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) { + DBusMessage* reply; + char** stringArray = nullptr; + int elements; + + if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, + &stringArray, &elements, DBUS_TYPE_INVALID) || + elements == 0) { + reply = dbus_message_new_error(aMsg, DBUS_BUS_NAME, "Wrong argument"); + dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr); + dbus_message_unref(reply); + } else { + aSearchResult->SetReply(dbus_message_new_method_return(aMsg)); + aSearchResult->SetSearchTerm(stringArray[0]); + GetGNOMEShellHistoryService()->QueryHistory(aSearchResult); + // DBus reply will be send asynchronously by + // nsGNOMEShellHistorySearchResult::SendDBusSearchResultReply() + // when GetGNOMEShellHistoryService() has the results. + } + + if (stringArray) { + dbus_free_string_array(stringArray); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +DBusHandlerResult DBusHandleSubsearchResultSet( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) { + DBusMessage* reply; + + char **unusedArray = nullptr, **stringArray = nullptr; + int unusedNum, elements; + + if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, + &unusedArray, &unusedNum, DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING, &stringArray, &elements, + DBUS_TYPE_INVALID) || + elements == 0) { + reply = dbus_message_new_error(aMsg, DBUS_BUS_NAME, "Wrong argument"); + dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr); + dbus_message_unref(reply); + } else { + aSearchResult->SetReply(dbus_message_new_method_return(aMsg)); + aSearchResult->SetSearchTerm(stringArray[0]); + GetGNOMEShellHistoryService()->QueryHistory(aSearchResult); + // DBus reply will be send asynchronously by + // nsGNOMEShellHistorySearchResult::SendDBusSearchResultReply() + // when GetGNOMEShellHistoryService() has the results. + } + + if (unusedArray) { + dbus_free_string_array(unusedArray); + } + if (stringArray) { + dbus_free_string_array(stringArray); + } + + return DBUS_HANDLER_RESULT_HANDLED; +} + +static void appendStringDictionary(DBusMessageIter* aIter, const char* aKey, + const char* aValue) { + DBusMessageIter iterDict, iterVar; + dbus_message_iter_open_container(aIter, DBUS_TYPE_DICT_ENTRY, nullptr, + &iterDict); + dbus_message_iter_append_basic(&iterDict, DBUS_TYPE_STRING, &aKey); + dbus_message_iter_open_container(&iterDict, DBUS_TYPE_VARIANT, "s", &iterVar); + dbus_message_iter_append_basic(&iterVar, DBUS_TYPE_STRING, &aValue); + dbus_message_iter_close_container(&iterDict, &iterVar); + dbus_message_iter_close_container(aIter, &iterDict); +} + +/* + "icon-data": a tuple of type (iiibiiay) describing a pixbuf with width, + height, rowstride, has-alpha, + bits-per-sample, channels, + image data +*/ +static void DBusAppendIcon(GnomeHistoryIcon* aIcon, DBusMessageIter* aIter) { + DBusMessageIter iterDict, iterVar, iterStruct; + dbus_message_iter_open_container(aIter, DBUS_TYPE_DICT_ENTRY, nullptr, + &iterDict); + const char* key = "icon-data"; + dbus_message_iter_append_basic(&iterDict, DBUS_TYPE_STRING, &key); + dbus_message_iter_open_container(&iterDict, DBUS_TYPE_VARIANT, "(iiibiiay)", + &iterVar); + dbus_message_iter_open_container(&iterVar, DBUS_TYPE_STRUCT, nullptr, + &iterStruct); + + int width = aIcon->GetWidth(); + int height = aIcon->GetHeight(); + dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &width); + dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &height); + int rowstride = width * 4; + dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &rowstride); + int hasAlpha = true; + dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_BOOLEAN, &hasAlpha); + int bitsPerSample = 8; + dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &bitsPerSample); + int channels = 4; + dbus_message_iter_append_basic(&iterStruct, DBUS_TYPE_INT32, &channels); + + DBusMessageIter iterArray; + dbus_message_iter_open_container(&iterStruct, DBUS_TYPE_ARRAY, "y", + &iterArray); + unsigned char* array = aIcon->GetData(); + dbus_message_iter_append_fixed_array(&iterArray, DBUS_TYPE_BYTE, &array, + width * height * 4); + dbus_message_iter_close_container(&iterStruct, &iterArray); + + dbus_message_iter_close_container(&iterVar, &iterStruct); + dbus_message_iter_close_container(&iterDict, &iterVar); + dbus_message_iter_close_container(aIter, &iterDict); +} + +/* Appends history search results to the DBUS reply. + + We can return those fields at GetResultMetas: + + "id": the result ID + "name": the display name for the result + "icon": a serialized GIcon (see g_icon_serialize()), or alternatively, + "gicon": a textual representation of a GIcon (see g_icon_to_string()), + or alternativly, + "icon-data": a tuple of type (iiibiiay) describing a pixbuf with width, + height, rowstride, has-alpha, bits-per-sample, and image data + "description": an optional short description (1-2 lines) +*/ +static void DBusAppendResultID( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + DBusMessageIter* aIter, const char* aID) { + nsCOMPtr<nsINavHistoryContainerResultNode> container = + aSearchResult->GetSearchResultContainer(); + + int index = DBusGetIndexFromIDKey(aID); + nsCOMPtr<nsINavHistoryResultNode> child; + container->GetChild(index, getter_AddRefs(child)); + nsAutoCString title; + if (NS_FAILED(child->GetTitle(title))) { + return; + } + + if (title.IsEmpty()) { + if (NS_FAILED(child->GetUri(title)) || title.IsEmpty()) { + return; + } + } + + const char* titleStr = title.get(); + appendStringDictionary(aIter, "id", aID); + appendStringDictionary(aIter, "name", titleStr); + + GnomeHistoryIcon* icon = aSearchResult->GetHistoryIcon(index); + if (icon) { + DBusAppendIcon(icon, aIter); + } else { + appendStringDictionary(aIter, "gicon", "text-html"); + } +} + +/* Search the web for: "searchTerm" to the DBUS reply. + */ +static void DBusAppendSearchID(DBusMessageIter* aIter, const char* aID) { + /* aID contains: + + KEYWORD_SEARCH_STRING:ssssss + + KEYWORD_SEARCH_STRING is a 'special:search' keyword + ssssss is a searched term, must be at least one character long + */ + + // aID contains only 'KEYWORD_SEARCH_STRING:' so we're missing searched + // string. + if (strlen(aID) <= KEYWORD_SEARCH_STRING_LEN + 1) { + return; + } + + appendStringDictionary(aIter, "id", KEYWORD_SEARCH_STRING); + + // Extract ssssss part from aID + auto searchTerm = nsAutoCStringN<32>(aID + KEYWORD_SEARCH_STRING_LEN + 1); + nsAutoCString gnomeSearchTitle; + if (GetGnomeSearchTitle(searchTerm.get(), gnomeSearchTitle)) { + appendStringDictionary(aIter, "name", gnomeSearchTitle.get()); + // TODO: When running on flatpak/snap we may need to use + // icon like org.mozilla.Firefox or so. + appendStringDictionary(aIter, "gicon", "firefox"); + } +} + +DBusHandlerResult DBusHandleResultMetas( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) { + DBusMessage* reply; + char** stringArray; + int elements; + + if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, + &stringArray, &elements, DBUS_TYPE_INVALID) || + elements == 0) { + reply = dbus_message_new_error(aMsg, DBUS_BUS_NAME, "Wrong argument"); + } else { + reply = dbus_message_new_method_return(aMsg); + + DBusMessageIter iter; + dbus_message_iter_init_append(reply, &iter); + DBusMessageIter iterArray; + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "a{sv}", + &iterArray); + + DBusMessageIter iterArray2; + for (int i = 0; i < elements; i++) { + dbus_message_iter_open_container(&iterArray, DBUS_TYPE_ARRAY, "{sv}", + &iterArray2); + if (strncmp(stringArray[i], KEYWORD_SEARCH_STRING, + KEYWORD_SEARCH_STRING_LEN) == 0) { + DBusAppendSearchID(&iterArray2, stringArray[i]); + } else { + DBusAppendResultID(aSearchResult, &iterArray2, stringArray[i]); + } + dbus_message_iter_close_container(&iterArray, &iterArray2); + } + + dbus_message_iter_close_container(&iter, &iterArray); + dbus_free_string_array(stringArray); + } + + dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; +} // namespace mozilla + +static void ActivateResultID( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + const char* aResultID, uint32_t aTimeStamp) { + char* commandLine = nullptr; + int tmp; + + if (strncmp(aResultID, KEYWORD_SEARCH_STRING, KEYWORD_SEARCH_STRING_LEN) == + 0) { + const char* urlList[3] = {"unused", "--search", + aSearchResult->GetSearchTerm().get()}; + commandLine = ConstructCommandLine(3, (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(2, (char**)urlList, nullptr, &tmp); + } + + if (commandLine) { + aSearchResult->HandleCommandLine(commandLine, aTimeStamp); + free(commandLine); + } +} + +static void DBusLaunchWithAllResults( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + uint32_t aTimeStamp) { + uint32_t childCount = 0; + nsresult rv = + aSearchResult->GetSearchResultContainer()->GetChildCount(&childCount); + if (NS_FAILED(rv) || childCount == 0) { + return; + } + + if (childCount > MAX_SEARCH_RESULTS_NUM) { + childCount = MAX_SEARCH_RESULTS_NUM; + } + + // Allocate space for all found results, "unused", "--search" and + // potential search request. + char** urlList = (char**)moz_xmalloc(sizeof(char*) * (childCount + 3)); + int urlListElements = 0; + + urlList[urlListElements++] = strdup("unused"); + + for (uint32_t i = 0; i < childCount; i++) { + nsCOMPtr<nsINavHistoryResultNode> child; + aSearchResult->GetSearchResultContainer()->GetChild(i, + getter_AddRefs(child)); + + if (!IsHistoryResultNodeURI(child)) { + continue; + } + + nsAutoCString uri; + nsresult rv = child->GetUri(uri); + if (NS_FAILED(rv)) { + continue; + } + urlList[urlListElements++] = strdup(uri.get()); + } + + // When there isn't any uri to open pass search at least. + if (!childCount) { + urlList[urlListElements++] = strdup("--search"); + urlList[urlListElements++] = strdup(aSearchResult->GetSearchTerm().get()); + } + + int tmp; + char* commandLine = + ConstructCommandLine(urlListElements, urlList, nullptr, &tmp); + if (commandLine) { + aSearchResult->HandleCommandLine(commandLine, aTimeStamp); + free(commandLine); + } + + for (int i = 0; i < urlListElements; i++) { + free(urlList[i]); + } + free(urlList); +} + +DBusHandlerResult DBusActivateResult( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) { + DBusMessage* reply; + char* resultID; + char** stringArray; + int elements; + uint32_t timestamp; + + if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_STRING, &resultID, + DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, &stringArray, + &elements, DBUS_TYPE_UINT32, ×tamp, + DBUS_TYPE_INVALID) || + resultID == nullptr) { + reply = dbus_message_new_error(aMsg, DBUS_BUS_NAME, "Wrong argument"); + } else { + reply = dbus_message_new_method_return(aMsg); + ActivateResultID(aSearchResult, resultID, timestamp); + dbus_free_string_array(stringArray); + } + + dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +DBusHandlerResult DBusLaunchSearch( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg) { + DBusMessage* reply; + char** stringArray; + int elements; + uint32_t timestamp; + + if (!dbus_message_get_args(aMsg, nullptr, DBUS_TYPE_ARRAY, DBUS_TYPE_STRING, + &stringArray, &elements, DBUS_TYPE_UINT32, + ×tamp, DBUS_TYPE_INVALID) || + elements == 0) { + reply = dbus_message_new_error(aMsg, DBUS_BUS_NAME, "Wrong argument"); + } else { + reply = dbus_message_new_method_return(aMsg); + DBusLaunchWithAllResults(aSearchResult, timestamp); + dbus_free_string_array(stringArray); + } + + dbus_connection_send(aSearchResult->GetDBusConnection(), reply, nullptr); + dbus_message_unref(reply); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +bool IsHistoryResultNodeURI(nsINavHistoryResultNode* aHistoryNode) { + uint32_t type; + nsresult rv = aHistoryNode->GetType(&type); + if (NS_FAILED(rv) || type != nsINavHistoryResultNode::RESULT_TYPE_URI) + return false; + + nsAutoCString title; + rv = aHistoryNode->GetTitle(title); + if (NS_SUCCEEDED(rv) && !title.IsEmpty()) { + return true; + } + + rv = aHistoryNode->GetUri(title); + return NS_SUCCEEDED(rv) && !title.IsEmpty(); +} diff --git a/browser/components/shell/nsGNOMEShellDBusHelper.h b/browser/components/shell/nsGNOMEShellDBusHelper.h new file mode 100644 index 0000000000..78522da20d --- /dev/null +++ b/browser/components/shell/nsGNOMEShellDBusHelper.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsGNOMEShellDBusHelper_h__ +#define __nsGNOMEShellDBusHelper_h__ + +#include "mozilla/DBusHelpers.h" +#include "nsIStringBundle.h" +#include "nsINavHistoryService.h" + +#define MAX_SEARCH_RESULTS_NUM 9 +#define KEYWORD_SEARCH_STRING "special:search" +#define KEYWORD_SEARCH_STRING_LEN 14 + +#define DBUS_BUS_NAME "org.mozilla.Firefox.SearchProvider" +#define DBUS_OBJECT_PATH "/org/mozilla/Firefox/SearchProvider" + +class nsGNOMEShellHistorySearchResult; + +DBusHandlerResult DBusIntrospect(DBusConnection* aConnection, + DBusMessage* aMsg); +DBusHandlerResult DBusHandleInitialResultSet( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg); +DBusHandlerResult DBusHandleSubsearchResultSet( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg); +DBusHandlerResult DBusHandleResultMetas( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg); +DBusHandlerResult DBusActivateResult( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg); +DBusHandlerResult DBusLaunchSearch( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, DBusMessage* aMsg); +bool IsHistoryResultNodeURI(nsINavHistoryResultNode* aHistoryNode); + +#endif // __nsGNOMEShellDBusHelper_h__ diff --git a/browser/components/shell/nsGNOMEShellSearchProvider.cpp b/browser/components/shell/nsGNOMEShellSearchProvider.cpp new file mode 100644 index 0000000000..40e6685134 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellSearchProvider.cpp @@ -0,0 +1,443 @@ +/* -*- 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 "nsIWidget.h" +#include "nsToolkitCompsCID.h" +#include "nsIFaviconService.h" +#include "RemoteUtils.h" +#include "base/message_loop.h" // for MessageLoop +#include "base/task.h" // for NewRunnableMethod, etc +#include "nsIServiceManager.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsPrintfCString.h" +#include "nsIIOService.h" + +#include <dbus/dbus.h> +#include <dbus/dbus-glib-lowlevel.h> + +#include "imgIContainer.h" +#include "imgITools.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +class AsyncFaviconDataReady final : public nsIFaviconDataCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIFAVICONDATACALLBACK + + AsyncFaviconDataReady(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + int aIconIndex, int aTimeStamp) + : mSearchResult(aSearchResult), + mIconIndex(aIconIndex), + mTimeStamp(aTimeStamp){}; + + private: + ~AsyncFaviconDataReady() {} + + RefPtr<nsGNOMEShellHistorySearchResult> mSearchResult; + int mIconIndex; + int mTimeStamp; +}; + +NS_IMPL_ISUPPORTS(AsyncFaviconDataReady, nsIFaviconDataCallback) + +// Inspired by SurfaceToPackedBGRA +static UniquePtr<uint8_t[]> SurfaceToPackedRGBA(DataSourceSurface* aSurface) { + IntSize size = aSurface->GetSize(); + CheckedInt<size_t> bufferSize = + CheckedInt<size_t>(size.width * 4) * CheckedInt<size_t>(size.height); + if (!bufferSize.isValid()) { + return nullptr; + } + UniquePtr<uint8_t[]> imageBuffer(new (std::nothrow) + uint8_t[bufferSize.value()]); + if (!imageBuffer) { + return nullptr; + } + + DataSourceSurface::MappedSurface map; + if (!aSurface->Map(DataSourceSurface::MapType::READ, &map)) { + return nullptr; + } + + // Convert BGRA to RGBA + uint32_t* aSrc = (uint32_t*)map.mData; + uint32_t* aDst = (uint32_t*)imageBuffer.get(); + for (int i = 0; i < size.width * size.height; i++, aDst++, aSrc++) { + *aDst = *aSrc & 0xff00ff00; + *aDst |= (*aSrc & 0xff) << 16; + *aDst |= (*aSrc & 0xff0000) >> 16; + } + + aSurface->Unmap(); + + return imageBuffer; +} + +NS_IMETHODIMP +AsyncFaviconDataReady::OnComplete(nsIURI* aFaviconURI, uint32_t aDataLen, + const uint8_t* aData, + const nsACString& aMimeType, + uint16_t aWidth) { + // This is a callback from some previous search so we don't want it + if (mTimeStamp != mSearchResult->GetTimeStamp() || !aData || !aDataLen) { + return NS_ERROR_FAILURE; + } + + // Decode the image from the format it was returned to us in (probably PNG) + nsCOMPtr<imgIContainer> container; + nsCOMPtr<imgITools> imgtool = do_CreateInstance("@mozilla.org/image/tools;1"); + nsresult rv = imgtool->DecodeImageFromBuffer( + reinterpret_cast<const char*>(aData), aDataLen, aMimeType, + getter_AddRefs(container)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<SourceSurface> surface = container->GetFrame( + imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + + if (!surface || surface->GetFormat() != SurfaceFormat::B8G8R8A8) { + return NS_ERROR_FAILURE; + } + + // Allocate a new buffer that we own. + RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface(); + UniquePtr<uint8_t[]> data = SurfaceToPackedRGBA(dataSurface); + if (!data) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mSearchResult->SetHistoryIcon(mTimeStamp, std::move(data), + surface->GetSize().width, + surface->GetSize().height, mIconIndex); + return NS_OK; +} + +DBusHandlerResult nsGNOMEShellSearchProvider::HandleSearchResultSet( + DBusMessage* aMsg, bool aInitialSearch) { + // Discard any existing search results. + mSearchResult = nullptr; + + RefPtr<nsGNOMEShellHistorySearchResult> newSearch = + new nsGNOMEShellHistorySearchResult(this, mConnection, + mSearchResultTimeStamp); + mSearchResultTimeStamp++; + newSearch->SetTimeStamp(mSearchResultTimeStamp); + + // Send the search request over DBus. We'll get reply over DBus it will be + // set to mSearchResult by nsGNOMEShellSearchProvider::SetSearchResult(). + return aInitialSearch + ? DBusHandleInitialResultSet(newSearch.forget(), aMsg) + : DBusHandleSubsearchResultSet(newSearch.forget(), aMsg); +} + +DBusHandlerResult nsGNOMEShellSearchProvider::HandleResultMetas( + DBusMessage* aMsg) { + if (!mSearchResult) { + NS_WARNING("Missing nsGNOMEShellHistorySearchResult."); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + return DBusHandleResultMetas(mSearchResult, aMsg); +} + +DBusHandlerResult nsGNOMEShellSearchProvider::ActivateResult( + DBusMessage* aMsg) { + if (!mSearchResult) { + NS_WARNING("Missing nsGNOMEShellHistorySearchResult."); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + return DBusActivateResult(mSearchResult, aMsg); +} + +DBusHandlerResult nsGNOMEShellSearchProvider::LaunchSearch(DBusMessage* aMsg) { + if (!mSearchResult) { + NS_WARNING("Missing nsGNOMEShellHistorySearchResult."); + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } + return DBusLaunchSearch(mSearchResult, aMsg); +} + +DBusHandlerResult nsGNOMEShellSearchProvider::HandleDBusMessage( + DBusConnection* aConnection, DBusMessage* aMsg) { + NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection."); + + const char* method = dbus_message_get_member(aMsg); + const char* iface = dbus_message_get_interface(aMsg); + + if ((strcmp("Introspect", method) == 0) && + (strcmp("org.freedesktop.DBus.Introspectable", iface) == 0)) { + return DBusIntrospect(mConnection, aMsg); + } + + if (strcmp("org.gnome.Shell.SearchProvider2", iface) == 0) { + if (strcmp("GetInitialResultSet", method) == 0) { + return HandleSearchResultSet(aMsg, /* aInitialSearch */ true); + } + if (strcmp("GetSubsearchResultSet", method) == 0) { + return HandleSearchResultSet(aMsg, /* aInitialSearch */ false); + } + if (strcmp("GetResultMetas", method) == 0) { + return HandleResultMetas(aMsg); + } + if (strcmp("ActivateResult", method) == 0) { + return ActivateResult(aMsg); + } + if (strcmp("LaunchSearch", method) == 0) { + return LaunchSearch(aMsg); + } + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +void nsGNOMEShellSearchProvider::UnregisterDBusInterface( + DBusConnection* aConnection) { + NS_ASSERTION(mConnection == aConnection, "Wrong D-Bus connection."); + // Not implemented +} + +static DBusHandlerResult message_handler(DBusConnection* conn, + DBusMessage* aMsg, void* user_data) { + auto interface = static_cast<nsGNOMEShellSearchProvider*>(user_data); + return interface->HandleDBusMessage(conn, aMsg); +} + +static void unregister(DBusConnection* conn, void* user_data) { + auto interface = static_cast<nsGNOMEShellSearchProvider*>(user_data); + interface->UnregisterDBusInterface(conn); +} + +static DBusObjectPathVTable remoteHandlersTable = { + .unregister_function = unregister, + .message_function = message_handler, +}; + +nsresult nsGNOMEShellSearchProvider::Startup() { + if (mConnection && dbus_connection_get_is_connected(mConnection)) { + // We're already connected so we don't need to reconnect + return NS_ERROR_ALREADY_INITIALIZED; + } + + mConnection = + already_AddRefed<DBusConnection>(dbus_bus_get(DBUS_BUS_SESSION, nullptr)); + if (!mConnection) { + return NS_ERROR_FAILURE; + } + dbus_connection_set_exit_on_disconnect(mConnection, false); + dbus_connection_setup_with_g_main(mConnection, nullptr); + + DBusError err; + dbus_error_init(&err); + dbus_bus_request_name(mConnection, DBUS_BUS_NAME, DBUS_NAME_FLAG_DO_NOT_QUEUE, + &err); + // The interface is already owned - there is another application/profile + // instance already running. + if (dbus_error_is_set(&err)) { + dbus_error_free(&err); + mConnection = nullptr; + return NS_ERROR_FAILURE; + } + + if (!dbus_connection_register_object_path(mConnection, DBUS_OBJECT_PATH, + &remoteHandlersTable, this)) { + mConnection = nullptr; + return NS_ERROR_FAILURE; + } + + mSearchResultTimeStamp = 0; + return NS_OK; +} + +void nsGNOMEShellSearchProvider::Shutdown() { + if (!mConnection) { + return; + } + + dbus_connection_unregister_object_path(mConnection, DBUS_OBJECT_PATH); + + // dbus_connection_unref() will be called by RefPtr here. + mConnection = nullptr; +} + +bool nsGNOMEShellSearchProvider::SetSearchResult( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult) { + MOZ_ASSERT(!mSearchResult); + + if (mSearchResultTimeStamp != aSearchResult->GetTimeStamp()) { + NS_WARNING("Time stamp mismatch."); + return false; + } + mSearchResult = aSearchResult; + return true; +} + +static void DispatchSearchResults( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult, + nsCOMPtr<nsINavHistoryContainerResultNode> aHistResultContainer) { + aSearchResult->ReceiveSearchResultContainer(aHistResultContainer); +} + +nsresult nsGNOMEShellHistoryService::QueryHistory( + RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult) { + if (!mHistoryService) { + mHistoryService = do_GetService(NS_NAVHISTORYSERVICE_CONTRACTID); + if (!mHistoryService) { + return NS_ERROR_FAILURE; + } + } + + nsresult rv; + nsCOMPtr<nsINavHistoryQuery> histQuery; + rv = mHistoryService->GetNewQuery(getter_AddRefs(histQuery)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = histQuery->SetSearchTerms( + NS_ConvertUTF8toUTF16(aSearchResult->GetSearchTerm())); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINavHistoryQueryOptions> histQueryOpts; + rv = mHistoryService->GetNewQueryOptions(getter_AddRefs(histQueryOpts)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = histQueryOpts->SetSortingMode( + nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING); + NS_ENSURE_SUCCESS(rv, rv); + + rv = histQueryOpts->SetMaxResults(MAX_SEARCH_RESULTS_NUM); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINavHistoryResult> histResult; + rv = mHistoryService->ExecuteQuery(histQuery, histQueryOpts, + getter_AddRefs(histResult)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINavHistoryContainerResultNode> resultContainer; + + rv = histResult->GetRoot(getter_AddRefs(resultContainer)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = resultContainer->SetContainerOpen(true); + NS_ENSURE_SUCCESS(rv, rv); + + // Simulate async searching by delayed reply. This search API will + // likely become async in the future and we want to be sure to not rely on + // its current synchronous behavior. + MOZ_ASSERT(MessageLoop::current()); + MessageLoop::current()->PostTask( + NewRunnableFunction("Gnome shell search results", &DispatchSearchResults, + aSearchResult, resultContainer)); + + return NS_OK; +} + +static void DBusGetIDKeyForURI(int aIndex, nsAutoCString& aUri, + nsAutoCString& aIDKey) { + // Compose ID as NN:URL where NN is index to our current history + // result container. + aIDKey = nsPrintfCString("%.2d:%s", aIndex, aUri.get()); +} + +void nsGNOMEShellHistorySearchResult::HandleSearchResultReply() { + MOZ_ASSERT(mReply); + + uint32_t childCount = 0; + nsresult rv = mHistResultContainer->GetChildCount(&childCount); + + DBusMessageIter iter; + dbus_message_iter_init_append(mReply, &iter); + DBusMessageIter iterArray; + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "s", &iterArray); + + if (NS_SUCCEEDED(rv) && childCount > 0) { + // Obtain the favicon service and get the favicon for the specified page + nsCOMPtr<nsIFaviconService> favIconSvc( + do_GetService("@mozilla.org/browser/favicon-service;1")); + nsCOMPtr<nsIIOService> ios(do_GetService(NS_IOSERVICE_CONTRACTID)); + + if (childCount > MAX_SEARCH_RESULTS_NUM) { + childCount = MAX_SEARCH_RESULTS_NUM; + } + + for (uint32_t i = 0; i < childCount; i++) { + nsCOMPtr<nsINavHistoryResultNode> child; + mHistResultContainer->GetChild(i, getter_AddRefs(child)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + if (!IsHistoryResultNodeURI(child)) { + continue; + } + + nsAutoCString uri; + child->GetUri(uri); + + nsCOMPtr<nsIURI> iconIri; + ios->NewURI(uri, nullptr, nullptr, getter_AddRefs(iconIri)); + nsCOMPtr<nsIFaviconDataCallback> callback = + new AsyncFaviconDataReady(this, i, mTimeStamp); + favIconSvc->GetFaviconDataForPage(iconIri, callback, 0); + + nsAutoCString idKey; + DBusGetIDKeyForURI(i, uri, idKey); + + const char* id = idKey.get(); + dbus_message_iter_append_basic(&iterArray, DBUS_TYPE_STRING, &id); + } + } + + nsPrintfCString searchString("%s:%s", KEYWORD_SEARCH_STRING, + mSearchTerm.get()); + const char* search = searchString.get(); + dbus_message_iter_append_basic(&iterArray, DBUS_TYPE_STRING, &search); + + dbus_message_iter_close_container(&iter, &iterArray); + + dbus_connection_send(mConnection, mReply, nullptr); + dbus_message_unref(mReply); + + mReply = nullptr; +} + +void nsGNOMEShellHistorySearchResult::ReceiveSearchResultContainer( + nsCOMPtr<nsINavHistoryContainerResultNode> aHistResultContainer) { + // Propagate search results to nsGNOMEShellSearchProvider. + // SetSearchResult() checks this is up-to-date search (our time stamp matches + // latest requested search timestamp). + if (mSearchProvider->SetSearchResult(this)) { + mHistResultContainer = aHistResultContainer; + HandleSearchResultReply(); + } +} + +void nsGNOMEShellHistorySearchResult::SetHistoryIcon(int aTimeStamp, + UniquePtr<uint8_t[]> aData, + int aWidth, int aHeight, + int aIconIndex) { + MOZ_ASSERT(mTimeStamp == aTimeStamp); + MOZ_RELEASE_ASSERT(aIconIndex < MAX_SEARCH_RESULTS_NUM); + mHistoryIcons[aIconIndex].Set(mTimeStamp, std::move(aData), aWidth, aHeight); +} + +GnomeHistoryIcon* nsGNOMEShellHistorySearchResult::GetHistoryIcon( + int aIconIndex) { + MOZ_RELEASE_ASSERT(aIconIndex < MAX_SEARCH_RESULTS_NUM); + if (mHistoryIcons[aIconIndex].GetTimeStamp() == mTimeStamp && + mHistoryIcons[aIconIndex].IsLoaded()) { + return mHistoryIcons + aIconIndex; + } + return nullptr; +} + +nsGNOMEShellHistoryService* GetGNOMEShellHistoryService() { + static nsGNOMEShellHistoryService gGNOMEShellHistoryService; + return &gGNOMEShellHistoryService; +} diff --git a/browser/components/shell/nsGNOMEShellSearchProvider.h b/browser/components/shell/nsGNOMEShellSearchProvider.h new file mode 100644 index 0000000000..8d7ee802e0 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellSearchProvider.h @@ -0,0 +1,139 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:expandtab:shiftwidth=2:tabstop=2: + */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsGNOMEShellSearchProvider_h__ +#define __nsGNOMEShellSearchProvider_h__ + +#include "mozilla/DBusHelpers.h" +#include "nsINavHistoryService.h" +#include "nsUnixRemoteServer.h" +#include "nsCOMPtr.h" +#include "mozilla/UniquePtr.h" +#include "nsGNOMEShellDBusHelper.h" + +class nsGNOMEShellSearchProvider; + +class GnomeHistoryIcon { + public: + GnomeHistoryIcon() : mTimeStamp(-1), mWidth(0), mHeight(0){}; + + // From which search is this icon + void Set(int aTimeStamp, mozilla::UniquePtr<uint8_t[]> aData, int aWidth, + int aHeight) { + mTimeStamp = aTimeStamp; + mWidth = aWidth; + mHeight = aHeight; + mData = std::move(aData); + } + + bool IsLoaded() { return mData && mWidth > 0 && mHeight > 0; } + int GetTimeStamp() { return mTimeStamp; } + uint8_t* GetData() { return mData.get(); } + int GetWidth() { return mWidth; } + int GetHeight() { return mHeight; } + + private: + int mTimeStamp; + mozilla::UniquePtr<uint8_t[]> mData; + int mWidth; + int mHeight; +}; + +// nsGNOMEShellHistorySearchResult is a container with contains search results +// which are files asynchronously by nsGNOMEShellHistoryService. +// The search results can be opened by Firefox then. +class nsGNOMEShellHistorySearchResult : public nsUnixRemoteServer { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsGNOMEShellHistorySearchResult) + + nsGNOMEShellHistorySearchResult(nsGNOMEShellSearchProvider* aSearchProvider, + DBusConnection* aConnection, int aTimeStamp) + : mSearchProvider(aSearchProvider), + mReply(nullptr), + mConnection(aConnection), + mTimeStamp(aTimeStamp){}; + + void SetReply(DBusMessage* aReply) { mReply = aReply; } + void SetSearchTerm(const char* aSearchTerm) { + mSearchTerm = nsAutoCString(aSearchTerm); + } + DBusConnection* GetDBusConnection() { return mConnection; } + void SetTimeStamp(int aTimeStamp) { mTimeStamp = aTimeStamp; } + int GetTimeStamp() { return mTimeStamp; } + nsAutoCString& GetSearchTerm() { return mSearchTerm; } + + // Receive (asynchronously) history search results from history service. + // This is called asynchronously by nsGNOMEShellHistoryService + // when we have search results available. + void ReceiveSearchResultContainer( + nsCOMPtr<nsINavHistoryContainerResultNode> aHistResultContainer); + + nsCOMPtr<nsINavHistoryContainerResultNode> GetSearchResultContainer() { + return mHistResultContainer; + } + void HandleCommandLine(const char* aBuffer, uint32_t aTimestamp) { + nsUnixRemoteServer::HandleCommandLine(aBuffer, aTimestamp); + } + + void SetHistoryIcon(int aTimeStamp, mozilla::UniquePtr<uint8_t[]> aData, + int aWidth, int aHeight, int aIconIndex); + GnomeHistoryIcon* GetHistoryIcon(int aIconIndex); + + private: + void HandleSearchResultReply(); + + ~nsGNOMEShellHistorySearchResult() = default; + + private: + nsGNOMEShellSearchProvider* mSearchProvider; + nsCOMPtr<nsINavHistoryContainerResultNode> mHistResultContainer; + nsAutoCString mSearchTerm; + DBusMessage* mReply; + DBusConnection* mConnection; + int mTimeStamp; + GnomeHistoryIcon mHistoryIcons[MAX_SEARCH_RESULTS_NUM]; +}; + +class nsGNOMEShellHistoryService { + public: + nsresult QueryHistory(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult); + + private: + nsCOMPtr<nsINavHistoryService> mHistoryService; +}; + +class nsGNOMEShellSearchProvider { + public: + nsGNOMEShellSearchProvider() + : mConnection(nullptr), mSearchResultTimeStamp(0) {} + ~nsGNOMEShellSearchProvider() { Shutdown(); } + + nsresult Startup(); + void Shutdown(); + + DBusHandlerResult HandleDBusMessage(DBusConnection* aConnection, + DBusMessage* msg); + void UnregisterDBusInterface(DBusConnection* aConnection); + + bool SetSearchResult(RefPtr<nsGNOMEShellHistorySearchResult> aSearchResult); + + private: + DBusHandlerResult HandleSearchResultSet(DBusMessage* msg, + bool aInitialSearch); + DBusHandlerResult HandleResultMetas(DBusMessage* msg); + DBusHandlerResult ActivateResult(DBusMessage* msg); + DBusHandlerResult LaunchSearch(DBusMessage* msg); + + // The connection is owned by DBus library + RefPtr<DBusConnection> mConnection; + RefPtr<nsGNOMEShellHistorySearchResult> mSearchResult; + int mSearchResultTimeStamp; +}; + +nsGNOMEShellHistoryService* GetGNOMEShellHistoryService(); + +#endif // __nsGNOMEShellSearchProvider_h__ diff --git a/browser/components/shell/nsGNOMEShellService.cpp b/browser/components/shell/nsGNOMEShellService.cpp new file mode 100644 index 0000000000..c485a29955 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellService.cpp @@ -0,0 +1,520 @@ +/* -*- 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 "nsComponentManagerUtils.h" +#include "nsIImageLoadingContent.h" +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "mozilla/Sprintf.h" +#include "mozilla/dom/Element.h" +#if defined(MOZ_WIDGET_GTK) +# include "nsIImageToPixbuf.h" +#endif +#include "nsXULAppAPI.h" +#include "gfxPlatform.h" + +#include <glib.h> +#include <glib-object.h> +#include <gtk/gtk.h> +#include <gdk/gdk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <limits.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 }, + { "ftp", false }, + { "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" +#define kDesktopImageGSKey "picture-uri" +#define kDesktopOptionGSKey "picture-options" +#define kDesktopDrawBGGSKey "draw-background" +#define kDesktopColorGSKey "primary-color" + +static bool IsRunningAsASnap() { + // SNAP holds the path to the snap, use SNAP_NAME + // which is easier to parse. + const char* snap_name = PR_GetEnv("SNAP_NAME"); + + // return early if not set. + if (snap_name == nullptr) { + return false; + } + + // snap_name as defined on https://snapcraft.io/firefox + return (strcmp(snap_name, "firefox") == 0); +} + +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 + const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + if (currentDesktop && strstr(currentDesktop, "GNOME") != nullptr && + 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 (IsRunningAsASnap()) { + const gchar* argv[] = {"xdg-settings", "check", "default-web-browser", + "firefox.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 (giovfs) { + handler.Truncate(); + nsCOMPtr<nsIHandlerApp> handlerApp; + giovfs->GetAppForURIScheme(nsDependentCString(appProtocols[i].name), + getter_AddRefs(handlerApp)); + gioApp = do_QueryInterface(handlerApp); + if (!gioApp) return NS_OK; + + gioApp->GetCommand(handler); + + if (!CheckHandlerMatchesAppName(handler)) + return NS_OK; // the handler is set to another app + } + } + + *aIsDefaultBrowser = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsGNOMEShellService::SetDefaultBrowser(bool aClaimAllTypes, bool aForAllUsers) { +#ifdef DEBUG + if (aForAllUsers) + NS_WARNING( + "Setting the default browser for all users is not yet supported"); +#endif + + if (IsRunningAsASnap()) { + const gchar* argv[] = {"xdg-settings", "set", "default-web-browser", + "firefox.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 = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStringBundle> brandBundle; + rv = bundleService->CreateBundle(BRAND_PROPERTIES, + getter_AddRefs(brandBundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString brandShortName; + brandBundle->GetStringFromName("brandShortName", brandShortName); + + // use brandShortName as the application id. + NS_ConvertUTF16toUTF8 id(brandShortName); + nsCOMPtr<nsIGIOMimeApp> appInfo; + rv = giovfs->FindAppFromCommand(mAppPath, getter_AddRefs(appInfo)); + if (NS_FAILED(rv)) { + // Application was not found in the list of installed applications + // provided by OS. Fallback to create appInfo from command and name. + rv = giovfs->CreateAppFromCommand(mAppPath, id, getter_AddRefs(appInfo)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // set handler for the protocols + for (unsigned int i = 0; i < ArrayLength(appProtocols); ++i) { + if (appProtocols[i].essential || aClaimAllTypes) { + appInfo->SetAsDefaultForURIScheme( + nsDependentCString(appProtocols[i].name)); + } + } + + // set handler for .html and xhtml files and MIME types: + if (aClaimAllTypes) { + // Add mime types for html, xhtml extension and set app to just created + // appinfo. + for (unsigned int i = 0; i < ArrayLength(appTypes); ++i) { + appInfo->SetAsDefaultForMimeType( + nsDependentCString(appTypes[i].mimeType)); + appInfo->SetAsDefaultForFileExtensions( + nsDependentCString(appTypes[i].extensions)); + } + } + } + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + (void)prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true); + // Reset the number of times the dialog should be shown + // before it is silenced. + (void)prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsGNOMEShellService::GetCanSetDesktopBackground(bool* aResult) { + // setting desktop background is currently only supported + // for Gnome or desktops using the same GSettings keys + const char* currentDesktop = getenv("XDG_CURRENT_DESKTOP"); + if (currentDesktop && strstr(currentDesktop, "GNOME") != nullptr) { + *aResult = true; + return NS_OK; + } + + const char* gnomeSession = getenv("GNOME_DESKTOP_SESSION_ID"); + if (gnomeSession) { + *aResult = true; + } else { + *aResult = false; + } + + return NS_OK; +} + +static nsresult WriteImage(const nsCString& aPath, imgIContainer* aImage) { +#if !defined(MOZ_WIDGET_GTK) + return NS_ERROR_NOT_AVAILABLE; +#else + nsCOMPtr<nsIImageToPixbuf> imgToPixbuf = + do_GetService("@mozilla.org/widget/image-to-gdk-pixbuf;1"); + if (!imgToPixbuf) return NS_ERROR_NOT_AVAILABLE; + + GdkPixbuf* pixbuf = imgToPixbuf->ConvertImageToPixbuf(aImage); + if (!pixbuf) return NS_ERROR_NOT_AVAILABLE; + + gboolean res = gdk_pixbuf_save(pixbuf, aPath.get(), "png", nullptr, nullptr); + + g_object_unref(pixbuf); + return res ? NS_OK : NS_ERROR_FAILURE; +#endif +} + +NS_IMETHODIMP +nsGNOMEShellService::SetDesktopBackground(dom::Element* aElement, + int32_t aPosition, + const nsACString& aImageName) { + 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 rv; + + // 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")); + + // get the product brand name from localized strings + nsAutoString brandName; + nsCID bundleCID = NS_STRINGBUNDLESERVICE_CID; + nsCOMPtr<nsIStringBundleService> bundleService(do_GetService(bundleCID)); + if (bundleService) { + nsCOMPtr<nsIStringBundle> brandBundle; + rv = bundleService->CreateBundle(BRAND_PROPERTIES, + getter_AddRefs(brandBundle)); + if (NS_SUCCEEDED(rv) && brandBundle) { + rv = brandBundle->GetStringFromName("brandShortName", brandName); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // 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 + rv = WriteImage(filePath, container); + NS_ENSURE_SUCCESS(rv, rv); + + 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) { + gchar* file_uri = g_filename_to_uri(filePath.get(), nullptr, nullptr); + if (!file_uri) return NS_ERROR_FAILURE; + + background_settings->SetString(nsLiteralCString(kDesktopOptionGSKey), + options); + + background_settings->SetString(nsLiteralCString(kDesktopImageGSKey), + nsDependentCString(file_uri)); + g_free(file_uri); + background_settings->SetBoolean(nsLiteralCString(kDesktopDrawBGGSKey), + true); + return rv; + } + } + + return NS_ERROR_FAILURE; +} + +#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(nsLiteralCString(kDesktopBGSchema), + getter_AddRefs(background_settings)); + if (background_settings) { + background_settings->GetString(nsLiteralCString(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(nsLiteralCString(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..c1a48135e8 --- /dev/null +++ b/browser/components/shell/nsGNOMEShellService.h @@ -0,0 +1,43 @@ +/* -*- 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" +#include "mozilla/Attributes.h" +#ifdef MOZ_ENABLE_DBUS +# include "nsGNOMEShellSearchProvider.h" +#endif + +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; + +#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..842ce5e8a7 --- /dev/null +++ b/browser/components/shell/nsIGNOMEShellService.idl @@ -0,0 +1,19 @@ +/* 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; +}; + diff --git a/browser/components/shell/nsIMacShellService.idl b/browser/components/shell/nsIMacShellService.idl new file mode 100644 index 0000000000..52f97ab4a5 --- /dev/null +++ b/browser/components/shell/nsIMacShellService.idl @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIShellService.idl" + +[scriptable, uuid(387fdc80-0077-4b60-a0d9-d9e80a83ba64)] +interface nsIMacShellService : nsIShellService +{ + /** + * Opens the desktop preferences, e.g. for after setting the background. + */ + void showDesktopPreferences(); + + /** + * @param aPaneID used by macOS to identify the pane to open. + * Example arguments: + * * "" - use the default Security and Privacy pane. + * * General + * * Privacy + * * Privacy_AllFiles + * * Privacy_Camera + * * Privacy_Microphone + */ + void showSecurityPreferences(in ACString aPaneID); +}; diff --git a/browser/components/shell/nsIShellService.idl b/browser/components/shell/nsIShellService.idl new file mode 100644 index 0000000000..a3fa434123 --- /dev/null +++ b/browser/components/shell/nsIShellService.idl @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIFile; + +webidl Element; + +[scriptable, uuid(2d1a95e4-5bd8-4eeb-b0a8-c1455fd2a357)] +interface nsIShellService : nsISupports +{ + /** + * Determines whether or not Firefox is the "Default Browser." + * This is simply whether or not Firefox is registered to handle + * http links. + * + * @param aForAllTypes true if the check should be made for HTTP and HTML. + * false if the check should be made for HTTP only. + * This parameter may be ignored on some platforms. + */ + boolean isDefaultBrowser([optional] in boolean aForAllTypes); + + /** + * Registers Firefox as the "Default Browser." + * + * @param aClaimAllTypes Register Firefox as the handler for + * additional protocols (ftp, chrome etc) + * and web documents (.html, .xhtml etc). + * @param aForAllUsers Whether or not Firefox should attempt + * to become the default browser for all + * users on a multi-user system. + */ + void setDefaultBrowser(in boolean aClaimAllTypes, in boolean aForAllUsers); + + /** + * Flags for positioning/sizing of the Desktop Background image. + */ + const long BACKGROUND_TILE = 1; + const long BACKGROUND_STRETCH = 2; + const long BACKGROUND_CENTER = 3; + const long BACKGROUND_FILL = 4; + const long BACKGROUND_FIT = 5; + const long BACKGROUND_SPAN = 6; + + /** + * Sets the desktop background image using either the HTML <IMG> + * element supplied or the background image of the element supplied. + * + * @param aImageElement Either a HTML <IMG> element or an element with + * a background image from which to source the + * background image. + * @param aPosition How to place the image on the desktop + * @param aImageName The image name. Equivalent to the leaf name of the + * location.href. + */ + void setDesktopBackground(in Element aElement, + in long aPosition, + in ACString aImageName); + + /** + * The desktop background color, visible when no background image is + * used, or if the background image is centered and does not fill the + * entire screen. A rgb value, where (r << 16 | g << 8 | b) + */ + attribute unsigned long desktopBackgroundColor; +}; diff --git a/browser/components/shell/nsIWindowsShellService.idl b/browser/components/shell/nsIWindowsShellService.idl new file mode 100644 index 0000000000..4a4e5e484b --- /dev/null +++ b/browser/components/shell/nsIWindowsShellService.idl @@ -0,0 +1,15 @@ +/* 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 +{ + void createShortcut(in nsIFile aBinary, in Array<AString> aArguments, + in AString aDescription, in nsIFile aIconFile, in AString aAppUserModelId, + in nsIFile aTarget); +}; diff --git a/browser/components/shell/nsMacShellService.cpp b/browser/components/shell/nsMacShellService.cpp new file mode 100644 index 0000000000..c07b46392e --- /dev/null +++ b/browser/components/shell/nsMacShellService.cpp @@ -0,0 +1,282 @@ +/* -*- 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 "DesktopBackgroundImage.h" + +#include <Carbon/Carbon.h> +#include <CoreFoundation/CoreFoundation.h> +#include <ApplicationServices/ApplicationServices.h> + +using mozilla::dom::Element; +using mozilla::widget::SetDesktopImage; + +#define NETWORK_PREFPANE "/System/Library/PreferencePanes/Network.prefPane"_ns +#define DESKTOP_PREFPANE \ + nsLiteralCString( \ + "/System/Library/PreferencePanes/DesktopScreenEffectsPref.prefPane") + +#define SAFARI_BUNDLE_IDENTIFIER "com.apple.Safari" + +NS_IMPL_ISUPPORTS(nsMacShellService, nsIMacShellService, nsIShellService, + nsIToolkitShellService, nsIWebProgressListener) + +NS_IMETHODIMP +nsMacShellService::IsDefaultBrowser(bool aForAllTypes, + bool* aIsDefaultBrowser) { + *aIsDefaultBrowser = false; + + CFStringRef firefoxID = ::CFBundleGetIdentifier(::CFBundleGetMainBundle()); + if (!firefoxID) { + // CFBundleGetIdentifier is expected to return nullptr only if the specified + // bundle doesn't have a bundle identifier in its plist. In this case, that + // means a failure, since our bundle does have an identifier. + return NS_ERROR_FAILURE; + } + + // Get the default http handler's bundle ID (or nullptr if it has not been + // explicitly set) + CFStringRef defaultBrowserID = + ::LSCopyDefaultHandlerForURLScheme(CFSTR("http")); + if (defaultBrowserID) { + *aIsDefaultBrowser = + ::CFStringCompare(firefoxID, defaultBrowserID, 0) == kCFCompareEqualTo; + ::CFRelease(defaultBrowserID); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMacShellService::SetDefaultBrowser(bool aClaimAllTypes, bool aForAllUsers) { + // Note: We don't support aForAllUsers on Mac OS X. + + CFStringRef firefoxID = ::CFBundleGetIdentifier(::CFBundleGetMainBundle()); + if (!firefoxID) { + return NS_ERROR_FAILURE; + } + + if (::LSSetDefaultHandlerForURLScheme(CFSTR("http"), firefoxID) != noErr) { + return NS_ERROR_FAILURE; + } + if (::LSSetDefaultHandlerForURLScheme(CFSTR("https"), firefoxID) != noErr) { + return NS_ERROR_FAILURE; + } + + if (aClaimAllTypes) { + if (::LSSetDefaultHandlerForURLScheme(CFSTR("ftp"), 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); +} + +NS_IMETHODIMP +nsMacShellService::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + return NS_OK; +} + +NS_IMETHODIMP +nsMacShellService::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aLocation, + uint32_t aFlags) { + return NS_OK; +} + +NS_IMETHODIMP +nsMacShellService::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + return NS_OK; +} + +NS_IMETHODIMP +nsMacShellService::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + return NS_OK; +} + +NS_IMETHODIMP +nsMacShellService::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + return NS_OK; +} + +NS_IMETHODIMP +nsMacShellService::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) { + if (NS_SUCCEEDED(aStatus) && (aStateFlags & STATE_STOP) && + (aRequest == nullptr)) { + nsCOMPtr<nsIObserverService> os( + do_GetService("@mozilla.org/observer-service;1")); + if (os) + os->NotifyObservers(nullptr, "shell:desktop-background-changed", nullptr); + + bool exists = false; + nsresult rv = mBackgroundFile->Exists(&exists); + if (NS_FAILED(rv) || !exists) { + return NS_OK; + } + + SetDesktopImage(mBackgroundFile); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMacShellService::ShowDesktopPreferences() { + nsCOMPtr<nsIFile> lf; + nsresult rv = + NS_NewNativeLocalFile(DESKTOP_PREFPANE, true, getter_AddRefs(lf)); + NS_ENSURE_SUCCESS(rv, rv); + bool exists; + lf->Exists(&exists); + if (!exists) return NS_ERROR_FILE_NOT_FOUND; + return lf->Launch(); +} + +NS_IMETHODIMP +nsMacShellService::GetDesktopBackgroundColor(uint32_t* aColor) { + // This method and |SetDesktopBackgroundColor| has no meaning on Mac OS X. + // The mac desktop preferences UI uses pictures for the few solid colors it + // supports. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMacShellService::SetDesktopBackgroundColor(uint32_t aColor) { + // This method and |GetDesktopBackgroundColor| has no meaning on Mac OS X. + // The mac desktop preferences UI uses pictures for the few solid colors it + // supports. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMacShellService::ShowSecurityPreferences(const nsACString& aPaneID) { + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + CFStringRef paneID = ::CFStringCreateWithBytes( + kCFAllocatorDefault, (const UInt8*)PromiseFlatCString(aPaneID).get(), + aPaneID.Length(), kCFStringEncodingUTF8, false); + + if (paneID) { + CFStringRef format = + CFSTR("x-apple.systempreferences:com.apple.preference.security?%@"); + if (format) { + CFStringRef urlStr = + CFStringCreateWithFormat(kCFAllocatorDefault, NULL, format, paneID); + if (urlStr) { + CFURLRef url = ::CFURLCreateWithString(NULL, urlStr, NULL); + rv = CocoaFileUtils::OpenURL(url); + + ::CFRelease(urlStr); + } + + ::CFRelease(format); + } + + ::CFRelease(paneID); + } + return rv; +} diff --git a/browser/components/shell/nsMacShellService.h b/browser/components/shell/nsMacShellService.h new file mode 100644 index 0000000000..bb08c72b4e --- /dev/null +++ b/browser/components/shell/nsMacShellService.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsmacshellservice_h____ +#define nsmacshellservice_h____ + +#include "nsToolkitShellService.h" +#include "nsIMacShellService.h" +#include "nsIWebProgressListener.h" +#include "nsIFile.h" +#include "nsCOMPtr.h" + +class nsMacShellService : public nsIMacShellService, + public nsToolkitShellService, + public nsIWebProgressListener { + public: + nsMacShellService(){}; + + NS_DECL_ISUPPORTS + NS_DECL_NSISHELLSERVICE + NS_DECL_NSIMACSHELLSERVICE + NS_DECL_NSIWEBPROGRESSLISTENER + + protected: + virtual ~nsMacShellService(){}; + + private: + nsCOMPtr<nsIFile> mBackgroundFile; +}; + +#endif // nsmacshellservice_h____ diff --git a/browser/components/shell/nsShellService.h b/browser/components/shell/nsShellService.h new file mode 100644 index 0000000000..346a16ad69 --- /dev/null +++ b/browser/components/shell/nsShellService.h @@ -0,0 +1,11 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#define PREF_CHECKDEFAULTBROWSER "browser.shell.checkDefaultBrowser" +#define PREF_DEFAULTBROWSERCHECKCOUNT "browser.shell.defaultBrowserCheckCount" + +#define SHELLSERVICE_PROPERTIES \ + "chrome://browser/locale/shellservice.properties" +#define BRAND_PROPERTIES "chrome://branding/locale/brand.properties" diff --git a/browser/components/shell/nsToolkitShellService.h b/browser/components/shell/nsToolkitShellService.h new file mode 100644 index 0000000000..552f3cd65d --- /dev/null +++ b/browser/components/shell/nsToolkitShellService.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nstoolkitshellservice_h____ +#define nstoolkitshellservice_h____ + +#include "nsIToolkitShellService.h" + +class nsToolkitShellService : public nsIToolkitShellService { + public: + NS_IMETHOD IsDefaultBrowser(bool aForAllTypes, bool* aIsDefaultBrowser) = 0; + + NS_IMETHODIMP IsDefaultApplication(bool* aIsDefaultBrowser) { + // Only care about the http(s) protocol. This only matters on Windows. + return IsDefaultBrowser(false, aIsDefaultBrowser); + } +}; + +#endif // nstoolkitshellservice_h____ diff --git a/browser/components/shell/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp new file mode 100644 index 0000000000..87f10cb80f --- /dev/null +++ b/browser/components/shell/nsWindowsShellService.cpp @@ -0,0 +1,626 @@ +/* -*- 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 "nsWindowsShellService.h" + +#include "BinaryPath.h" +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "mozilla/RefPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsIContent.h" +#include "nsIImageLoadingContent.h" +#include "nsIOutputStream.h" +#include "nsIPrefService.h" +#include "nsIStringBundle.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsShellService.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIWindowsRegKey.h" +#include "nsUnicharUtils.h" +#include "nsIURLFormatter.h" +#include "nsXULAppAPI.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/dom/Element.h" +#include "mozilla/gfx/2D.h" +#include "WindowsDefaultBrowser.h" + +#include "windows.h" +#include "shellapi.h" +#include <propvarutil.h> +#include <propkey.h> + +#include <shlobj.h> +#include "WinUtils.h" + +#include <mbstring.h> + +#undef ACCESS_READ + +#ifndef MAX_BUF +# define MAX_BUF 4096 +#endif + +#define REG_SUCCEEDED(val) (val == ERROR_SUCCESS) + +#define REG_FAILED(val) (val != ERROR_SUCCESS) + +#ifdef DEBUG +# define NS_ENSURE_HRESULT(hres, ret) \ + do { \ + HRESULT result = hres; \ + if (MOZ_UNLIKELY(FAILED(result))) { \ + mozilla::SmprintfPointer msg = mozilla::Smprintf( \ + "NS_ENSURE_HRESULT(%s, %s) failed with " \ + "result 0x%" PRIX32, \ + #hres, #ret, static_cast<uint32_t>(result)); \ + NS_WARNING(msg.get()); \ + return ret; \ + } \ + } while (false) +#else +# define NS_ENSURE_HRESULT(hres, ret) \ + if (MOZ_UNLIKELY(FAILED(hres))) return ret +#endif + +using mozilla::IsWin8OrLater; +using namespace mozilla; + +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) { + // Make sure the Prog ID matches what we have + 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; + } + + LPCWSTR progID = isProtocol ? L"FirefoxURL" : L"FirefoxHTML"; + bool isDefault = !wcsnicmp(registeredApp, progID, wcslen(progID)); + + nsAutoString regAppName(registeredApp); + CoTaskMemFree(registeredApp); + + if (isDefault) { + // 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; + } + + wchar_t fullCmd[MAX_BUF] = L""; + _snwprintf(fullCmd, MAX_BUF, L"\"%s\" -osint -url \"%%1\"", exePath); + + isDefault = _wcsicmp(fullCmd, cmdFromReg) == 0; + } + + return isDefault; +} + +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; +} + +nsresult nsWindowsShellService::LaunchControlPanelDefaultsSelectionUI() { + IApplicationAssociationRegistrationUI* pAARUI; + HRESULT hr = CoCreateInstance( + CLSID_ApplicationAssociationRegistrationUI, NULL, CLSCTX_INPROC, + IID_IApplicationAssociationRegistrationUI, (void**)&pAARUI); + if (SUCCEEDED(hr)) { + mozilla::UniquePtr<wchar_t[]> appRegName; + GetAppRegName(appRegName); + hr = pAARUI->LaunchAdvancedAssociationUI(appRegName.get()); + pAARUI->Release(); + } + return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsWindowsShellService::LaunchControlPanelDefaultPrograms() { + return ::LaunchControlPanelDefaultPrograms() ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsWindowsShellService::LaunchModernSettingsDialogDefaultApps() { + return ::LaunchModernSettingsDialogDefaultApps() ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsWindowsShellService::InvokeHTTPOpenAsVerb() { + nsCOMPtr<nsIURLFormatter> formatter( + do_GetService("@mozilla.org/toolkit/URLFormatterService;1")); + if (!formatter) { + return NS_ERROR_UNEXPECTED; + } + + nsString urlStr; + nsresult rv = formatter->FormatURLPref(u"app.support.baseURL"_ns, urlStr); + if (NS_FAILED(rv)) { + return rv; + } + if (!StringBeginsWith(urlStr, u"https://"_ns)) { + return NS_ERROR_FAILURE; + } + urlStr.AppendLiteral("win10-default-browser"); + + SHELLEXECUTEINFOW seinfo = {sizeof(SHELLEXECUTEINFOW)}; + seinfo.lpVerb = L"openas"; + seinfo.lpFile = urlStr.get(); + seinfo.nShow = SW_SHOWNORMAL; + if (!ShellExecuteExW(&seinfo)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +nsresult nsWindowsShellService::LaunchHTTPHandlerPane() { + OPENASINFO info; + info.pcszFile = L"http"; + info.pcszClass = nullptr; + info.oaifInFlags = + OAIF_FORCE_REGISTRATION | OAIF_URL_PROTOCOL | OAIF_REGISTER_EXT; + + HRESULT hr = SHOpenWithDialog(nullptr, &info); + if (SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED))) { + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsWindowsShellService::SetDefaultBrowser(bool aClaimAllTypes, + bool aForAllUsers) { + nsAutoString appHelperPath; + if (NS_FAILED(GetHelperPath(appHelperPath))) return NS_ERROR_FAILURE; + + if (aForAllUsers) { + appHelperPath.AppendLiteral(" /SetAsDefaultAppGlobal"); + } else { + appHelperPath.AppendLiteral(" /SetAsDefaultAppUser"); + } + + nsresult rv = LaunchHelper(appHelperPath); + if (NS_SUCCEEDED(rv) && IsWin8OrLater()) { + if (aClaimAllTypes) { + if (IsWin10OrLater()) { + rv = LaunchModernSettingsDialogDefaultApps(); + } else { + rv = LaunchControlPanelDefaultsSelectionUI(); + } + // The above call should never really fail, but just in case + // fall back to showing the HTTP association screen only. + if (NS_FAILED(rv)) { + if (IsWin10OrLater()) { + rv = InvokeHTTPOpenAsVerb(); + } else { + rv = LaunchHTTPHandlerPane(); + } + } + } else { + // Windows 10 blocks attempts to load the + // HTTP Handler association dialog. + if (IsWin10OrLater()) { + rv = LaunchModernSettingsDialogDefaultApps(); + } else { + rv = LaunchHTTPHandlerPane(); + } + + // The above call should never really fail, but just in case + // fall back to showing control panel for all defaults + if (NS_FAILED(rv)) { + rv = LaunchControlPanelDefaultsSelectionUI(); + } + } + } + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + (void)prefs->SetBoolPref(PREF_CHECKDEFAULTBROWSER, true); + // Reset the number of times the dialog should be shown + // before it is silenced. + (void)prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, 0); + } + + return rv; +} + +static nsresult WriteBitmap(nsIFile* aFile, imgIContainer* aImage) { + nsresult rv; + + RefPtr<gfx::SourceSurface> surface = aImage->GetFrame( + imgIContainer::FRAME_FIRST, imgIContainer::FLAG_SYNC_DECODE); + NS_ENSURE_TRUE(surface, NS_ERROR_FAILURE); + + // For either of the following formats we want to set the biBitCount member + // of the BITMAPINFOHEADER struct to 32, below. For that value the bitmap + // format defines that the A8/X8 WORDs in the bitmap byte stream be ignored + // for the BI_RGB value we use for the biCompression member. + MOZ_ASSERT(surface->GetFormat() == gfx::SurfaceFormat::B8G8R8A8 || + surface->GetFormat() == gfx::SurfaceFormat::B8G8R8X8); + + RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface(); + NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE); + + int32_t width = dataSurface->GetSize().width; + int32_t height = dataSurface->GetSize().height; + int32_t bytesPerPixel = 4 * sizeof(uint8_t); + uint32_t bytesPerRow = bytesPerPixel * width; + + // initialize these bitmap structs which we will later + // serialize directly to the head of the bitmap file + BITMAPINFOHEADER bmi; + bmi.biSize = sizeof(BITMAPINFOHEADER); + bmi.biWidth = width; + bmi.biHeight = height; + bmi.biPlanes = 1; + bmi.biBitCount = (WORD)bytesPerPixel * 8; + bmi.biCompression = BI_RGB; + bmi.biSizeImage = bytesPerRow * height; + bmi.biXPelsPerMeter = 0; + bmi.biYPelsPerMeter = 0; + bmi.biClrUsed = 0; + bmi.biClrImportant = 0; + + BITMAPFILEHEADER bf; + bf.bfType = 0x4D42; // 'BM' + bf.bfReserved1 = 0; + bf.bfReserved2 = 0; + bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); + bf.bfSize = bf.bfOffBits + bmi.biSizeImage; + + // get a file output stream + nsCOMPtr<nsIOutputStream> stream; + rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile); + NS_ENSURE_SUCCESS(rv, rv); + + gfx::DataSourceSurface::MappedSurface map; + if (!dataSurface->Map(gfx::DataSourceSurface::MapType::READ, &map)) { + return NS_ERROR_FAILURE; + } + + // write the bitmap headers and rgb pixel data to the file + rv = NS_ERROR_FAILURE; + if (stream) { + uint32_t written; + stream->Write((const char*)&bf, sizeof(BITMAPFILEHEADER), &written); + if (written == sizeof(BITMAPFILEHEADER)) { + stream->Write((const char*)&bmi, sizeof(BITMAPINFOHEADER), &written); + if (written == sizeof(BITMAPINFOHEADER)) { + // write out the image data backwards because the desktop won't + // show bitmaps with negative heights for top-to-bottom + uint32_t i = map.mStride * height; + do { + i -= map.mStride; + stream->Write(((const char*)map.mData) + i, bytesPerRow, &written); + if (written == bytesPerRow) { + rv = NS_OK; + } else { + rv = NS_ERROR_FAILURE; + break; + } + } while (i != 0); + } + } + + stream->Close(); + } + + dataSurface->Unmap(); + + return rv; +} + +NS_IMETHODIMP +nsWindowsShellService::SetDesktopBackground(dom::Element* aElement, + int32_t aPosition, + const nsACString& aImageName) { + if (!aElement || !aElement->IsHTMLElement(nsGkAtoms::img)) { + // XXX write background loading stuff! + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv; + nsCOMPtr<nsIImageLoadingContent> imageContent = + do_QueryInterface(aElement, &rv); + if (!imageContent) return rv; + + // get the image container + nsCOMPtr<imgIRequest> request; + rv = imageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(request)); + if (!request) return rv; + + nsCOMPtr<imgIContainer> container; + rv = request->GetImage(getter_AddRefs(container)); + if (!container) return NS_ERROR_FAILURE; + + // get the file name from localized strings + nsCOMPtr<nsIStringBundleService> bundleService( + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStringBundle> shellBundle; + rv = bundleService->CreateBundle(SHELLSERVICE_PROPERTIES, + getter_AddRefs(shellBundle)); + NS_ENSURE_SUCCESS(rv, rv); + + // e.g. "Desktop Background.bmp" + nsAutoString fileLeafName; + rv = shellBundle->GetStringFromName("desktopBackgroundLeafNameWin", + fileLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + // get the profile root directory + nsCOMPtr<nsIFile> file; + rv = NS_GetSpecialDirectory(NS_APP_APPLICATION_REGISTRY_DIR, + getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + // eventually, the path is "%APPDATA%\Mozilla\Firefox\Desktop Background.bmp" + rv = file->Append(fileLeafName); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString path; + rv = file->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + // write the bitmap to a file in the profile directory. + // We have to write old bitmap format for Windows 7 wallpapar support. + rv = WriteBitmap(file, container); + + // if the file was written successfully, set it as the system wallpaper + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + u"Control Panel\\Desktop"_ns, + nsIWindowsRegKey::ACCESS_SET_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString tile; + nsAutoString style; + switch (aPosition) { + case BACKGROUND_TILE: + style.Assign('0'); + tile.Assign('1'); + break; + case BACKGROUND_CENTER: + style.Assign('0'); + tile.Assign('0'); + break; + case BACKGROUND_STRETCH: + style.Assign('2'); + tile.Assign('0'); + break; + case BACKGROUND_FILL: + style.AssignLiteral("10"); + tile.Assign('0'); + break; + case BACKGROUND_FIT: + style.Assign('6'); + tile.Assign('0'); + break; + case BACKGROUND_SPAN: + style.AssignLiteral("22"); + tile.Assign('0'); + break; + } + + rv = regKey->WriteStringValue(u"TileWallpaper"_ns, tile); + NS_ENSURE_SUCCESS(rv, rv); + rv = regKey->WriteStringValue(u"WallpaperStyle"_ns, style); + NS_ENSURE_SUCCESS(rv, rv); + rv = regKey->Close(); + NS_ENSURE_SUCCESS(rv, rv); + + ::SystemParametersInfoW(SPI_SETDESKWALLPAPER, 0, (PVOID)path.get(), + SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + } + return rv; +} + +NS_IMETHODIMP +nsWindowsShellService::GetDesktopBackgroundColor(uint32_t* aColor) { + uint32_t color = ::GetSysColor(COLOR_DESKTOP); + *aColor = + (GetRValue(color) << 16) | (GetGValue(color) << 8) | GetBValue(color); + return NS_OK; +} + +NS_IMETHODIMP +nsWindowsShellService::SetDesktopBackgroundColor(uint32_t aColor) { + int aParameters[2] = {COLOR_BACKGROUND, COLOR_DESKTOP}; + BYTE r = (aColor >> 16); + BYTE g = (aColor << 16) >> 24; + BYTE b = (aColor << 24) >> 24; + COLORREF colors[2] = {RGB(r, g, b), RGB(r, g, b)}; + + ::SetSysColors(sizeof(aParameters) / sizeof(int), aParameters, colors); + + nsresult rv; + nsCOMPtr<nsIWindowsRegKey> regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = regKey->Create(nsIWindowsRegKey::ROOT_KEY_CURRENT_USER, + u"Control Panel\\Colors"_ns, + nsIWindowsRegKey::ACCESS_SET_VALUE); + NS_ENSURE_SUCCESS(rv, rv); + + wchar_t rgb[12]; + _snwprintf(rgb, 12, L"%u %u %u", r, g, b); + + rv = regKey->WriteStringValue(u"Background"_ns, nsDependentString(rgb)); + NS_ENSURE_SUCCESS(rv, rv); + + return regKey->Close(); +} + +NS_IMETHODIMP +nsWindowsShellService::CreateShortcut(nsIFile* aBinary, + const nsTArray<nsString>& aArguments, + const nsAString& aDescription, + nsIFile* aIconFile, + const nsAString& aAppUserModelId, + nsIFile* aTarget) { + NS_ENSURE_ARG(aBinary); + NS_ENSURE_ARG(aTarget); + + 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()); + + 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\" ", arg.get()); + } + + link->SetArguments(arguments.get()); + + if (aIconFile) { + nsString icon(aIconFile->NativePath()); + link->SetIconLocation(icon.get(), 0); + } + + 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); + + nsString target(aTarget->NativePath()); + hr = persist->Save(target.get(), TRUE); + NS_ENSURE_HRESULT(hr, NS_ERROR_FAILURE); + + return NS_OK; +} + +nsWindowsShellService::nsWindowsShellService() {} + +nsWindowsShellService::~nsWindowsShellService() {} diff --git a/browser/components/shell/nsWindowsShellService.h b/browser/components/shell/nsWindowsShellService.h new file mode 100644 index 0000000000..6127f687bd --- /dev/null +++ b/browser/components/shell/nsWindowsShellService.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nswindowsshellservice_h____ +#define nswindowsshellservice_h____ + +#include "nscore.h" +#include "nsString.h" +#include "nsToolkitShellService.h" +#include "nsIShellService.h" +#include "nsIWindowsShellService.h" + +#include <windows.h> +#include <ole2.h> + +class nsWindowsShellService : public nsIShellService, + public nsToolkitShellService, + public nsIWindowsShellService { + virtual ~nsWindowsShellService(); + + public: + nsWindowsShellService(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISHELLSERVICE + NS_DECL_NSIWINDOWSSHELLSERVICE + + protected: + nsresult LaunchControlPanelDefaultsSelectionUI(); + nsresult LaunchControlPanelDefaultPrograms(); + nsresult LaunchModernSettingsDialogDefaultApps(); + nsresult InvokeHTTPOpenAsVerb(); + nsresult LaunchHTTPHandlerPane(); +}; + +#endif // nswindowsshellservice_h____ diff --git a/browser/components/shell/search-provider-files/README b/browser/components/shell/search-provider-files/README new file mode 100644 index 0000000000..81ea16d9b6 --- /dev/null +++ b/browser/components/shell/search-provider-files/README @@ -0,0 +1,20 @@ +In order to get gnome shell search provider registered and active +you need to install the firefox-search-provider.ini and firefox.desktop files system wide. + +The locations may be distro specific, for instance Fedora and Ubuntu expect +the files at: + +/usr/share/gnome-shell/search-providers/firefox-search-provider.ini +/usr/share/applications/firefox.desktop + +firefox.desktop is a system-wide Firefox launcher. It may come with your +distribution or you can use this one. Update name of firefox desktop file at firefox-search-provider.ini +according your actual file at /usr/share/applications. + +firefox-search-provider.ini registers Firefox as a search provider. +When the file is correctly installed you can see Firefox as a searchable application +at Settings -> Search at Gnome controll center. + +Gnome shell search provider is active only when Firefox is running. When it's active +you can see it as org.mozilla.Firefox.SearchProvider D-Bus service. + diff --git a/browser/components/shell/search-provider-files/firefox-search-provider.ini b/browser/components/shell/search-provider-files/firefox-search-provider.ini new file mode 100644 index 0000000000..3868e3d528 --- /dev/null +++ b/browser/components/shell/search-provider-files/firefox-search-provider.ini @@ -0,0 +1,5 @@ +[Shell Search Provider] +DesktopId=firefox.desktop +BusName=org.mozilla.Firefox.SearchProvider +ObjectPath=/org/mozilla/Firefox/SearchProvider +Version=2 diff --git a/browser/components/shell/search-provider-files/firefox.desktop b/browser/components/shell/search-provider-files/firefox.desktop new file mode 100644 index 0000000000..118aae6b73 --- /dev/null +++ b/browser/components/shell/search-provider-files/firefox.desktop @@ -0,0 +1,273 @@ +[Desktop Entry] +Version=1.0 +Name=Firefox +GenericName=Web Browser +GenericName[ca]=Navegador web +GenericName[cs]=Webový prohlížeč +GenericName[es]=Navegador web +GenericName[fa]=مرورگر اینترنتی +GenericName[fi]=WWW-selain +GenericName[fr]=Navigateur Web +GenericName[hu]=Webböngésző +GenericName[it]=Browser Web +GenericName[ja]=ウェブ・ブラウザ +GenericName[ko]=웹 브라우저 +GenericName[nb]=Nettleser +GenericName[nl]=Webbrowser +GenericName[nn]=Nettlesar +GenericName[no]=Nettleser +GenericName[pl]=Przeglądarka WWW +GenericName[pt]=Navegador Web +GenericName[pt_BR]=Navegador Web +GenericName[sk]=Internetový prehliadač +GenericName[sv]=Webbläsare +Comment=Browse the Web +Comment[ca]=Navegueu per el web +Comment[cs]=Prohlížení stránek World Wide Webu +Comment[de]=Im Internet surfen +Comment[es]=Navegue por la web +Comment[fa]=صفحات شبکه جهانی اینترنت را مرور نمایید +Comment[fi]=Selaa Internetin WWW-sivuja +Comment[fr]=Navigue sur Internet +Comment[hu]=A világháló böngészése +Comment[it]=Esplora il web +Comment[ja]=ウェブを閲覧します +Comment[ko]=웹을 돌아 다닙니다 +Comment[nb]=Surf på nettet +Comment[nl]=Verken het internet +Comment[nn]=Surf på nettet +Comment[no]=Surf på nettet +Comment[pl]=Przeglądanie stron WWW +Comment[pt]=Navegue na Internet +Comment[pt_BR]=Navegue na Internet +Comment[sk]=Prehliadanie internetu +Comment[sv]=Surfa på webben +Exec=firefox %u +Icon=firefox +Terminal=false +Type=Application +MimeType=text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https; +StartupNotify=true +Categories=Network;WebBrowser; +Keywords=web;browser;internet; +Actions=new-window;new-private-window; + +X-Desktop-File-Install-Version=0.24 + +[Desktop Action new-window] +Name=Open a New Window +Name[ach]=Dirica manyen +Name[af]=Nuwe venster +Name[an]=Nueva finestra +Name[ar]=نافذة جديدة +Name[as]=নতুন উইন্ডো +Name[ast]=Ventana nueva +Name[az]=Yeni Pəncərə +Name[be]=Новае акно +Name[bg]=Нов прозорец +Name[bn_BD]=নতুন উইন্ডো (N) +Name[bn_IN]=নতুন উইন্ডো +Name[br]=Prenestr nevez +Name[brx]=गोदान उइन्ड'(N) +Name[bs]=Novi prozor +Name[ca]=Finestra nova +Name[cak]=K'ak'a' tzuwäch +Name[cs]=Nové okno +Name[cy]=Ffenestr Newydd +Name[da]=Nyt vindue +Name[de]=Neues Fenster +Name[dsb]=Nowe wokno +Name[el]=Νέο παράθυρο +Name[en_GB]=New Window +Name[en_US]=New Window +Name[en_ZA]=New Window +Name[eo]=Nova fenestro +Name[es_AR]=Nueva ventana +Name[es_CL]=Nueva ventana +Name[es_ES]=Nueva ventana +Name[es_MX]=Nueva ventana +Name[et]=Uus aken +Name[eu]=Leiho berria +Name[fa]=پنجره جدید +Name[ff]=Henorde Hesere +Name[fi]=Uusi ikkuna +Name[fr]=Nouvelle fenêtre +Name[fy_NL]=Nij finster +Name[ga_IE]=Fuinneog Nua +Name[gd]=Uinneag ùr +Name[gl]=Nova xanela +Name[gn]=Ovetã pyahu +Name[gu_IN]=નવી વિન્ડો +Name[he]=חלון חדש +Name[hi_IN]=नया विंडो +Name[hr]=Novi prozor +Name[hsb]=Nowe wokno +Name[hu]=Új ablak +Name[hy_AM]=Նոր Պատուհան +Name[id]=Jendela Baru +Name[is]=Nýr gluggi +Name[it]=Nuova finestra +Name[ja]=新しいウィンドウ +Name[ja_JP-mac]=新規ウインドウ +Name[ka]=ახალი ფანჯარა +Name[kk]=Жаңа терезе +Name[km]=បង្អួចថ្មី +Name[kn]=ಹೊಸ ಕಿಟಕಿ +Name[ko]=새 창 +Name[kok]=नवें जनेल +Name[ks]=نئئ وِنڈو +Name[lij]=Neuvo barcon +Name[lo]=ຫນ້າຕ່າງໃຫມ່ +Name[lt]=Naujas langas +Name[ltg]=Jauns lūgs +Name[lv]=Jauns logs +Name[mai]=नव विंडो +Name[mk]=Нов прозорец +Name[ml]=പുതിയ ജാലകം +Name[mr]=नवीन पटल +Name[ms]=Tetingkap Baru +Name[my]=ဝင်းဒိုးအသစ် +Name[nb_NO]=Nytt vindu +Name[ne_NP]=नयाँ सञ्झ्याल +Name[nl]=Nieuw venster +Name[nn_NO]=Nytt vindauge +Name[or]=ନୂତନ ୱିଣ୍ଡୋ +Name[pa_IN]=ਨਵੀਂ ਵਿੰਡੋ +Name[pl]=Nowe okno +Name[pt_BR]=Nova janela +Name[pt_PT]=Nova janela +Name[rm]=Nova fanestra +Name[ro]=Fereastră nouă +Name[ru]=Новое окно +Name[sat]=नावा विंडो (N) +Name[si]=නව කවුළුවක් +Name[sk]=Nové okno +Name[sl]=Novo okno +Name[son]=Zanfun taaga +Name[sq]=Dritare e Re +Name[sr]=Нови прозор +Name[sv_SE]=Nytt fönster +Name[ta]=புதிய சாளரம் +Name[te]=కొత్త విండో +Name[th]=หน้าต่างใหม่ +Name[tr]=Yeni pencere +Name[tsz]=Eraatarakua jimpani +Name[uk]=Нове вікно +Name[ur]=نیا دریچہ +Name[uz]=Yangi oyna +Name[vi]=Cửa sổ mới +Name[wo]=Palanteer bu bees +Name[xh]=Ifestile entsha +Name[zh_CN]=新建窗口 +Name[zh_TW]=開新視窗 + + +Exec=firefox --new-window %u + +[Desktop Action new-private-window] +Name=Open a New Private Window +Name[ach]=Dirica manyen me mung +Name[af]=Nuwe privaatvenster +Name[an]=Nueva finestra privada +Name[ar]=نافذة خاصة جديدة +Name[as]=নতুন ব্যক্তিগত উইন্ডো +Name[ast]=Ventana privada nueva +Name[az]=Yeni Məxfi Pəncərə +Name[be]=Новае акно адасаблення +Name[bg]=Нов прозорец за поверително сърфиране +Name[bn_BD]=নতুন ব্যক্তিগত উইন্ডো +Name[bn_IN]=নতুন ব্যক্তিগত উইন্ডো +Name[br]=Prenestr merdeiñ prevez nevez +Name[brx]=गोदान प्राइभेट उइन्ड' +Name[bs]=Novi privatni prozor +Name[ca]=Finestra privada nova +Name[cak]=K'ak'a' ichinan tzuwäch +Name[cs]=Nové anonymní okno +Name[cy]=Ffenestr Breifat Newydd +Name[da]=Nyt privat vindue +Name[de]=Neues privates Fenster +Name[dsb]=Nowe priwatne wokno +Name[el]=Νέο παράθυρο ιδιωτικής περιήγησης +Name[en_GB]=New Private Window +Name[en_US]=New Private Window +Name[en_ZA]=New Private Window +Name[eo]=Nova privata fenestro +Name[es_AR]=Nueva ventana privada +Name[es_CL]=Nueva ventana privada +Name[es_ES]=Nueva ventana privada +Name[es_MX]=Nueva ventana privada +Name[et]=Uus privaatne aken +Name[eu]=Leiho pribatu berria +Name[fa]=پنجره ناشناس جدید +Name[ff]=Henorde Suturo Hesere +Name[fi]=Uusi yksityinen ikkuna +Name[fr]=Nouvelle fenêtre de navigation privée +Name[fy_NL]=Nij priveefinster +Name[ga_IE]=Fuinneog Nua Phríobháideach +Name[gd]=Uinneag phrìobhaideach ùr +Name[gl]=Nova xanela privada +Name[gn]=Ovetã ñemi pyahu +Name[gu_IN]=નવી ખાનગી વિન્ડો +Name[he]=חלון פרטי חדש +Name[hi_IN]=नयी निजी विंडो +Name[hr]=Novi privatni prozor +Name[hsb]=Nowe priwatne wokno +Name[hu]=Új privát ablak +Name[hy_AM]=Սկսել Գաղտնի դիտարկում +Name[id]=Jendela Mode Pribadi Baru +Name[is]=Nýr huliðsgluggi +Name[it]=Nuova finestra anonima +Name[ja]=新しいプライベートウィンドウ +Name[ja_JP-mac]=新規プライベートウインドウ +Name[ka]=ახალი პირადი ფანჯარა +Name[kk]=Жаңа жекелік терезе +Name[km]=បង្អួចឯកជនថ្មី +Name[kn]=ಹೊಸ ಖಾಸಗಿ ಕಿಟಕಿ +Name[ko]=새 사생활 보호 모드 +Name[kok]=नवो खाजगी विंडो +Name[ks]=نْو پرایوٹ وینڈو& +Name[lij]=Neuvo barcon privou +Name[lo]=ເປີດຫນ້າຕ່າງສວນຕົວຂື້ນມາໃຫມ່ +Name[lt]=Naujas privataus naršymo langas +Name[ltg]=Jauns privatais lūgs +Name[lv]=Jauns privātais logs +Name[mai]=नया निज विंडो (W) +Name[mk]=Нов приватен прозорец +Name[ml]=പുതിയ സ്വകാര്യ ജാലകം +Name[mr]=नवीन वैयक्तिक पटल +Name[ms]=Tetingkap Persendirian Baharu +Name[my]=New Private Window +Name[nb_NO]=Nytt privat vindu +Name[ne_NP]=नयाँ निजी सञ्झ्याल +Name[nl]=Nieuw privévenster +Name[nn_NO]=Nytt privat vindauge +Name[or]=ନୂତନ ବ୍ୟକ୍ତିଗତ ୱିଣ୍ଡୋ +Name[pa_IN]=ਨਵੀਂ ਪ੍ਰਾਈਵੇਟ ਵਿੰਡੋ +Name[pl]=Nowe okno prywatne +Name[pt_BR]=Nova janela privativa +Name[pt_PT]=Nova janela privada +Name[rm]=Nova fanestra privata +Name[ro]=Fereastră privată nouă +Name[ru]=Новое приватное окно +Name[sat]=नावा निजेराक् विंडो (W ) +Name[si]=නව පුද්ගලික කවුළුව (W) +Name[sk]=Nové okno v režime Súkromné prehliadanie +Name[sl]=Novo zasebno okno +Name[son]=Sutura zanfun taaga +Name[sq]=Dritare e Re Private +Name[sr]=Нови приватан прозор +Name[sv_SE]=Nytt privat fönster +Name[ta]=புதிய தனிப்பட்ட சாளரம் +Name[te]=కొత్త ఆంతరంగిక విండో +Name[th]=หน้าต่างส่วนตัวใหม่ +Name[tr]=Yeni gizli pencere +Name[tsz]=Juchiiti eraatarakua jimpani +Name[uk]=Приватне вікно +Name[ur]=نیا نجی دریچہ +Name[uz]=Yangi maxfiy oyna +Name[vi]=Cửa sổ riêng tư mới +Name[wo]=Panlanteeru biir bu bees +Name[xh]=Ifestile yangasese entsha +Name[zh_CN]=新建隐私浏览窗口 +Name[zh_TW]=新增隱私視窗 +Exec=firefox --private-window %u diff --git a/browser/components/shell/test/.eslintrc.js b/browser/components/shell/test/.eslintrc.js new file mode 100644 index 0000000000..1779fd7f1c --- /dev/null +++ b/browser/components/shell/test/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; diff --git a/browser/components/shell/test/browser.ini b/browser/components/shell/test/browser.ini new file mode 100644 index 0000000000..e567f0cf11 --- /dev/null +++ b/browser/components/shell/test/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] + +[browser_420786.js] +skip-if = os != "linux" +[browser_633221.js] +skip-if = os != "linux" +[browser_1119088.js] +support-files = + mac_desktop_image.py +skip-if = os != "mac" || verify +[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..492af305d3 --- /dev/null +++ b/browser/components/shell/test/browser_1119088.js @@ -0,0 +1,172 @@ +// 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"; + +XPCOMUtils.defineLazyModuleGetters(this, { + FileUtils: "resource://gre/modules/FileUtils.jsm", +}); + +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 = Cc["@mozilla.org/uuid-generator;1"].getService( + Ci.nsIUUIDGenerator + ); + 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..3443011189 --- /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..b61aeba6fa --- /dev/null +++ b/browser/components/shell/test/browser_633221.js @@ -0,0 +1,15 @@ +const { ShellService } = ChromeUtils.import( + "resource:///modules/ShellService.jsm" +); + +function test() { + ShellService.setDefaultBrowser(true, false); + ok( + ShellService.isDefaultBrowser(true, false), + "we got here and are the default browser" + ); + ok( + ShellService.isDefaultBrowser(true, true), + "we got here and are the default browser" + ); +} diff --git a/browser/components/shell/test/browser_setDesktopBackgroundPreview.js b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js new file mode 100644 index 0000000000..a16fb30459 --- /dev/null +++ b/browser/components/shell/test/browser_setDesktopBackgroundPreview.js @@ -0,0 +1,66 @@ +/* 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"); + document.getElementById("context-setDesktopBackground").click(); + + // Need to explicitly close the menu (and wait for it), otherwise it fails + // verify/later tests + const menuClosed = BrowserTestUtils.waitForPopupEvent(menu, "hidden"); + menu.hidePopup(); + + 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/chrome.ini b/browser/components/shell/test/chrome.ini new file mode 100644 index 0000000000..8b4c9153f9 --- /dev/null +++ b/browser/components/shell/test/chrome.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + headless.html + headless_redirect.html + headless_redirect.html^headers^ + +[test_headless_screenshot.html] +skip-if = (toolkit == 'android') || (os == 'win') #Bug 1429950 , Bug 1583315 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_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..ca7bc0b297 --- /dev/null +++ b/browser/components/shell/test/mac_desktop_image.py @@ -0,0 +1,171 @@ +#!/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. +""" + +from __future__ import absolute_import +from __future__ import print_function + +# +# 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 + +import argparse +import logging +import os +import sys + + +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/test_headless_screenshot.html b/browser/components/shell/test/test_headless_screenshot.html new file mode 100644 index 0000000000..4c5f5e4dcb --- /dev/null +++ b/browser/components/shell/test/test_headless_screenshot.html @@ -0,0 +1,155 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1378010 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1378010</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + const {Subprocess} = ChromeUtils.import("resource://gre/modules/Subprocess.jsm"); + const {OS} = ChromeUtils.import("resource://gre/modules/osfile.jsm"); + + SimpleTest.requestLongerTimeout(2); + + const screenshotPath = OS.Path.join(OS.Constants.Path.tmpDir, "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 = OS.Path.join(OS.Constants.Path.tmpDir, "headless_test_screenshot_profile"); + const prefsPath = OS.Path.join(profilePath, mochiPrefsName); + const firefoxArgs = ["-profile", profilePath, "-no-remote"]; + + await OS.File.makeDir(profilePath); + await OS.File.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", + }, + }); + 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 OS.File.removeDir(profilePath); + } + + async function testFileCreationPositive(args, path) { + await runFirefox(args); + + let saved = await OS.File.exists(path); + ok(saved, "A screenshot should be saved as " + path); + if (!saved) { + return; + } + + let info = await OS.File.stat(path); + ok(info.size > 0, "Screenshot should not be an empty file"); + await OS.File.remove(path); + } + + async function testFileCreationNegative(args, path) { + await runFirefox(args); + + let saved = await OS.File.exists(path); + ok(!saved, "A screenshot should not be saved"); + await OS.File.remove(path, { ignoreAbsent: true }); + } + + async function testWindowSizePositive(width, height) { + let size = width + ""; + if (height) { + size += "," + height; + } + + await runFirefox(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", screenshotPath, "-window-size", size]); + + let saved = await OS.File.exists(screenshotPath); + ok(saved, "A screenshot should be saved in the tmp directory"); + if (!saved) { + return; + } + + let data = await OS.File.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.onloadend = 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 OS.File.remove(screenshotPath); + } + + (async function() { + SimpleTest.waitForExplicitFinish(); + + // Test all four basic variations of the "screenshot" argument + // when a file path is specified. + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", screenshotPath], screenshotPath); + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", `-screenshot=${screenshotPath}`], screenshotPath); + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "--screenshot", screenshotPath], screenshotPath); + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", `--screenshot=${screenshotPath}`], screenshotPath); + + // Test variations of the "screenshot" argument when a file path + // isn't specified. + await testFileCreationPositive(["-screenshot", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html"], "screenshot.png"); + await testFileCreationPositive(["http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot"], "screenshot.png"); + await testFileCreationPositive(["--screenshot", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html"], "screenshot.png"); + await testFileCreationPositive(["http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "--screenshot"], "screenshot.png"); + + // Test invalid URL arguments (either no argument or too many arguments). + await testFileCreationNegative(["-screenshot"], "screenshot.png"); + await testFileCreationNegative(["http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "http://mochi.test:8888/headless.html", "-screenshot"], "screenshot.png"); + + // Test all four basic variations of the "window-size" argument. + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size", "800"], "screenshot.png"); + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size=800"], "screenshot.png"); + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "--window-size", "800"], "screenshot.png"); + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "--window-size=800"], "screenshot.png"); + + // Test other variations of the "window-size" argument. + await testWindowSizePositive(800, 600); + await testWindowSizePositive(1234); + await testFileCreationNegative(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size", "hello"], "screenshot.png"); + await testFileCreationNegative(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless.html", "-screenshot", "-window-size", "800,"], "screenshot.png"); + + // Test when the requested URL redirects + await testFileCreationPositive(["-url", "http://mochi.test:8888/chrome/browser/components/shell/test/headless_redirect.html", "-screenshot", screenshotPath], screenshotPath); + + SimpleTest.finish(); + })(); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1378010">Mozilla Bug 1378010</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> 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..69e09db70a --- /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_task(async function setup() { + info("Ensure System Preferences isn't already running"); + killSystemPreferences(); +}); + +add_task(async function test_prefsOpen() { + let shellSvc = Cc["@mozilla.org/browser/shell-service;1"].getService( + Ci.nsIMacShellService + ); + shellSvc.showSecurityPreferences("Privacy_AllFiles"); + + equal(killSystemPreferences(), 0, "Ensure System Preferences was started"); +}); diff --git a/browser/components/shell/test/unit/xpcshell.ini b/browser/components/shell/test/unit/xpcshell.ini new file mode 100644 index 0000000000..27dfb2a452 --- /dev/null +++ b/browser/components/shell/test/unit/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +firefox-appdir = browser + +[test_macOS_showSecurityPreferences.js] +skip-if = toolkit != "cocoa" |