commit 4a827df2b32cb5c5f28fc93501afa3a0da2206a9 parent 72a7b93d81bf508b084609a67a8a53b1a955b34b Author: Chris DuPuis <cdupuis@mozilla.com> Date: Fri, 21 Nov 2025 17:12:52 +0000 Bug 1997657 - Enable backup and restore by default r=hsohaney,dmcintosh,mconley,mstriemer This is significantly more involved than just flipping a switch because we want Windows 10 machines to have backup enabled but restore disabled, while Windows 11 machines have backup disabled and restore enabled. And for testing, we want the unit tests to all be runnable on any Windows machine. Differential Revision: https://phabricator.services.mozilla.com/D272362 Diffstat:
14 files changed, 227 insertions(+), 48 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -3438,10 +3438,16 @@ pref("browser.mailto.dualPrompt.dismissXClickMinutes", 1440); // one day pref("browser.backup.enabled", true); // Pref to control whether scheduled backups run or not. pref("browser.backup.scheduled.enabled", false); -// Pref to control visibility and usability of the create backup feature. -pref("browser.backup.archive.enabled", false); -// Pref to control visibility and usability of the restore from backup feature. -pref("browser.backup.restore.enabled", false); + +// Prefs to control visibility and usability of the create backup and restore from backup features. +#ifdef XP_WIN + pref("browser.backup.archive.enabled", false); + pref("browser.backup.restore.enabled", false); +#else + pref("browser.backup.archive.enabled", false); + pref("browser.backup.restore.enabled", false); +#endif + // The number of SQLite database pages to backup per step. pref("browser.backup.sqlite.pages_per_step", 50); // The delay between SQLite database backup steps in milliseconds. diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs @@ -24,7 +24,11 @@ const BACKUP_DIR_PREF_NAME = "browser.backup.location"; const BACKUP_ERROR_CODE_PREF_NAME = "browser.backup.errorCode"; const SCHEDULED_BACKUPS_ENABLED_PREF_NAME = "browser.backup.scheduled.enabled"; const BACKUP_ARCHIVE_ENABLED_PREF_NAME = "browser.backup.archive.enabled"; +const BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME = + "browser.backup.archive.overridePlatformCheck"; const BACKUP_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled"; +const BACKUP_RESTORE_ENABLED_OVERRIDE_PREF_NAME = + "browser.backup.restore.overridePlatformCheck"; const IDLE_THRESHOLD_SECONDS_PREF_NAME = "browser.backup.scheduled.idle-threshold-seconds"; const MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME = @@ -675,6 +679,20 @@ export class BackupService extends EventTarget { }; } + if ( + !this.#osSupportsBackup && + !Services.prefs.getBoolPref( + BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, + false + ) + ) { + return { + enabled: false, + reason: "Backup creation not enabled on this os version yet", + internalReason: "os version", + }; + } + return { enabled: true }; } @@ -728,6 +746,19 @@ export class BackupService extends EventTarget { internalReason: "selectable profiles", }; } + if ( + !this.#osSupportsRestore && + !Services.prefs.getBoolPref( + BACKUP_RESTORE_ENABLED_OVERRIDE_PREF_NAME, + false + ) + ) { + return { + enabled: false, + reason: "Backup restore not enabled on this os version yet", + internalReason: "os version", + }; + } return { enabled: true }; } @@ -820,8 +851,6 @@ export class BackupService extends EventTarget { embeddedComponentPersistentData: {}, recoveryErrorCode: ERRORS.NONE, backupErrorCode: lazy.backupErrorCode, - archiveEnabledStatus: this.archiveEnabledStatus.enabled, - restoreEnabledStatus: this.restoreEnabledStatus.enabled, }; /** @@ -1230,6 +1259,17 @@ export class BackupService extends EventTarget { return this.#instance; } + static checkOsSupportsBackup(osParams) { + // Currently we only want to show Backup on Windows 10 devices. + // The first build of Windows 11 is 22000 + return ( + osParams.name == "Windows_NT" && + osParams.version == "10.0" && + osParams.build && + Number(osParams.build) < 22000 + ); + } + /** * Create a BackupService instance. * @@ -1290,8 +1330,25 @@ export class BackupService extends EventTarget { } Glean.browserBackup.restoredProfileData.set(payload); }); + const osParams = { + name: Services.sysinfo.getProperty("name"), + version: Services.sysinfo.getProperty("version"), + build: Services.sysinfo.getProperty("build"), + }; + this.#osSupportsBackup = BackupService.checkOsSupportsBackup(osParams); + this.#osSupportsRestore = true; + this.#lastSeenArchiveStatus = this.archiveEnabledStatus; + this.#lastSeenRestoreStatus = this.restoreEnabledStatus; } + // Backup is currently limited to Windows 10. Will be populated by constructor + #osSupportsBackup = false; + // Restore is not limited, but leaving this in place if restrictions are needed. + #osSupportsRestore = true; + // Remembering status allows us to notify observers when the status changes + #lastSeenArchiveStatus = false; + #lastSeenRestoreStatus = false; + /** * Returns a reference to a Promise that will resolve with undefined once * postRecovery steps have had a chance to run. This will also be resolved @@ -4139,20 +4196,50 @@ export class BackupService extends EventTarget { * 2. If archive is disabled, clean up any backup files */ #handleStatusChange() { + const archiveStatus = this.archiveEnabledStatus; + const restoreStatus = this.restoreEnabledStatus; // Update the BackupService state before notifying observers about the // state change this.#_state.archiveEnabledStatus = this.archiveEnabledStatus.enabled; this.#_state.restoreEnabledStatus = this.restoreEnabledStatus.enabled; - this.#notifyStatusObservers(); - - if (!this.archiveEnabledStatus.enabled) { + this.#updateGleanEnablement(archiveStatus, restoreStatus); + if ( + archiveStatus.enabled != this.#lastSeenArchiveStatus || + restoreStatus.enabled != this.#lastSeenRestoreStatus + ) { + this.#lastSeenArchiveStatus = archiveStatus.enabled; + this.#lastSeenRestoreStatus = restoreStatus.enabled; + this.#notifyStatusObservers(); + } + if (!archiveStatus.enabled) { // We won't wait for this promise to accept/reject since rejections are // ignored anyways this.cleanupBackupFiles(); } } + #updateGleanEnablement(archiveStatus, restoreStatus) { + Glean.browserBackup.archiveEnabled.set(archiveStatus.enabled); + Glean.browserBackup.restoreEnabled.set(restoreStatus.enabled); + if (!archiveStatus.enabled) { + this.#wasArchivePreviouslyDisabled = true; + Glean.browserBackup.archiveDisabledReason.set( + archiveStatus.internalReason + ); + } else if (this.#wasArchivePreviouslyDisabled) { + Glean.browserBackup.archiveDisabledReason.set("reenabled"); + } + if (!restoreStatus.enabled) { + this.#wasRestorePreviouslyDisabled = true; + Glean.browserBackup.restoreDisabledReason.set( + restoreStatus.internalReason + ); + } else if (this.#wasRestorePreviouslyDisabled) { + Glean.browserBackup.restoreDisabledReason.set("reenabled"); + } + } + /** * Notify any listeners about the availability of the backup service, then * update relevant telemetry metrics. @@ -4163,24 +4250,6 @@ export class BackupService extends EventTarget { ); Services.obs.notifyObservers(null, "backup-service-status-updated"); - - let status = this.archiveEnabledStatus; - Glean.browserBackup.archiveEnabled.set(status.enabled); - if (!status.enabled) { - this.#wasArchivePreviouslyDisabled = true; - Glean.browserBackup.archiveDisabledReason.set(status.internalReason); - } else if (this.#wasArchivePreviouslyDisabled) { - Glean.browserBackup.archiveDisabledReason.set("reenabled"); - } - - status = this.restoreEnabledStatus; - Glean.browserBackup.restoreEnabled.set(status.enabled); - if (!status.enabled) { - this.#wasRestorePreviouslyDisabled = true; - Glean.browserBackup.restoreDisabledReason.set(status.internalReason); - } else if (this.#wasRestorePreviouslyDisabled) { - Glean.browserBackup.restoreDisabledReason.set("reenabled"); - } } async cleanupBackupFiles() { diff --git a/browser/components/backup/metrics.yaml b/browser/components/backup/metrics.yaml @@ -716,7 +716,7 @@ browser.backup: description: > Only set if `browser.backup.enabled` is `false`. Possible reasons are "nimbus", "pref" (non-Nimbus), "policy", "sanitizeOnShutdown", - "selectable profiles". + "selectable profiles", "os version". bugs: - https://bugzilla.mozilla.org/show_bug.cgi?id=1992808 data_reviews: diff --git a/browser/components/backup/tests/browser/browser.toml b/browser/components/backup/tests/browser/browser.toml @@ -4,6 +4,8 @@ prefs = [ "browser.backup.archive.enabled=true", "browser.backup.restore.enabled=true", "browser.backup.scheduled.enabled=false", + "browser.backup.archive.overridePlatformCheck=true", + "browser.backup.restore.overridePlatformCheck=true", ] support-files = [ "head.js", diff --git a/browser/components/backup/tests/chrome/chrome.toml b/browser/components/backup/tests/chrome/chrome.toml @@ -3,6 +3,8 @@ prefs = [ "browser.backup.scheduled.enabled=false", "browser.backup.archive.enabled=true", "browser.backup.restore.enabled=true", + "browser.backup.archive.overridePlatformCheck=true", + "browser.backup.restore.overridePlatformCheck=true", "privacy.sanitize.sanitizeOnShutdown=false" ] support-files = ["head.js"] diff --git a/browser/components/backup/tests/chrome/test_disable_backup_encryption.html b/browser/components/backup/tests/chrome/test_disable_backup_encryption.html @@ -5,6 +5,7 @@ <title>Tests for the disable-backup-encryption component</title> <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="head.js"></script> <script src="chrome://browser/content/backup/disable-backup-encryption.mjs" type="module" diff --git a/browser/components/backup/tests/chrome/test_restore_from_backup.html b/browser/components/backup/tests/chrome/test_restore_from_backup.html @@ -5,6 +5,7 @@ <title>Tests for the restore-from-backup component</title> <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="head.js"></script> <script src="chrome://browser/content/backup/restore-from-backup.mjs" type="module" diff --git a/browser/components/backup/tests/chrome/test_turn_off_scheduled_backups.html b/browser/components/backup/tests/chrome/test_turn_off_scheduled_backups.html @@ -5,6 +5,7 @@ <title>Tests for the turn-off-scheduled-backups component</title> <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <script type="application/javascript" src="head.js"></script> <script src="chrome://browser/content/backup/turn-off-scheduled-backups.mjs" type="module" diff --git a/browser/components/backup/tests/marionette/test_backup.py b/browser/components/backup/tests/marionette/test_backup.py @@ -27,6 +27,8 @@ class BackupTest(MarionetteTestCase): "browser.backup.log": True, "browser.backup.archive.enabled": True, "browser.backup.restore.enabled": True, + "browser.backup.archive.overridePlatformCheck": True, + "browser.backup.restore.overridePlatformCheck": True, # Necessary to test Session Restore from backup, which relies on # the crash restore mechanism. "browser.sessionstore.resume_from_crash": True, diff --git a/browser/components/backup/tests/xpcshell/test_BackupService.js b/browser/components/backup/tests/xpcshell/test_BackupService.js @@ -1272,15 +1272,14 @@ add_task(async function test_getBackupFileInfo_error_handling() { */ add_task(async function test_changing_prefs_cleanup() { let sandbox = sinon.createSandbox(); - let bs = BackupService.init(); - + Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true); + let bs = new BackupService(); + bs.initStatusObservers(); let cleanupStub = sandbox.stub(bs, "cleanupBackupFiles"); let statusUpdatePromise = TestUtils.topicObserved( "backup-service-status-updated" ); - Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, false); - await statusUpdatePromise; Assert.equal( @@ -1299,3 +1298,33 @@ add_task(async function test_changing_prefs_cleanup() { Services.prefs.clearUserPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); }); + +add_task(function test_checkOsSupportsBackup_win10() { + const osParams = { + name: "Windows_NT", + version: "10.0", + build: "20000", + }; + const result = BackupService.checkOsSupportsBackup(osParams); + Assert.ok(result); +}); + +add_task(function test_checkOsSupportsBackup_win11() { + const osParams = { + name: "Windows_NT", + version: "10.0", + build: "22000", + }; + const result = BackupService.checkOsSupportsBackup(osParams); + Assert.ok(!result); +}); + +add_task(function test_checkOsSupportsBackup_linux() { + const osParams = { + name: "Linux", + version: "10.0", + build: "22000", + }; + const result = BackupService.checkOsSupportsBackup(osParams); + Assert.ok(!result); +}); diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_enabled.js b/browser/components/backup/tests/xpcshell/test_BackupService_enabled.js @@ -12,6 +12,8 @@ const { NimbusTestUtils } = ChromeUtils.importESModule( const BACKUP_DIR_PREF_NAME = "browser.backup.location"; const BACKUP_ARCHIVE_ENABLED_PREF_NAME = "browser.backup.archive.enabled"; +const BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME = + "browser.backup.archive.overridePlatformCheck"; const BACKUP_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled"; const SANITIZE_ON_SHUTDOWN_PREF_NAME = "privacy.sanitize.sanitizeOnShutdown"; const SELECTABLE_PROFILES_CREATED_PREF_NAME = "browser.profiles.created"; @@ -60,7 +62,7 @@ add_task(async function test_archive_killswitch_enrollment() { }, // Nimbus calls onUpdate if any experiments are running, meaning that the // observer service will be notified twice, i.e. one spurious call. - startup: 1, + startup: 0, }); }); @@ -68,6 +70,7 @@ add_task(async function test_archive_enabled_pref() { await archiveTemplate({ internalReason: "pref", async disable() { + Services.prefs.unlockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, false); }, async enable() { @@ -77,20 +80,26 @@ add_task(async function test_archive_enabled_pref() { }); add_task(async function test_archive_policy() { + let storedDefault; await archiveTemplate({ internalReason: "policy", - async disable() { - Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, false); - Services.prefs.lockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); - return 1; + disable: () => { + Services.prefs.unlockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); + const defaults = Services.prefs.getDefaultBranch(""); + storedDefault = defaults.getBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); + defaults.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, false); + defaults.lockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); + return 0; }, - async enable() { + enable: () => { + const defaults = Services.prefs.getDefaultBranch(""); + defaults.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, storedDefault); Services.prefs.unlockPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME); Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME, true); - return 1; + return 0; }, // At startup, there wouldn't have been a spurious call. - startup: -1, + startup: 0, }); }); @@ -118,6 +127,49 @@ add_task(async function test_archive_selectable_profiles() { }); }); +add_task(async function test_archive_disabled_unsupported_os() { + const sandbox = sinon.createSandbox(); + const archiveWasEnabled = Services.prefs.getBoolPref( + BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, + false + ); + Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, false); + sandbox.stub(BackupService, "checkOsSupportsBackup").returns(false); + try { + const bs = new BackupService(); + const status = bs.archiveEnabledStatus; + Assert.equal(false, status.enabled); + Assert.equal("os version", status.internalReason); + } finally { + sandbox.restore(); + Services.prefs.setBoolPref( + BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, + archiveWasEnabled + ); + } +}); + +add_task(async function test_archive_enabled_supported_os() { + const sandbox = sinon.createSandbox(); + const archiveWasEnabled = Services.prefs.getBoolPref( + BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, + false + ); + Services.prefs.setBoolPref(BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, false); + sandbox.stub(BackupService, "checkOsSupportsBackup").returns(true); + try { + const bs = new BackupService(); + const status = bs.archiveEnabledStatus; + Assert.equal(true, status.enabled); + } finally { + sandbox.restore(); + Services.prefs.setBoolPref( + BACKUP_ARCHIVE_ENABLED_OVERRIDE_PREF_NAME, + archiveWasEnabled + ); + } +}); + add_task(async function test_restore_killswitch_enrollment() { let cleanupExperiment; await restoreTemplate({ @@ -133,7 +185,7 @@ add_task(async function test_restore_killswitch_enrollment() { }, // Nimbus calls onUpdate if any experiments are running, meaning that the // observer service will be notified twice, i.e. one spurious call. - startup: 1, + startup: 0, }); }); @@ -150,20 +202,24 @@ add_task(async function test_restore_enabled_pref() { }); add_task(async function test_restore_policy() { + let storedDefault; await restoreTemplate({ internalReason: "policy", async disable() { - Services.prefs.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, false); + const defaults = Services.prefs.getDefaultBranch(""); + storedDefault = defaults.getBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME); + defaults.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, false); Services.prefs.lockPref(BACKUP_RESTORE_ENABLED_PREF_NAME); - return 1; + return 0; }, async enable() { Services.prefs.unlockPref(BACKUP_RESTORE_ENABLED_PREF_NAME); - Services.prefs.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, true); - return 1; + const defaults = Services.prefs.getDefaultBranch(""); + defaults.setBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME, storedDefault); + return 0; }, // At startup, there wouldn't have been a spurious call. - startup: -1, + startup: 0, }); }); diff --git a/browser/components/backup/tests/xpcshell/test_BackupService_wrongPassword.js b/browser/components/backup/tests/xpcshell/test_BackupService_wrongPassword.js @@ -28,12 +28,14 @@ add_setup(async function () { PathUtils.tempDir, "wrongPasswordTestBackup" ); + bs = new BackupService({ FakeBackupResource1 }); await bs.enableEncryption(correctPassword); testBackupPath = (await bs.createBackup({ profilePath: testBackupDirPath })) .archivePath; registerCleanupFunction(async () => { sandbox.restore(); + bs = null; await IOUtils.remove(testBackupDirPath, { recursive: true }); }); diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -1,11 +1,15 @@ [DEFAULT] -run-if = ["os != 'android'"] +run-if = [ + "os != 'android'", +] head = "head.js" firefox-appdir = "browser" prefs = [ "browser.backup.log=true", "browser.backup.archive.enabled=true", "browser.backup.restore.enabled=true", + "browser.backup.archive.overridePlatformCheck=true", + "browser.backup.restore.overridePlatformCheck=true", ] ["test_AddonsBackupResource.js"] @@ -45,7 +49,9 @@ support-files = [ ["test_BackupService_internalPostRecovery.js"] ["test_BackupService_onedrive.js"] -run-if = ["os == 'win'"] +run-if = [ + "os == 'win'", +] run-sequentially = ["true"] # Mock Windows registry interferes with normal operations ["test_BackupService_recoverFromSnapshotFolder.js"] diff --git a/browser/components/preferences/tests/browser.toml b/browser/components/preferences/tests/browser.toml @@ -2,6 +2,8 @@ prefs = [ "extensions.formautofill.addresses.available='on'", "signon.management.page.os-auth.enabled=true", + "browser.backup.archive.overridePlatformCheck=true", + "browser.backup.restore.overridePlatformCheck=true", ] support-files = [ "head.js",