diff options
Diffstat (limited to 'browser/components/installerprefs')
10 files changed, 443 insertions, 0 deletions
diff --git a/browser/components/installerprefs/InstallerPrefs.jsm b/browser/components/installerprefs/InstallerPrefs.jsm new file mode 100644 index 0000000000..668f25c12f --- /dev/null +++ b/browser/components/installerprefs/InstallerPrefs.jsm @@ -0,0 +1,148 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/** + * The installer prefs component provides a way to get a specific set of prefs + * from a profile into a place where the installer can read them. The primary + * reason for wanting to do this is so we can run things like Shield studies + * on installer features; normally those are enabled by setting a pref, but + * the installer runs outside of any profile and so has no access to prefs. + * So we need to do something else to allow it to read these prefs. + * + * The mechanism we use here is to reflect the values of a list of relevant + * prefs into registry values. One registry value is created for each pref + * that is set. Each installation of the product gets its own registry key + * (based on the path hash). This is obviously a somewhat wider scope than a + * single profile, but it should be close enough in enough cases to suit our + * purposes here. + * + * Currently this module only supports bool prefs. Other types could likely + * be added if needed, but it doesn't seem necessary for the primary use case. + */ + +// All prefs processed through this component must be in this branch. +const INSTALLER_PREFS_BRANCH = "installer."; + +// This is the list of prefs that will be reflected to the registry. It should +// be kept up to date so that it reflects the list of prefs that are in +// current use (e.g., currently active experiments). +// Only add prefs to this list which are in INSTALLER_PREFS_BRANCH; +// any others will be ignored. +const INSTALLER_PREFS_LIST = ["installer.taskbarpin.win10.enabled"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// This constructor can take a list of prefs to override the default one, +// but this is really only intended for tests to use. Normal usage should be +// to leave the parameter omitted/undefined. +function InstallerPrefs(prefsList) { + this.prefsList = prefsList || INSTALLER_PREFS_LIST; + + // Each pref to be reflected will get a value created under this key, in HKCU. + // The path will look something like: + // "Software\Mozilla\Firefox\Installer\71AE18FE3142402B\". + XPCOMUtils.defineLazyGetter(this, "_registryKeyPath", function() { + const app = AppConstants.MOZ_APP_NAME; + const vendor = Services.appinfo.vendor || "Mozilla"; + const xreDirProvider = Cc[ + "@mozilla.org/xre/directory-provider;1" + ].getService(Ci.nsIXREDirProvider); + const installHash = xreDirProvider.getInstallHash(); + return `Software\\${vendor}\\${app}\\Installer\\${installHash}`; + }); +} + +InstallerPrefs.prototype = { + classID: Components.ID("{cd8a6995-1f19-4cdd-9ed1-d6263302f594}"), + contractID: "@mozilla.org/installerprefs;1", + + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + observe(subject, topic, data) { + switch (topic) { + case "profile-after-change": { + if ( + AppConstants.platform != "win" || + !this.prefsList || + !this.prefsList.length + ) { + // This module has no work to do. + break; + } + const regKey = this._openRegKey(); + this._reflectPrefsToRegistry(regKey); + this._registerPrefListeners(); + regKey.close(); + break; + } + case "nsPref:changed": { + const regKey = this._openRegKey(); + if (this.prefsList.includes(data)) { + this._reflectOnePrefToRegistry(regKey, data); + } + regKey.close(); + break; + } + } + }, + + _registerPrefListeners() { + Services.prefs.addObserver(INSTALLER_PREFS_BRANCH, this); + }, + + _cleanRegistryKey(regKey) { + for (let i = regKey.valueCount - 1; i >= 0; --i) { + const name = regKey.getValueName(i); + if (name.startsWith(INSTALLER_PREFS_BRANCH)) { + regKey.removeValue(name); + } + } + }, + + _reflectPrefsToRegistry(regKey) { + this._cleanRegistryKey(regKey); + this.prefsList.forEach(pref => + this._reflectOnePrefToRegistry(regKey, pref) + ); + }, + + _reflectOnePrefToRegistry(regKey, pref) { + if (!pref.startsWith(INSTALLER_PREFS_BRANCH)) { + return; + } + + const value = Services.prefs.getBoolPref(pref, false); + if (value) { + regKey.writeIntValue(pref, 1); + } else { + try { + regKey.removeValue(pref); + } catch (ex) { + // This removeValue call is prone to failing because the value we + // tried to remove didn't exist. Obviously that isn't really an error + // that we need to handle. + } + } + }, + + _openRegKey() { + const key = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + key.create( + key.ROOT_KEY_CURRENT_USER, + this._registryKeyPath, + key.ACCESS_READ | key.ACCESS_WRITE | key.WOW64_64 + ); + return key; + }, +}; + +var EXPORTED_SYMBOLS = ["InstallerPrefs"]; diff --git a/browser/components/installerprefs/components.conf b/browser/components/installerprefs/components.conf new file mode 100644 index 0000000000..2bd525f0b0 --- /dev/null +++ b/browser/components/installerprefs/components.conf @@ -0,0 +1,16 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{cd8a6995-1f19-4cdd-9ed1-d6263302f594}', + 'contract_ids': ['@mozilla.org/installerprefs;1'], + 'jsm': 'resource:///modules/InstallerPrefs.jsm', + 'constructor': 'InstallerPrefs', + 'categories': {'profile-after-change': 'InstallerPrefs'}, + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, +] diff --git a/browser/components/installerprefs/moz.build b/browser/components/installerprefs/moz.build new file mode 100644 index 0000000000..853f2d888a --- /dev/null +++ b/browser/components/installerprefs/moz.build @@ -0,0 +1,18 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "Installer") + +XPCSHELL_TESTS_MANIFESTS += ["test/unit/xpcshell.ini"] + +EXTRA_JS_MODULES += [ + "InstallerPrefs.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/browser/components/installerprefs/test/unit/head.js b/browser/components/installerprefs/test/unit/head.js new file mode 100644 index 0000000000..3d00a2437f --- /dev/null +++ b/browser/components/installerprefs/test/unit/head.js @@ -0,0 +1,137 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const { InstallerPrefs } = ChromeUtils.import( + "resource:///modules/InstallerPrefs.jsm" +); + +let gRegistryKeyPath = ""; + +function startModule(prefsList) { + // Construct an InstallerPrefs object and simulate a profile-after-change + // event on it, so that it performs its full startup procedure. + const prefsModule = new InstallerPrefs(prefsList); + prefsModule.observe(null, "profile-after-change", ""); + + gRegistryKeyPath = prefsModule._registryKeyPath; + + registerCleanupFunction(() => cleanupReflectedPrefs(prefsList)); +} + +function getRegistryKey() { + const key = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + key.open( + key.ROOT_KEY_CURRENT_USER, + gRegistryKeyPath, + key.ACCESS_READ | key.WOW64_64 + ); + return key; +} + +function verifyReflectedPrefs(prefsList) { + let key; + try { + key = getRegistryKey(); + } catch (ex) { + Assert.ok(false, `Failed to open registry key: ${ex}`); + return; + } + + for (const pref of prefsList) { + if (pref.startsWith("installer.")) { + if (Services.prefs.getPrefType(pref) != Services.prefs.PREF_BOOL) { + Assert.ok( + !key.hasValue(pref), + `Pref ${pref} should not be in the registry because its type is not bool` + ); + } else if (Services.prefs.getBoolPref(pref, false)) { + Assert.ok(key.hasValue(pref), `Pref ${pref} should be in the registry`); + Assert.equal( + key.getValueType(pref), + key.TYPE_INT, + `Pref ${pref} should be type DWORD` + ); + Assert.equal( + key.readIntValue(pref), + 1, + `Pref ${pref} should have value 1` + ); + } else { + Assert.ok( + !key.hasValue(pref), + `Pref ${pref} should not be in the registry because it is false` + ); + } + } else { + Assert.ok( + !key.hasValue(pref), + `Pref ${pref} should not be in the registry because its name is invalid` + ); + } + } + + key.close(); +} + +function cleanupReflectedPrefs(prefsList) { + // Clear out the prefs themselves. + prefsList.forEach(pref => Services.prefs.clearUserPref(pref)); + + // Get the registry key path without the path hash at the end, + // then delete the subkey with the path hash. + const app = AppConstants.MOZ_APP_NAME; + const vendor = Services.appinfo.vendor || "Mozilla"; + const xreDirProvider = Cc["@mozilla.org/xre/directory-provider;1"].getService( + Ci.nsIXREDirProvider + ); + + const path = `Software\\${vendor}\\${app}\\Installer`; + + const key = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + key.open( + key.ROOT_KEY_CURRENT_USER, + path, + key.ACCESS_READ | key.ACCESS_WRITE | key.WOW64_64 + ); + const installHash = xreDirProvider.getInstallHash(); + key.removeChild(installHash); + } catch (ex) { + // Nothing left to clean up. + return; + } + + // If the Installer key is now empty, we need to clean it up also, because + // that would mean that this test created it. + if (key.childCount == 0) { + // Unfortunately we can't delete the actual open key, so we'll have to + // open its parent and delete the one we're after as a child. + key.close(); + const parentKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + try { + parentKey.open( + parentKey.ROOT_KEY_CURRENT_USER, + `Software\\${vendor}\\${app}`, + parentKey.ACCESS_READ | parentKey.ACCESS_WRITE | parentKey.WOW64_64 + ); + parentKey.removeChild("Installer"); + parentKey.close(); + } catch (ex) { + // Nothing we can do, and this isn't worth failing the test over. + } + } else { + key.close(); + } +} diff --git a/browser/components/installerprefs/test/unit/test_empty_prefs_list.js b/browser/components/installerprefs/test/unit/test_empty_prefs_list.js new file mode 100644 index 0000000000..a42e0d50bb --- /dev/null +++ b/browser/components/installerprefs/test/unit/test_empty_prefs_list.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/** + * Test passing an empty list of pref names to the module. + */ + +add_task(function() { + const PREFS_LIST = []; + + startModule(PREFS_LIST); + + Assert.throws( + getRegistryKey, + /NS_ERROR_FAILURE/, + "The registry key shouldn't have been created, so opening it should fail" + ); +}); diff --git a/browser/components/installerprefs/test/unit/test_invalid_name.js b/browser/components/installerprefs/test/unit/test_invalid_name.js new file mode 100644 index 0000000000..7de279c338 --- /dev/null +++ b/browser/components/installerprefs/test/unit/test_invalid_name.js @@ -0,0 +1,20 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/** + * Test that using prefs with invalid names is not allowed. + */ + +add_task(function() { + const PREFS_LIST = [ + "the.wrong.branch", + "installer.the.right.branch", + "not.a.real.pref", + ]; + + startModule(PREFS_LIST); + + verifyReflectedPrefs(PREFS_LIST); +}); diff --git a/browser/components/installerprefs/test/unit/test_nonbool_pref.js b/browser/components/installerprefs/test/unit/test_nonbool_pref.js new file mode 100644 index 0000000000..ad2f0be860 --- /dev/null +++ b/browser/components/installerprefs/test/unit/test_nonbool_pref.js @@ -0,0 +1,19 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/** + * Test that prefs of types other than bool are not reflected. + */ + +add_task(function() { + const PREFS_LIST = ["installer.int", "installer.string"]; + + Services.prefs.setIntPref("installer.int", 12); + Services.prefs.setStringPref("installer.string", "I'm a string"); + + startModule(PREFS_LIST); + + verifyReflectedPrefs(PREFS_LIST); +}); diff --git a/browser/components/installerprefs/test/unit/test_pref_change.js b/browser/components/installerprefs/test/unit/test_pref_change.js new file mode 100644 index 0000000000..1e28f6622c --- /dev/null +++ b/browser/components/installerprefs/test/unit/test_pref_change.js @@ -0,0 +1,26 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/** + * Test that pref values are correctly updated when they change. + */ + +add_task(function() { + const PREFS_LIST = ["installer.pref"]; + + Services.prefs.setBoolPref("installer.pref", true); + + startModule(PREFS_LIST); + + verifyReflectedPrefs(PREFS_LIST); + + Services.prefs.setBoolPref("installer.pref", false); + + verifyReflectedPrefs(PREFS_LIST); + + Services.prefs.setBoolPref("installer.pref", true); + + verifyReflectedPrefs(PREFS_LIST); +}); diff --git a/browser/components/installerprefs/test/unit/test_pref_values.js b/browser/components/installerprefs/test/unit/test_pref_values.js new file mode 100644 index 0000000000..db955da067 --- /dev/null +++ b/browser/components/installerprefs/test/unit/test_pref_values.js @@ -0,0 +1,19 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/** + * Test that pref values are reflected correctly. + */ + +add_task(function() { + const PREFS_LIST = ["installer.true.value", "installer.false.value"]; + + Services.prefs.setBoolPref("installer.true.value", true); + Services.prefs.setBoolPref("installer.false.value", false); + + startModule(PREFS_LIST); + + verifyReflectedPrefs(PREFS_LIST); +}); diff --git a/browser/components/installerprefs/test/unit/xpcshell.ini b/browser/components/installerprefs/test/unit/xpcshell.ini new file mode 100644 index 0000000000..f0ee7e3cea --- /dev/null +++ b/browser/components/installerprefs/test/unit/xpcshell.ini @@ -0,0 +1,20 @@ +[DEFAULT] +head = head.js +firefox-appdir = browser +skip-if = os != 'win' + +# These tests must all run sequentially because they use the same registry key. +# It might be possible to get around this requirement by overriding the install +# hash so each test uses a different key, and if a lot more tests are added here +# then it would be worth looking into that. +[test_empty_prefs_list.js] +run-sequentially = Uses the Windows registry +skip-if = os == 'win' && msix # https://bugzilla.mozilla.org/show_bug.cgi?id=1807932 +[test_invalid_name.js] +run-sequentially = Uses the Windows registry +[test_nonbool_pref.js] +run-sequentially = Uses the Windows registry +[test_pref_change.js] +run-sequentially = Uses the Windows registry +[test_pref_values.js] +run-sequentially = Uses the Windows registry |