diff options
Diffstat (limited to 'browser/modules')
-rw-r--r-- | browser/modules/BrowserUsageTelemetry.sys.mjs | 8 | ||||
-rw-r--r-- | browser/modules/BrowserWindowTracker.sys.mjs | 4 | ||||
-rw-r--r-- | browser/modules/FirefoxBridgeExtensionUtils.sys.mjs | 360 | ||||
-rw-r--r-- | browser/modules/PageActions.sys.mjs | 2 | ||||
-rw-r--r-- | browser/modules/PermissionUI.sys.mjs | 4 | ||||
-rw-r--r-- | browser/modules/Sanitizer.sys.mjs | 40 | ||||
-rw-r--r-- | browser/modules/moz.build | 7 | ||||
-rw-r--r-- | browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js | 350 | ||||
-rw-r--r-- | browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js | 222 | ||||
-rw-r--r-- | browser/modules/test/unit/xpcshell.toml | 9 |
10 files changed, 980 insertions, 26 deletions
diff --git a/browser/modules/BrowserUsageTelemetry.sys.mjs b/browser/modules/BrowserUsageTelemetry.sys.mjs index 607ed4e31d..410d1e2ea3 100644 --- a/browser/modules/BrowserUsageTelemetry.sys.mjs +++ b/browser/modules/BrowserUsageTelemetry.sys.mjs @@ -759,7 +759,10 @@ export let BrowserUsageTelemetry = { if (URL == AppConstants.BROWSER_CHROME_URL) { return this._getBrowserWidgetContainer(node); } - if (URL.startsWith("about:preferences")) { + if ( + URL.startsWith("about:preferences") || + URL.startsWith("about:settings") + ) { // Find the element's category. let container = node.closest("[data-category]"); if (!container) { @@ -833,7 +836,8 @@ export let BrowserUsageTelemetry = { // Find the actual element we're interested in. let node = sourceEvent.target; const isAboutPreferences = - node.ownerDocument.URL.startsWith("about:preferences"); + node.ownerDocument.URL.startsWith("about:preferences") || + node.ownerDocument.URL.startsWith("about:settings"); while ( !UI_TARGET_ELEMENTS.includes(node.localName) && !node.classList?.contains("wants-telemetry") && diff --git a/browser/modules/BrowserWindowTracker.sys.mjs b/browser/modules/BrowserWindowTracker.sys.mjs index a15c6a0fb7..cead9df7ba 100644 --- a/browser/modules/BrowserWindowTracker.sys.mjs +++ b/browser/modules/BrowserWindowTracker.sys.mjs @@ -287,9 +287,6 @@ export const BrowserWindowTracker = { remote = undefined, fission = undefined, } = {}) { - let telemetryObj = {}; - TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj); - let windowFeatures = "chrome,dialog=no,all"; if (features) { windowFeatures += `,${features}`; @@ -344,7 +341,6 @@ export const BrowserWindowTracker = { win.addEventListener( "MozAfterPaint", () => { - TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj); if ( Services.prefs.getIntPref("browser.startup.page") == 1 && loadURIString == lazy.HomePage.get() diff --git a/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs b/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs new file mode 100644 index 0000000000..7b0094205d --- /dev/null +++ b/browser/modules/FirefoxBridgeExtensionUtils.sys.mjs @@ -0,0 +1,360 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", +}); + +/** + * Default implementation of the helper class to assist in deleting the firefox protocols. + * See maybeDeleteBridgeProtocolRegistryEntries for more info. + */ +class DeleteBridgeProtocolRegistryEntryHelperImplementation { + getApplicationPath() { + return Services.dirsvc.get("XREExeF", Ci.nsIFile).path; + } + + openRegistryRoot() { + const wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + + wrk.open(wrk.ROOT_KEY_CURRENT_USER, "Software\\Classes", wrk.ACCESS_ALL); + + return wrk; + } + + deleteChildren(start) { + // Recursively delete all of the children of the children + // Go through the list in reverse order, so that shrinking + // the list doesn't rearrange things while iterating + for (let i = start.childCount; i > 0; i--) { + const childName = start.getChildName(i - 1); + const child = start.openChild(childName, start.ACCESS_ALL); + this.deleteChildren(child); + child.close(); + + start.removeChild(childName); + } + } + + deleteRegistryTree(root, toDeletePath) { + var start = root.openChild(toDeletePath, root.ACCESS_ALL); + this.deleteChildren(start); + start.close(); + + root.removeChild(toDeletePath); + } +} + +export const FirefoxBridgeExtensionUtils = { + /** + * In Firefox 122, we enabled the firefox and firefox-private protocols. + * We switched over to using firefox-bridge and firefox-private-bridge, + * + * but we want to clean up the use of the other protocols. + * + * deleteBridgeProtocolRegistryEntryHelper handles everything outside of the logic needed for + * this method so that the logic in maybeDeleteBridgeProtocolRegistryEntries can be unit tested + * + * We only delete the entries for the firefox and firefox-private protocols if + * they were set up to use this install and in the format that Firefox installed + * them with. If the entries are changed in any way, it is assumed that the user + * mucked with them manually and knows what they are doing. + */ + maybeDeleteBridgeProtocolRegistryEntries( + deleteBridgeProtocolRegistryEntryHelper = new DeleteBridgeProtocolRegistryEntryHelperImplementation() + ) { + try { + var wrk = deleteBridgeProtocolRegistryEntryHelper.openRegistryRoot(); + const path = deleteBridgeProtocolRegistryEntryHelper.getApplicationPath(); + + const maybeDeleteRegistryKey = (protocol, protocolCommand) => { + const openCommandPath = protocol + "\\shell\\open\\command"; + if (wrk.hasChild(openCommandPath)) { + let deleteProtocolEntry = false; + + try { + var openCommandKey = wrk.openChild( + openCommandPath, + wrk.ACCESS_READ + ); + if (openCommandKey.valueCount == 1) { + const defaultKeyName = ""; + if (openCommandKey.getValueName(0) == defaultKeyName) { + if ( + openCommandKey.getValueType(defaultKeyName) == + Ci.nsIWindowsRegKey.TYPE_STRING + ) { + const val = openCommandKey.readStringValue(defaultKeyName); + if (val == protocolCommand) { + deleteProtocolEntry = true; + } + } + } + } + } finally { + openCommandKey.close(); + } + + if (deleteProtocolEntry) { + deleteBridgeProtocolRegistryEntryHelper.deleteRegistryTree( + wrk, + protocol + ); + } + } + }; + + maybeDeleteRegistryKey("firefox", `\"${path}\" -osint -url \"%1\"`); + maybeDeleteRegistryKey( + "firefox-private", + `\"${path}\" -osint -private-window \"%1\"` + ); + } catch (err) { + console.error(err); + } finally { + wrk.close(); + } + }, + + /** + * Registers the firefox-bridge and firefox-private-bridge protocols + * on the Windows platform. + */ + maybeRegisterFirefoxBridgeProtocols() { + const FIREFOX_BRIDGE_HANDLER_NAME = "firefox-bridge"; + const FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME = "firefox-private-bridge"; + const path = Services.dirsvc.get("XREExeF", Ci.nsIFile).path; + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + wrk.open(wrk.ROOT_KEY_CLASSES_ROOT, "", wrk.ACCESS_READ); + let FxSet = wrk.hasChild(FIREFOX_BRIDGE_HANDLER_NAME); + let FxPrivateSet = wrk.hasChild(FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME); + wrk.close(); + if (FxSet && FxPrivateSet) { + return; + } + wrk.open(wrk.ROOT_KEY_CURRENT_USER, "Software\\Classes", wrk.ACCESS_ALL); + const maybeUpdateRegistry = (isSetAlready, handler, protocolName) => { + if (isSetAlready) { + return; + } + let FxKey = wrk.createChild(handler, wrk.ACCESS_ALL); + try { + // Write URL protocol key + FxKey.writeStringValue("", protocolName); + FxKey.writeStringValue("URL Protocol", ""); + FxKey.close(); + // Write defaultIcon key + FxKey.create( + FxKey.ROOT_KEY_CURRENT_USER, + "Software\\Classes\\" + handler + "\\DefaultIcon", + FxKey.ACCESS_ALL + ); + FxKey.open( + FxKey.ROOT_KEY_CURRENT_USER, + "Software\\Classes\\" + handler + "\\DefaultIcon", + FxKey.ACCESS_ALL + ); + FxKey.writeStringValue("", `\"${path}\",1`); + FxKey.close(); + // Write shell\\open\\command key + FxKey.create( + FxKey.ROOT_KEY_CURRENT_USER, + "Software\\Classes\\" + handler + "\\shell", + FxKey.ACCESS_ALL + ); + FxKey.create( + FxKey.ROOT_KEY_CURRENT_USER, + "Software\\Classes\\" + handler + "\\shell\\open", + FxKey.ACCESS_ALL + ); + FxKey.create( + FxKey.ROOT_KEY_CURRENT_USER, + "Software\\Classes\\" + handler + "\\shell\\open\\command", + FxKey.ACCESS_ALL + ); + FxKey.open( + FxKey.ROOT_KEY_CURRENT_USER, + "Software\\Classes\\" + handler + "\\shell\\open\\command", + FxKey.ACCESS_ALL + ); + if (handler == FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME) { + FxKey.writeStringValue( + "", + `\"${path}\" -osint -private-window \"%1\"` + ); + } else { + FxKey.writeStringValue("", `\"${path}\" -osint -url \"%1\"`); + } + } catch (ex) { + console.error(ex); + } finally { + FxKey.close(); + } + }; + + try { + maybeUpdateRegistry( + FxSet, + FIREFOX_BRIDGE_HANDLER_NAME, + "URL:Firefox Bridge Protocol" + ); + } catch (ex) { + console.error(ex); + } + + try { + maybeUpdateRegistry( + FxPrivateSet, + FIREFOX_PRIVATE_BRIDGE_HANDLER_NAME, + "URL:Firefox Private Bridge Protocol" + ); + } catch (ex) { + console.error(ex); + } + } catch (ex) { + console.error(ex); + } finally { + wrk.close(); + } + }, + + getNativeMessagingHostId() { + let nativeMessagingHostId = "org.mozilla.firefox_bridge_nmh"; + if (AppConstants.NIGHTLY_BUILD) { + nativeMessagingHostId += "_nightly"; + } else if (AppConstants.MOZ_DEV_EDITION) { + nativeMessagingHostId += "_dev"; + } else if (AppConstants.IS_ESR) { + nativeMessagingHostId += "_esr"; + } + return nativeMessagingHostId; + }, + + getExtensionOrigins() { + return Services.prefs + .getStringPref("browser.firefoxbridge.extensionOrigins", "") + .split(","); + }, + + async maybeWriteManifestFiles( + nmhManifestFolder, + nativeMessagingHostId, + dualBrowserExtensionOrigins + ) { + try { + let binFile = Services.dirsvc.get("XREExeF", Ci.nsIFile).parent; + if (AppConstants.platform == "win") { + binFile.append("nmhproxy.exe"); + } else if (AppConstants.platform == "macosx") { + binFile.append("nmhproxy"); + } else { + throw new Error("Unsupported platform"); + } + + let jsonContent = { + name: nativeMessagingHostId, + description: "Firefox Native Messaging Host", + path: binFile.path, + type: "stdio", + allowed_origins: dualBrowserExtensionOrigins, + }; + let nmhManifestFile = await IOUtils.getFile( + nmhManifestFolder, + `${nativeMessagingHostId}.json` + ); + + // This throws an error if the JSON file doesn't exist + // or if it's corrupt. + let correctFileExists = true; + try { + correctFileExists = lazy.ObjectUtils.deepEqual( + await IOUtils.readJSON(nmhManifestFile.path), + jsonContent + ); + } catch (e) { + correctFileExists = false; + } + if (!correctFileExists) { + await IOUtils.writeJSON(nmhManifestFile.path, jsonContent); + } + } catch (e) { + console.error(e); + } + }, + + async ensureRegistered() { + let nmhManifestFolder = null; + if (AppConstants.platform == "win") { + // We don't have permission to write to the application install directory + // so instead write to %AppData%\Mozilla\Firefox. + nmhManifestFolder = PathUtils.join( + Services.dirsvc.get("AppData", Ci.nsIFile).path, + "Mozilla", + "Firefox" + ); + } else if (AppConstants.platform == "macosx") { + nmhManifestFolder = + "~/Library/Application Support/Google/Chrome/NativeMessagingHosts/"; + } else { + throw new Error("Unsupported platform"); + } + await this.maybeWriteManifestFiles( + nmhManifestFolder, + this.getNativeMessagingHostId(), + this.getExtensionOrigins() + ); + if (AppConstants.platform == "win") { + this.maybeWriteNativeMessagingRegKeys( + "Software\\Google\\Chrome\\NativeMessagingHosts", + nmhManifestFolder, + this.getNativeMessagingHostId() + ); + } + }, + + maybeWriteNativeMessagingRegKeys( + regPath, + nmhManifestFolder, + NATIVE_MESSAGING_HOST_ID + ) { + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + let expectedValue = PathUtils.join( + nmhManifestFolder, + `${NATIVE_MESSAGING_HOST_ID}.json` + ); + try { + // If the key already exists it will just be opened + wrk.create( + wrk.ROOT_KEY_CURRENT_USER, + regPath + `\\${NATIVE_MESSAGING_HOST_ID}`, + wrk.ACCESS_ALL + ); + if (wrk.readStringValue("") == expectedValue) { + return; + } + } catch (e) { + // The key either doesn't have a value or doesn't exist + // In either case we need to write it. + } + wrk.writeStringValue("", expectedValue); + } catch (e) { + // The method fails if we can't access the key + // which means it doesn't exist. That's a normal situation. + // We don't need to do anything here. + } finally { + wrk.close(); + } + }, +}; diff --git a/browser/modules/PageActions.sys.mjs b/browser/modules/PageActions.sys.mjs index 4e089f2fc7..f5951142dd 100644 --- a/browser/modules/PageActions.sys.mjs +++ b/browser/modules/PageActions.sys.mjs @@ -896,7 +896,7 @@ Action.prototype = { * The chosen icon URL. */ _iconURLForSize(urls, preferredSize) { - // This case is copied from ExtensionParent.jsm so that our image logic is + // This case is copied from ExtensionParent.sys.mjs so that our image logic is // the same, so that WebExtensions page action tests that deal with icons // pass. let bestSize = null; diff --git a/browser/modules/PermissionUI.sys.mjs b/browser/modules/PermissionUI.sys.mjs index 2f44073f43..e94beb79ac 100644 --- a/browser/modules/PermissionUI.sys.mjs +++ b/browser/modules/PermissionUI.sys.mjs @@ -19,8 +19,8 @@ * const { Integration } = ChromeUtils.importESModule( * "resource://gre/modules/Integration.sys.mjs" * ); - * const { PermissionUI } = ChromeUtils.import( - * "resource:///modules/PermissionUI.jsm" + * const { PermissionUI } = ChromeUtils.importESModule( + * "resource:///modules/PermissionUI.sys.mjs" * ); * * const SoundCardIntegration = base => { diff --git a/browser/modules/Sanitizer.sys.mjs b/browser/modules/Sanitizer.sys.mjs index 895cf6df83..13b7a307ea 100644 --- a/browser/modules/Sanitizer.sys.mjs +++ b/browser/modules/Sanitizer.sys.mjs @@ -27,7 +27,7 @@ var logConsole; function log(msg) { if (!logConsole) { logConsole = console.createInstance({ - prefix: "** Sanitizer.jsm", + prefix: "Sanitizer", maxLogLevelPref: "browser.sanitizer.loglevel", }); } @@ -397,32 +397,38 @@ export var Sanitizer = { }, /** - * Migrate old sanitize on shutdown prefs to the new prefs for the new - * clear on shutdown dialog. Does nothing if the migration was completed before - * based on the pref privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs + * Migrate old sanitize prefs to the new prefs for the new + * clear history dialog. Does nothing if the migration was completed before + * based on the pref privacy.sanitize.cpd.hasMigratedToNewPrefs or + * privacy.sanitize.clearOnShutdown.hasMigratedToNewPrefs * + * @param {string} context - one of "clearOnShutdown" or "cpd", which indicates which + * pref branch to migrate prefs from based on the dialog context */ - maybeMigrateSanitizeOnShutdownPrefs() { + maybeMigratePrefs(context) { if ( Services.prefs.getBoolPref( - "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs" + `privacy.sanitize.${context}.hasMigratedToNewPrefs` ) ) { return; } - let cookies = Services.prefs.getBoolPref("privacy.clearOnShutdown.cookies"); - let history = Services.prefs.getBoolPref("privacy.clearOnShutdown.history"); - let cache = Services.prefs.getBoolPref("privacy.clearOnShutdown.cache"); + let cookies = Services.prefs.getBoolPref(`privacy.${context}.cookies`); + let history = Services.prefs.getBoolPref(`privacy.${context}.history`); + let cache = Services.prefs.getBoolPref(`privacy.${context}.cache`); let siteSettings = Services.prefs.getBoolPref( - "privacy.clearOnShutdown.siteSettings" + `privacy.${context}.siteSettings` ); + let newContext = + context == "clearOnShutdown" ? "clearOnShutdown_v2" : "clearHistory"; + // We set cookiesAndStorage to true if cookies are enabled for clearing on shutdown // regardless of what sessions and offlineApps are set to // This is because cookie clearing behaviour takes precedence over sessions and offlineApps clearing. Services.prefs.setBoolPref( - "privacy.clearOnShutdown_v2.cookiesAndStorage", + `privacy.${newContext}.cookiesAndStorage`, cookies ); @@ -430,27 +436,27 @@ export var Sanitizer = { // shutdown, regardless of what form data is set to. // This is because history clearing behavious takes precedence over formdata clearing. Services.prefs.setBoolPref( - "privacy.clearOnShutdown_v2.historyFormDataAndDownloads", + `privacy.${newContext}.historyFormDataAndDownloads`, history ); // cache and siteSettings follow the old dialog prefs - Services.prefs.setBoolPref("privacy.clearOnShutdown_v2.cache", cache); + Services.prefs.setBoolPref(`privacy.${newContext}.cache`, cache); Services.prefs.setBoolPref( - "privacy.clearOnShutdown_v2.siteSettings", + `privacy.${newContext}.siteSettings`, siteSettings ); Services.prefs.setBoolPref( - "privacy.sanitize.sanitizeOnShutdown.hasMigratedToNewPrefs", + `privacy.sanitize.${context}.hasMigratedToNewPrefs`, true ); }, // When making any changes to the sanitize implementations here, // please check whether the changes are applicable to Android - // (mobile/android/modules/geckoview/GeckoViewStorageController.jsm) as well. + // (mobile/android/modules/geckoview/GeckoViewStorageController.sys.mjs) as well. items: { cache: { @@ -1089,7 +1095,7 @@ async function sanitizeOnShutdown(progress) { } else { // Perform a migration if this is the first time sanitizeOnShutdown is // running for the user with the new dialog - Sanitizer.maybeMigrateSanitizeOnShutdownPrefs(); + Sanitizer.maybeMigratePrefs("clearOnShutdown"); progress.sanitizationPrefs = { privacy_sanitize_sanitizeOnShutdown: Services.prefs.getBoolPref( diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 99b6bfd56f..7e5e418f68 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -7,6 +7,12 @@ with Files("**"): BUG_COMPONENT = ("Firefox", "General") +with Files("FirefoxBridgeExtensionUtils.sys.mjs"): + BUG_COMPONENT = ("Firefox", "Shell Integration") + +with Files("test/unit/test_FirefoxBridgeExtensionUtils.sys.mjs"): + BUG_COMPONENT = ("Firefox", "Shell Integration") + with Files("test/browser/*Telemetry*"): BUG_COMPONENT = ("Toolkit", "Telemetry") @@ -128,6 +134,7 @@ EXTRA_JS_MODULES += [ "EveryWindow.sys.mjs", "ExtensionsUI.sys.mjs", "FaviconLoader.sys.mjs", + "FirefoxBridgeExtensionUtils.sys.mjs", "HomePage.sys.mjs", "LaterRun.sys.mjs", "NewTabPagePreloading.sys.mjs", diff --git a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js new file mode 100644 index 0000000000..1273ee950b --- /dev/null +++ b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtils.js @@ -0,0 +1,350 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { FirefoxBridgeExtensionUtils } = ChromeUtils.importESModule( + "resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs" +); + +const FIREFOX_SHELL_OPEN_COMMAND_PATH = "firefox\\shell\\open\\command"; +const FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH = + "firefox-private\\shell\\open\\command"; + +class StubbedRegistryKey { + #children; + #originalChildren; + #closeCalled; + #deletedChildren; + #openedForRead; + #values; + + constructor(children, values) { + this.#children = children; + this.#values = values; + this.#originalChildren = new Map(children); + + this.#closeCalled = false; + this.#openedForRead = false; + this.#deletedChildren = new Set([]); + } + + get ACCESS_READ() { + return 0; + } + + reset() { + this.#closeCalled = false; + this.#deletedChildren = new Set([]); + this.#children = new Map(this.#originalChildren); + } + + open(accessLevel) { + this.#openedForRead = true; + } + + get wasOpenedForRead() { + return this.#openedForRead; + } + + openChild(path, accessLevel) { + const result = this.#children.get(path); + result?.open(accessLevel); + return result; + } + + hasChild(path) { + return this.#children.has(path); + } + + close() { + this.#closeCalled = true; + } + + removeChild(path) { + this.#deletedChildren.add(path); + + // delete the actual child if it's in there + this.#children.delete(path); + } + + isChildDeleted(path) { + return this.#deletedChildren.has(path); + } + + getChildName(index) { + let i = 0; + for (const [key] of this.#children) { + if (i == index) { + return key; + } + i++; + } + + return undefined; + } + + readStringValue(name) { + return this.#values.get(name); + } + + get childCount() { + return this.#children.size; + } + + getValueType(entryName) { + if (typeof this.readStringValue(entryName) == "string") { + return Ci.nsIWindowsRegKey.TYPE_STRING; + } + + throw new Error(`${entryName} not found in registry`); + } + + get wasCloseCalled() { + return this.#closeCalled; + } + + getValueName(index) { + let i = 0; + for (const [key] of this.#values) { + if (i == index) { + return key; + } + i++; + } + + return undefined; + } + + get valueCount() { + return this.#values.size; + } +} + +class StubbedDeleteBridgeProtocolRegistryEntryHelper { + #applicationPath; + #registryRootKey; + + constructor({ applicationPath, registryRootKey }) { + this.#applicationPath = applicationPath; + this.#registryRootKey = registryRootKey; + } + + getApplicationPath() { + return this.#applicationPath; + } + + openRegistryRoot() { + return this.#registryRootKey; + } + + deleteRegistryTree(root, toDeletePath) { + // simplify this for tests + root.removeChild(toDeletePath); + } +} + +add_task(async function test_DeleteWhenSameFirefoxInstall() { + const applicationPath = "testPath"; + + const firefoxEntries = new Map(); + firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`); + + const firefoxProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxEntries + ); + + const firefoxPrivateEntries = new Map(); + firefoxPrivateEntries.set( + "", + `\"${applicationPath}\" -osint -private-window \"%1\"` + ); + const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxPrivateEntries + ); + + const children = new Map(); + children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey); + children.set( + FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + firefoxPrivateProtocolRegKey + ); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); + ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); + ok( + registryRootKey.isChildDeleted("firefox"), + "Firefox protocol registry entry deleted" + ); + + ok( + firefoxPrivateProtocolRegKey.wasOpenedForRead, + "Firefox private key opened" + ); + ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed"); + ok( + registryRootKey.isChildDeleted("firefox-private"), + "Firefox private protocol registry entry deleted" + ); +}); + +add_task(async function test_DeleteWhenDifferentFirefoxInstall() { + const applicationPath = "testPath"; + const badApplicationPath = "testPath2"; + + const firefoxEntries = new Map(); + firefoxEntries.set("", `\"${badApplicationPath}\" -osint -url \"%1\"`); + + const firefoxProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxEntries + ); + + const firefoxPrivateEntries = new Map(); + firefoxPrivateEntries.set( + "", + `\"${badApplicationPath}\" -osint -private-window \"%1\"` + ); + const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxPrivateEntries + ); + + const children = new Map(); + children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey); + children.set( + FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + firefoxPrivateProtocolRegKey + ); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); + ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); + ok( + !registryRootKey.isChildDeleted("firefox"), + "Firefox protocol registry entry not deleted" + ); + + ok( + firefoxPrivateProtocolRegKey.wasOpenedForRead, + "Firefox private key opened" + ); + ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed"); + ok( + !registryRootKey.isChildDeleted("firefox-private"), + "Firefox private protocol registry entry not deleted" + ); +}); + +add_task(async function test_DeleteWhenNoRegistryEntries() { + const applicationPath = "testPath"; + + const firefoxPrivateEntries = new Map(); + const firefoxPrivateProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxPrivateEntries + ); + + const children = new Map(); + children.set( + FIREFOX_PRIVATE_SHELL_OPEN_COMMAND_PATH, + firefoxPrivateProtocolRegKey + ); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok( + firefoxPrivateProtocolRegKey.wasOpenedForRead, + "Firefox private key opened" + ); + ok(firefoxPrivateProtocolRegKey.wasCloseCalled, "Firefox private key closed"); + ok( + !registryRootKey.isChildDeleted("firefox"), + "Firefox protocol registry entry deleted when it shouldn't be" + ); + ok( + !registryRootKey.isChildDeleted("firefox-private"), + "Firefox private protocol registry deleted when it shouldn't be" + ); +}); + +add_task(async function test_DeleteWhenUnexpectedRegistryEntries() { + const applicationPath = "testPath"; + + const firefoxEntries = new Map(); + firefoxEntries.set("", `\"${applicationPath}\" -osint -url \"%1\"`); + firefoxEntries.set("extraEntry", "extraValue"); + const firefoxProtocolRegKey = new StubbedRegistryKey( + new Map(), + firefoxEntries + ); + + const children = new Map(); + children.set(FIREFOX_SHELL_OPEN_COMMAND_PATH, firefoxProtocolRegKey); + + const registryRootKey = new StubbedRegistryKey(children, new Map()); + + const stubbedDeleteBridgeProtocolRegistryHelper = + new StubbedDeleteBridgeProtocolRegistryEntryHelper({ + applicationPath, + registryRootKey, + }); + + FirefoxBridgeExtensionUtils.maybeDeleteBridgeProtocolRegistryEntries( + stubbedDeleteBridgeProtocolRegistryHelper + ); + + ok(registryRootKey.wasCloseCalled, "Root key closed"); + + ok(firefoxProtocolRegKey.wasOpenedForRead, "Firefox key opened"); + ok(firefoxProtocolRegKey.wasCloseCalled, "Firefox key closed"); + ok( + !registryRootKey.isChildDeleted("firefox"), + "Firefox protocol registry entry deleted when it shouldn't be" + ); + ok( + !registryRootKey.isChildDeleted("firefox-private"), + "Firefox private protocol registry deleted when it shouldn't be" + ); +}); diff --git a/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js new file mode 100644 index 0000000000..cef550d705 --- /dev/null +++ b/browser/modules/test/unit/test_FirefoxBridgeExtensionUtilsNativeManifest.js @@ -0,0 +1,222 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); +const { FirefoxBridgeExtensionUtils } = ChromeUtils.importESModule( + "resource:///modules/FirefoxBridgeExtensionUtils.sys.mjs" +); + +const DUAL_BROWSER_EXTENSION_ORIGIN = ["chrome-extension://fake-origin/"]; +const NATIVE_MESSAGING_HOST_ID = "org.mozilla.firefox_bridge_test"; + +let dir = FileUtils.getDir("TmpD", ["NativeMessagingHostsTest"]); +dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +let userDir = dir.clone(); +userDir.append("user"); +userDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +let appDir = dir.clone(); +appDir.append("app"); +appDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + +let dirProvider = { + getFile(property) { + if (property == "Home") { + return userDir.clone(); + } else if (property == "AppData") { + return appDir.clone(); + } + return null; + }, +}; + +try { + Services.dirsvc.undefine("Home"); +} catch (e) {} +try { + Services.dirsvc.undefine("AppData"); +} catch (e) {} +Services.dirsvc.registerProvider(dirProvider); + +registerCleanupFunction(() => { + Services.dirsvc.unregisterProvider(dirProvider); + dir.remove(true); +}); + +const USER_TEST_PATH = PathUtils.join(userDir.path, "manifestDir"); + +let binFile = null; +add_setup(async function () { + binFile = Services.dirsvc.get("XREExeF", Ci.nsIFile).parent.clone(); + if (AppConstants.platform == "win") { + binFile.append("nmhproxy.exe"); + } else if (AppConstants.platform == "macosx") { + binFile.append("nmhproxy"); + } else { + throw new Error("Unsupported platform"); + } +}); + +function getExpectedOutput() { + return { + name: NATIVE_MESSAGING_HOST_ID, + description: "Firefox Native Messaging Host", + path: binFile.path, + type: "stdio", + allowed_origins: DUAL_BROWSER_EXTENSION_ORIGIN, + }; +} + +add_task(async function test_maybeWriteManifestFiles() { + await FirefoxBridgeExtensionUtils.maybeWriteManifestFiles( + USER_TEST_PATH, + NATIVE_MESSAGING_HOST_ID, + DUAL_BROWSER_EXTENSION_ORIGIN + ); + let expectedOutput = JSON.stringify(getExpectedOutput()); + let nmhManifestFilePath = PathUtils.join( + USER_TEST_PATH, + `${NATIVE_MESSAGING_HOST_ID}.json` + ); + let nmhManifestFileContent = await IOUtils.readUTF8(nmhManifestFilePath); + await IOUtils.remove(nmhManifestFilePath); + Assert.equal(nmhManifestFileContent, expectedOutput); +}); + +add_task(async function test_maybeWriteManifestFilesIncorrect() { + let nmhManifestFile = await IOUtils.getFile( + USER_TEST_PATH, + `${NATIVE_MESSAGING_HOST_ID}.json` + ); + + let incorrectInput = { + name: NATIVE_MESSAGING_HOST_ID, + description: "Manifest with unexpected description", + path: binFile.path, + type: "stdio", + allowed_origins: DUAL_BROWSER_EXTENSION_ORIGIN, + }; + await IOUtils.writeJSON(nmhManifestFile.path, incorrectInput); + + // Write correct JSON to the file and check to make sure it matches + // the expected output + await FirefoxBridgeExtensionUtils.maybeWriteManifestFiles( + USER_TEST_PATH, + NATIVE_MESSAGING_HOST_ID, + DUAL_BROWSER_EXTENSION_ORIGIN + ); + let expectedOutput = JSON.stringify(getExpectedOutput()); + + let nmhManifestFilePath = PathUtils.join( + USER_TEST_PATH, + `${NATIVE_MESSAGING_HOST_ID}.json` + ); + let nmhManifestFileContent = await IOUtils.readUTF8(nmhManifestFilePath); + await IOUtils.remove(nmhManifestFilePath); + Assert.equal(nmhManifestFileContent, expectedOutput); +}); + +add_task(async function test_maybeWriteManifestFilesAlreadyExists() { + // Write file and confirm it exists + await FirefoxBridgeExtensionUtils.maybeWriteManifestFiles( + USER_TEST_PATH, + NATIVE_MESSAGING_HOST_ID, + DUAL_BROWSER_EXTENSION_ORIGIN + ); + let nmhManifestFile = await IOUtils.getFile( + USER_TEST_PATH, + `${NATIVE_MESSAGING_HOST_ID}.json` + ); + + // Modify file modificatiomn time to be older than the write time + let oldModificationTime = Date.now() - 1000000; + let setModificationTime = await IOUtils.setModificationTime( + nmhManifestFile.path, + oldModificationTime + ); + Assert.equal(oldModificationTime, setModificationTime); + + // Call function which writes correct JSON to the file and make sure + // the modification time is the same, meaning we haven't written anything + await FirefoxBridgeExtensionUtils.maybeWriteManifestFiles( + USER_TEST_PATH, + NATIVE_MESSAGING_HOST_ID, + DUAL_BROWSER_EXTENSION_ORIGIN + ); + let stat = await IOUtils.stat(nmhManifestFile.path); + await IOUtils.remove(nmhManifestFile.path); + Assert.equal(stat.lastModified, oldModificationTime); +}); + +add_task(async function test_maybeWriteManifestFilesDirDoesNotExist() { + let testDir = dir.clone(); + // This folder does not exist, so we want to make sure it's created + testDir.append("dirDoesNotExist"); + await FirefoxBridgeExtensionUtils.maybeWriteManifestFiles( + testDir.path, + NATIVE_MESSAGING_HOST_ID, + DUAL_BROWSER_EXTENSION_ORIGIN + ); + + ok(await IOUtils.exists(testDir.path)); + ok( + await IOUtils.exists( + PathUtils.join(testDir.path, `${NATIVE_MESSAGING_HOST_ID}.json`) + ) + ); + await IOUtils.remove(testDir.path, { recursive: true }); +}); + +add_task(async function test_ensureRegistered() { + let expectedJSONDirPath = null; + let nativeHostId = "org.mozilla.firefox_bridge_nmh"; + if (AppConstants.NIGHTLY_BUILD) { + nativeHostId = "org.mozilla.firefox_bridge_nmh_nightly"; + } else if (AppConstants.MOZ_DEV_EDITION) { + nativeHostId = "org.mozilla.firefox_bridge_nmh_dev"; + } else if (AppConstants.IS_ESR) { + nativeHostId = "org.mozilla.firefox_bridge_nmh_esr"; + } + + if (AppConstants.platform == "macosx") { + expectedJSONDirPath = PathUtils.joinRelative( + userDir.path, + "Library/Application Support/Google/Chrome/NativeMessagingHosts/" + ); + } else if (AppConstants.platform == "win") { + expectedJSONDirPath = PathUtils.joinRelative( + appDir.path, + "Mozilla\\Firefox" + ); + } else { + throw new Error("Unsupported platform"); + } + + ok(!(await IOUtils.exists(expectedJSONDirPath))); + let expectedJSONPath = PathUtils.join( + expectedJSONDirPath, + `${nativeHostId}.json` + ); + + await FirefoxBridgeExtensionUtils.ensureRegistered(); + let realOutput = { + name: nativeHostId, + description: "Firefox Native Messaging Host", + path: binFile.path, + type: "stdio", + allowed_origins: FirefoxBridgeExtensionUtils.getExtensionOrigins(), + }; + + let expectedOutput = JSON.stringify(realOutput); + let JSONContent = await IOUtils.readUTF8(expectedJSONPath); + await IOUtils.remove(expectedJSONPath); + Assert.equal(JSONContent, expectedOutput); +}); diff --git a/browser/modules/test/unit/xpcshell.toml b/browser/modules/test/unit/xpcshell.toml index 1738e92194..dcc9a98985 100644 --- a/browser/modules/test/unit/xpcshell.toml +++ b/browser/modules/test/unit/xpcshell.toml @@ -5,6 +5,15 @@ skip-if = ["os == 'android'"] # bug 1730213 ["test_E10SUtils_nested_URIs.js"] +["test_FirefoxBridgeExtensionUtils.js"] +run-if = ["os == 'win'"] # Test of a Windows-specific feature + +["test_FirefoxBridgeExtensionUtilsNativeManifest.js"] +run-if = [ + "os == 'win'", + "os == 'mac'", +] + ["test_HomePage.js"] ["test_HomePage_ignore.js"] |