commit c01bee7d4143bff35e8b0565c259bd118595e3d3
parent a5f0adfbc8343f1ae430cb347c18e524a7704b90
Author: Meg Viar <lmegviar@gmail.com>
Date: Thu, 6 Nov 2025 13:54:29 +0000
Bug 1998516 - Add ability to set a pref to the value of an existing pref via the SET_PREF special message action r=mimi,omc-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D271487
Diffstat:
4 files changed, 232 insertions(+), 2 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
@@ -1835,11 +1835,14 @@ pref("browser.newtab.preload", true);
pref("termsofuse.acceptedVersion", 0);
// Stringified timestamp of when the user last accepted the TOU
pref("termsofuse.acceptedDate", "0");
+// Stringified timestamp of when the user first accepted the TOU, only set if
+// they have accepted more than one version.
+pref("termsofuse.firstAcceptedDate", "0");
// The most up-to-date version of the TOU, we set the minimum and current
// version as 4 to distinguish it from version numbers used in previous TOU
// experiments and rollouts
pref("termsofuse.currentVersion", 4);
-// The minimum version fo the TOU that a user must have accepted to not be
+// The minimum version of the TOU that a user must have accepted to not be
// presented with the TOU modal
pref("termsofuse.minimumVersion", 4);
// Should we bypass the TOU modal notification completely, currently only true
diff --git a/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs b/toolkit/components/messaging-system/lib/SpecialMessageActions.sys.mjs
@@ -193,6 +193,49 @@ export const SpecialMessageActions = {
},
/**
+ * Set a target pref's value to the value of a source pref.
+ *
+ * @param {string} targetPref - The name of a pref to be updated.
+ * @param {string} sourcePref - The name of the source pref whose value will be copied to the target pref.
+ */
+ copyPrefValue(targetPref, sourcePref) {
+ const sourceType = Services.prefs.getPrefType(sourcePref);
+ const targetType = Services.prefs.getPrefType(targetPref);
+ if (
+ targetType !== Services.prefs.PREF_INVALID &&
+ targetType !== sourceType
+ ) {
+ throw new Error(
+ `Special message action with type SET_PREF(copyFromPref), target pref "${targetPref}" has type ${targetType} which does not match source pref "${sourcePref}" type ${sourceType}.`
+ );
+ }
+ switch (sourceType) {
+ case Services.prefs.PREF_STRING:
+ Services.prefs.setStringPref(
+ targetPref,
+ Services.prefs.getStringPref(sourcePref)
+ );
+ break;
+ case Services.prefs.PREF_INT:
+ Services.prefs.setIntPref(
+ targetPref,
+ Services.prefs.getIntPref(sourcePref)
+ );
+ break;
+ case Services.prefs.PREF_BOOL:
+ Services.prefs.setBoolPref(
+ targetPref,
+ Services.prefs.getBoolPref(sourcePref)
+ );
+ break;
+ default:
+ throw new Error(
+ `Special message action with type SET_PREF(copyFromPref), pref of "${sourcePref}" is invalid or not a supported type.`
+ );
+ }
+ },
+
+ /**
* Set prefs with special message actions
*
* @param {Object} pref - A pref to be updated.
@@ -235,6 +278,7 @@ export const SpecialMessageActions = {
"sidebar.visibility",
"termsofuse.acceptedVersion",
"termsofuse.acceptedDate",
+ "termsofuse.firstAcceptedDate",
"termsofuse.currentVersion",
"termsofuse.minimumVersion",
"privacy.trackingprotection.allow_list.baseline.enabled",
@@ -263,6 +307,8 @@ export const SpecialMessageActions = {
case "object":
if (pref.value.timestamp) {
Services.prefs.setStringPref(pref.name, Date.now().toString());
+ } else if (pref.value.copyFromPref) {
+ this.copyPrefValue(pref.name, pref.value.copyFromPref);
} else {
Services.prefs.clearUserPref(pref.name);
}
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/SpecialMessageActionSchemas.json
@@ -544,7 +544,7 @@
"type": ["boolean", "string", "number", "null", "object"]
}
},
- "description": "An object representing a pref containing a name and a value. The value can be an object with the property `timestamp` with a truthy value to set the pref to a string of the current time in ms."
+ "description": "An object representing a pref containing a name and a value. The value can be an object with the property `timestamp` with a truthy value to set the pref to a string of the current time in ms. It can also be an object with the property 'copyFromPref' with a string value that is the name of a source pref whose value will be copied to the target pref."
},
"onImpression": {
"type": "boolean",
diff --git a/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_set_prefs.js b/toolkit/components/messaging-system/schemas/SpecialMessageActionSchemas/test/browser/browser_sma_set_prefs.js
@@ -333,3 +333,184 @@ add_task(async function test_update_namespaced_timestamp_pref_onImpression() {
Services.prefs.clearUserPref(namespaced);
});
+
+add_task(async function test_copyFromPref_string() {
+ const sourcePrefName = "messaging-system-action.source.string";
+ Services.prefs.setStringPref(sourcePrefName, "test");
+
+ const targetPrefName = "messaging-system-action.target.string";
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(targetPrefName);
+ Services.prefs.clearUserPref(sourcePrefName);
+ });
+
+ const action = {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: targetPrefName,
+ value: { copyFromPref: sourcePrefName },
+ },
+ },
+ };
+
+ await SMATestUtils.executeAndValidateAction(action);
+
+ Assert.equal(
+ Services.prefs.getStringPref(targetPrefName),
+ Services.prefs.getStringPref(sourcePrefName),
+ "Copied source string pref value to target pref"
+ );
+});
+
+add_task(async function test_copyFromPref_bool() {
+ const sourcePrefName = "messaging-system-action.source.bool";
+ Services.prefs.setBoolPref(sourcePrefName, true);
+
+ const targetPrefName = "messaging-system-action.target.bool";
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(targetPrefName);
+ Services.prefs.clearUserPref(sourcePrefName);
+ });
+
+ const action = {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: targetPrefName,
+ value: { copyFromPref: sourcePrefName },
+ },
+ },
+ };
+
+ await SMATestUtils.executeAndValidateAction(action);
+
+ Assert.equal(
+ Services.prefs.getBoolPref(targetPrefName),
+ Services.prefs.getBoolPref(sourcePrefName),
+ "Copied source bool pref value to target pref"
+ );
+});
+
+add_task(async function test_copyFromPref_int() {
+ const sourcePrefName = "messaging-system-action.source.int";
+ Services.prefs.setIntPref(sourcePrefName, 42);
+
+ const targetPrefName = "messaging-system-action.target.int";
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(targetPrefName);
+ Services.prefs.clearUserPref(sourcePrefName);
+ });
+
+ const action = {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: targetPrefName,
+ value: { copyFromPref: sourcePrefName },
+ },
+ },
+ };
+
+ await SMATestUtils.executeAndValidateAction(action);
+
+ Assert.equal(
+ Services.prefs.getIntPref(targetPrefName),
+ Services.prefs.getIntPref(sourcePrefName),
+ "Copied source int pref value to target pref"
+ );
+});
+
+add_task(async function test_copyFromPref_type_mismatch_throws() {
+ const sourcePrefName = "messaging-system-action.source.mismatch.int";
+ Services.prefs.setIntPref(sourcePrefName, 7);
+
+ const targetPrefName = "messaging-system-action.target.mismatch.bool";
+ Services.prefs.setBoolPref(targetPrefName, true);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(targetPrefName);
+ Services.prefs.clearUserPref(sourcePrefName);
+ });
+
+ const action = {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: targetPrefName,
+ value: { copyFromPref: sourcePrefName },
+ },
+ },
+ };
+
+ await Assert.rejects(
+ SMATestUtils.executeAndValidateAction(action),
+ /SET_PREF\(copyFromPref\).*type/i,
+ "Throws when destination type does not match source type"
+ );
+});
+
+add_task(async function test_copyFromPref_missing_source_throws() {
+ const sourcePrefName = "messaging-system-action.source.missing";
+ const targetPrefName = "messaging-system-action.target.missing";
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(targetPrefName);
+ });
+
+ const action = {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: targetPrefName,
+ value: { copyFromPref: sourcePrefName },
+ },
+ },
+ };
+
+ await Assert.rejects(
+ SMATestUtils.executeAndValidateAction(action),
+ /SET_PREF\(copyFromPref\).*(does not exist|invalid|unsupported)/i,
+ "Throws when source pref does not exist"
+ );
+
+ Assert.equal(
+ Services.prefs.getPrefType(targetPrefName),
+ Services.prefs.PREF_INVALID,
+ "Target pref not created on missing source"
+ );
+});
+
+add_task(async function test_copyFromPref_overwrite_same_type() {
+ const sourcePrefName = "messaging-system-action.source.overwrite.string";
+ Services.prefs.setStringPref(sourcePrefName, "new");
+
+ const targetPrefName = "messaging-system-action.target.overwrite.string";
+ Services.prefs.setStringPref(targetPrefName, "old");
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(targetPrefName);
+ Services.prefs.clearUserPref(sourcePrefName);
+ });
+
+ const action = {
+ type: "SET_PREF",
+ data: {
+ pref: {
+ name: targetPrefName,
+ value: { copyFromPref: sourcePrefName },
+ },
+ },
+ };
+
+ await SMATestUtils.executeAndValidateAction(action);
+
+ Assert.equal(
+ Services.prefs.getStringPref(targetPrefName),
+ Services.prefs.getStringPref(sourcePrefName),
+ "Overwrote existing target value with source value (same type)"
+ );
+});