commit 59d5dd70c4fe40906a3acad13648dbccfd759d1f parent a982c54cf9162509a9f4093884caeeb8d0d94f0c Author: Harsheet <hsohaney@mozilla.com> Date: Wed, 22 Oct 2025 21:11:32 +0000 Bug 1991951 - (part 2) Split up prefs for backup service to backup and restore. r=omc-reviewers,akulyk,nrishel,mviar,fchasen,jprickett Differential Revision: https://phabricator.services.mozilla.com/D267855 Diffstat:
18 files changed, 224 insertions(+), 76 deletions(-)
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js @@ -3438,8 +3438,10 @@ 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 the visibility of the backup section in about:preferences -pref("browser.backup.preferences.ui.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); // 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/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs @@ -248,7 +248,6 @@ let JSWINDOWACTORS = { "about:welcome*", "chrome://browser/content/spotlight.html", ], - enablePreference: "browser.backup.preferences.ui.enabled", }, BlockedSite: { diff --git a/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_restore_backup.js b/browser/components/aboutwelcome/tests/browser/browser_aboutwelcome_restore_backup.js @@ -57,7 +57,9 @@ add_task(async function test_aboutwelcome_embedded_backup_restore_properties() { await pushPrefs([ "browser.backup.enabled", true, - "browser.backup.preferences.ui.enabled", + "browser.backup.archive.enabled", + true, + "browser.backup.restore.enabled", true, ]); diff --git a/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js b/browser/components/asrouter/tests/browser/browser_asrouter_targeting.js @@ -2443,6 +2443,8 @@ add_task(async function check_backupArchiveEnabled() { const sandbox = sinon.createSandbox(); registerCleanupFunction(() => sandbox.restore()); + await pushPrefs(["browser.backup.archive.enabled", true]); + is( await ASRouterTargeting.Environment.backupArchiveEnabled, true, @@ -2468,6 +2470,8 @@ add_task(async function check_backupRestoreEnabled() { const sandbox = sinon.createSandbox(); registerCleanupFunction(() => sandbox.restore()); + await pushPrefs(["browser.backup.restore.enabled", true]); + is( await ASRouterTargeting.Environment.backupRestoreEnabled, true, diff --git a/browser/components/backup/BackupService.sys.mjs b/browser/components/backup/BackupService.sys.mjs @@ -22,6 +22,8 @@ import { BackupError } from "resource:///modules/backup/BackupError.mjs"; 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_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled"; const IDLE_THRESHOLD_SECONDS_PREF_NAME = "browser.backup.scheduled.idle-threshold-seconds"; const MINIMUM_TIME_BETWEEN_BACKUPS_SECONDS_PREF_NAME = @@ -618,6 +620,13 @@ export class BackupService extends EventTarget { }; } + if (!Services.prefs.getBoolPref(BACKUP_ARCHIVE_ENABLED_PREF_NAME)) { + return { + enabled: false, + reason: "Archiving a profile disabled by user pref.", + }; + } + return { enabled: true }; } @@ -637,6 +646,13 @@ export class BackupService extends EventTarget { }; } + if (!Services.prefs.getBoolPref(BACKUP_RESTORE_ENABLED_PREF_NAME)) { + return { + enabled: false, + reason: "Restoring a profile disabled by user pref.", + }; + } + return { enabled: true }; } @@ -1051,6 +1067,7 @@ export class BackupService extends EventTarget { constructor(backupResources = DefaultBackupResources) { super(); lazy.logConsole.debug("Instantiated"); + this.#registerStatusObservers(); for (const resourceName in backupResources) { let resource = backupResources[resourceName]; @@ -3589,6 +3606,7 @@ export class BackupService extends EventTarget { } case "quit-application-granted": { this.uninitBackupScheduler(); + this.#unregisterStatusObservers(); break; } case "passwordmgr-storage-changed": { @@ -3639,6 +3657,38 @@ export class BackupService extends EventTarget { } } + #registerStatusObservers() { + // We don't use this.#observer since any changes to the prefs or nimbus should + // immediately reflect across any observers, instead of waiting on idle + Services.prefs.addObserver( + BACKUP_ARCHIVE_ENABLED_PREF_NAME, + this.#notifyStatusObservers + ); + Services.prefs.addObserver( + BACKUP_RESTORE_ENABLED_PREF_NAME, + this.#notifyStatusObservers + ); + lazy.NimbusFeatures.backupService.onUpdate(this.#notifyStatusObservers); + } + + #unregisterStatusObservers() { + Services.prefs.removeObserver( + BACKUP_ARCHIVE_ENABLED_PREF_NAME, + this.#notifyStatusObservers + ); + Services.prefs.removeObserver( + BACKUP_RESTORE_ENABLED_PREF_NAME, + this.#notifyStatusObservers + ); + } + + /** + * Notify any listeners about the availability of the backup service. + */ + #notifyStatusObservers = () => { + Services.obs.notifyObservers(null, "backup-service-status-updated"); + }; + /** * Called when the last known backup should be deleted and a new one * created. This uses the #regenerationDebouncer to debounce clusters of diff --git a/browser/components/backup/content/backup-settings.mjs b/browser/components/backup/content/backup-settings.mjs @@ -7,6 +7,12 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; import { getErrorL10nId } from "chrome://browser/content/backup/backup-errors.mjs"; import { ERRORS } from "chrome://browser/content/backup/backup-constants.mjs"; +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BackupService: "resource:///modules/backup/BackupService.sys.mjs", +}); + // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/backup/turn-on-scheduled-backups.mjs"; // eslint-disable-next-line import/no-unassigned-import @@ -26,11 +32,14 @@ const BACKUP_ERROR_CODE_PREF_NAME = "browser.backup.errorCode"; */ export default class BackupSettings extends MozLitElement { #placeholderIconURL = "chrome://global/skin/icons/page-portrait.svg"; + #backupService = lazy.BackupService.init(); static properties = { backupServiceState: { type: Object }, backupErrorCode: { type: Number }, _enableEncryptionTypeAttr: { type: String }, + _archiveEnabled: { type: Boolean }, + _restoreEnabled: { type: Boolean }, }; static get queries() { @@ -88,8 +97,14 @@ export default class BackupSettings extends MozLitElement { }; this.backupErrorCode = this.#readBackupErrorPref(); this._enableEncryptionTypeAttr = ""; + this.updateArchiveAndRestoreState(); } + updateArchiveAndRestoreState = () => { + this._archiveEnabled = this.#backupService.archiveEnabledStatus.enabled; + this._restoreEnabled = this.#backupService.restoreEnabledStatus.enabled; + }; + /** * Dispatches the BackupUI:InitWidget custom event upon being attached to the * DOM, which registers with BackupUIChild for BackupService state updates. @@ -100,6 +115,21 @@ export default class BackupSettings extends MozLitElement { new CustomEvent("BackupUI:InitWidget", { bubbles: true }) ); + Services.obs.addObserver( + this.updateArchiveAndRestoreState, + "backup-service-status-updated" + ); + + this._cleanupObs = () => { + Services.obs.removeObserver( + this.updateArchiveAndRestoreState, + "backup-service-status-updated" + ); + window.removeEventListener("unload", this._cleanupObs); + }; + + window.addEventListener("unload", this._cleanupObs, { once: true }); + this.addEventListener("dialogCancel", this); this.addEventListener("restoreFromBackupConfirm", this); this.addEventListener("restoreFromBackupChooseFile", this); @@ -467,46 +497,46 @@ export default class BackupSettings extends MozLitElement { ${this.turnOffScheduledBackupsDialogTemplate()} ${this.enableBackupEncryptionDialogTemplate()} ${this.disableBackupEncryptionDialogTemplate()} - - <section id="scheduled-backups"> - <div class="backups-control"> - <span - id="scheduled-backups-enabled" - data-l10n-id=${scheduledBackupsEnabledL10nID} - class="heading-medium" - ></span> - - <moz-button - id="backup-trigger-button" - @click=${this.handleBackupTrigger} - data-l10n-id=${backupTriggerL10nID} - ?disabled=${this.backupServiceState.backupInProgress || - !this.backupServiceState.scheduledBackupsEnabled} - ></moz-button> - - <moz-button - id="backup-toggle-scheduled-button" - @click=${this.handleShowScheduledBackups} - data-l10n-id="settings-data-backup-toggle" - ></moz-button> - - ${this.backupServiceState.scheduledBackupsEnabled - ? null - : this.scheduledBackupsDescriptionTemplate()} - </div> - - ${this.backupServiceState.lastBackupDate - ? this.lastBackupInfoTemplate() - : null} - ${this.backupServiceState.scheduledBackupsEnabled - ? this.backupLocationTemplate() - : null} - ${this.backupServiceState.scheduledBackupsEnabled - ? this.sensitiveDataTemplate() - : null} - </section> - - ${this.restoreFromBackupTemplate()} `; + ${this._archiveEnabled + ? html` <section id="scheduled-backups"> + <div class="backups-control"> + <span + id="scheduled-backups-enabled" + data-l10n-id=${scheduledBackupsEnabledL10nID} + class="heading-medium" + ></span> + + <moz-button + id="backup-trigger-button" + @click=${this.handleBackupTrigger} + data-l10n-id=${backupTriggerL10nID} + ?disabled=${this.backupServiceState.backupInProgress || + !this.backupServiceState.scheduledBackupsEnabled} + ></moz-button> + + <moz-button + id="backup-toggle-scheduled-button" + @click=${this.handleShowScheduledBackups} + data-l10n-id="settings-data-backup-toggle" + ></moz-button> + + ${this.backupServiceState.scheduledBackupsEnabled + ? null + : this.scheduledBackupsDescriptionTemplate()} + </div> + + ${this.backupServiceState.lastBackupDate + ? this.lastBackupInfoTemplate() + : null} + ${this.backupServiceState.scheduledBackupsEnabled + ? this.backupLocationTemplate() + : null} + ${this.backupServiceState.scheduledBackupsEnabled + ? this.sensitiveDataTemplate() + : null} + </section>` + : null} + ${this._restoreEnabled ? this.restoreFromBackupTemplate() : null} `; } } diff --git a/browser/components/backup/tests/browser/browser.toml b/browser/components/backup/tests/browser/browser.toml @@ -1,7 +1,8 @@ [DEFAULT] prefs = [ "browser.backup.enabled=true", - "browser.backup.preferences.ui.enabled=true", + "browser.backup.archive.enabled=true", + "browser.backup.restore.enabled=true", "browser.backup.scheduled.enabled=false", ] support-files = [ diff --git a/browser/components/backup/tests/browser/browser_settings.js b/browser/components/backup/tests/browser/browser_settings.js @@ -8,6 +8,8 @@ const { MockRegistrar } = ChromeUtils.importESModule( ); const SCHEDULED_BACKUPS_ENABLED_PREF = "browser.backup.scheduled.enabled"; +const BACKUP_ARCHIVE_ENABLED_PREF = "browser.backup.archive.enabled"; +const BACKUP_RESTORE_ENABLED_PREF = "browser.backup.restore.enabled"; add_setup(async () => { MockFilePicker.init(window.browsingContext); @@ -35,7 +37,7 @@ add_task(async function test_preferences_visibility() { }); await SpecialPowers.pushPrefEnv({ - set: [["browser.backup.preferences.ui.enabled", false]], + set: [[BACKUP_ARCHIVE_ENABLED_PREF, false]], }); await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => { @@ -44,12 +46,40 @@ add_task(async function test_preferences_visibility() { Assert.ok(backupSection, "Found backup preferences section"); Assert.ok( + BrowserTestUtils.isVisible(backupSection), + "Backup section is still visible" + ); + + let settings = browser.contentDocument.querySelector("backup-settings"); + let backupArchiveSection = settings.querySelector("#scheduled-backups"); + + Assert.ok(!backupArchiveSection, "Backup archive section is not available"); + + Assert.ok( + settings.restoreFromBackupEl, + "Backup restore section is available" + ); + }); + await SpecialPowers.pushPrefEnv({ + set: [[BACKUP_RESTORE_ENABLED_PREF, false]], + }); + await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => { + let settings = browser.contentDocument.querySelector("backup-settings"); + Assert.ok( + !settings.restoreFromBackupEl, + "Backup Restore section is not available" + ); + + let backupSection = + browser.contentDocument.querySelector("#dataBackupGroup"); + Assert.ok( BrowserTestUtils.isHidden(backupSection), "Backup section is now hidden" ); }); await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); }); /** @@ -66,6 +96,15 @@ add_task(async function test_disable_backup_encryption_confirm() { .stub(BackupService.prototype, "disableEncryption") .resolves(true); + Assert.ok( + Services.prefs.getBoolPref(BACKUP_RESTORE_ENABLED_PREF), + "Restore pref is back to true" + ); + Assert.ok( + Services.prefs.getBoolPref(BACKUP_ARCHIVE_ENABLED_PREF), + "Archive pref is back to true" + ); + await SpecialPowers.pushPrefEnv({ set: [[SCHEDULED_BACKUPS_ENABLED_PREF, true]], }); @@ -296,7 +335,7 @@ add_task(async function test_last_backup_info_and_location() { await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => { let sandbox = sinon.createSandbox(); - let bs = BackupService.get(); + let bs = getAndMaybeInitBackupService(); await SpecialPowers.pushPrefEnv({ set: [["browser.backup.location", TEST_PROFILE_PATH]], diff --git a/browser/components/backup/tests/browser/browser_settings_create_backup.js b/browser/components/backup/tests/browser/browser_settings_create_backup.js @@ -36,7 +36,7 @@ add_task(async function test_create_new_backup_trigger() { await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => { let settings = browser.contentDocument.querySelector("backup-settings"); - let bs = BackupService.get(); + let bs = getAndMaybeInitBackupService(); Assert.ok(!bs.state.backupInProgress, "There is no backup in progress"); @@ -93,7 +93,7 @@ add_task(async function test_create_new_backup_trigger() { * Tests if the "Backup now" button is disabled if a backup is already underway */ add_task(async function test_create_backup_trigger_disabled() { - let bs = BackupService.get(); + let bs = getAndMaybeInitBackupService(); const sandbox = sinon.createSandbox(); diff --git a/browser/components/backup/tests/browser/browser_settings_errors.js b/browser/components/backup/tests/browser/browser_settings_errors.js @@ -27,7 +27,7 @@ add_task(async function test_error_visibility_heuristic() { sandbox.restore(); }); - const bs = BackupService.get(); + const bs = getAndMaybeInitBackupService(); sandbox .stub(bs, "resolveArchiveDestFolderPath") .rejects(new BackupError("forced failure", ERRORS.UNKNOWN)); diff --git a/browser/components/backup/tests/browser/browser_settings_restore_from_backup.js b/browser/components/backup/tests/browser/browser_settings_restore_from_backup.js @@ -23,7 +23,7 @@ add_setup(async () => { // It's possible for other tests to change the internal state of the BackupService // which can lead to complications with the auto detection behaviour. Let's just reset // these states before testing - let bs = BackupService.get(); + let bs = getAndMaybeInitBackupService(); bs.resetLastBackupInternalState(); registerCleanupFunction(async () => { @@ -175,7 +175,7 @@ add_task(async function test_restore_from_backup() { add_task(async function test_restore_in_progress() { await BrowserTestUtils.withNewTab("about:preferences#sync", async browser => { let sandbox = sinon.createSandbox(); - let bs = BackupService.get(); + let bs = getAndMaybeInitBackupService(); let { promise: recoverPromise, resolve: recoverResolve } = Promise.withResolvers(); diff --git a/browser/components/backup/tests/browser/head.js b/browser/components/backup/tests/browser/head.js @@ -58,6 +58,19 @@ function createMockValidityPassEventPromise(parentEl, passwordInputsEl, event) { } /** + * Tries to get the BackupService, and if it is not inited, inits it + * + * @returns {BackupService} + */ +function getAndMaybeInitBackupService() { + try { + return BackupService.get(); + } catch { + return BackupService.init(); + } +} + +/** * Dispatches an input event for a password input field. * * @param {HTMLElement} inputEl diff --git a/browser/components/backup/tests/chrome/chrome.toml b/browser/components/backup/tests/chrome/chrome.toml @@ -1,6 +1,8 @@ [DEFAULT] prefs = [ "browser.backup.scheduled.enabled=false", + "browser.backup.archive.enabled=true", + "browser.backup.restore.enabled=true", ] support-files = ["head.js"] diff --git a/browser/components/backup/tests/marionette/test_backup.py b/browser/components/backup/tests/marionette/test_backup.py @@ -22,7 +22,12 @@ class BackupTest(MarionetteTestCase): # by default for Marionette. Also "browser.backup.log" has to be set # to true before Firefox starts in order for it to be displayed. self.marionette.enforce_gecko_prefs( - {"browser.backup.enabled": True, "browser.backup.log": True} + { + "browser.backup.enabled": True, + "browser.backup.log": True, + "browser.backup.archive.enabled": True, + "browser.backup.restore.enabled": True, + } ) self.marionette.set_context("chrome") diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -4,6 +4,8 @@ head = "head.js" firefox-appdir = "browser" prefs = [ "browser.backup.log=true", + "browser.backup.archive.enabled=true", + "browser.backup.restore.enabled=true", ] ["test_AddonsBackupResource.js"] diff --git a/browser/components/preferences/sync.js b/browser/components/preferences/sync.js @@ -19,7 +19,12 @@ const FXA_LOGIN_FAILED = 2; const SYNC_DISCONNECTED = 0; const SYNC_CONNECTED = 1; -const BACKUP_UI_ENABLED_PREF = "browser.backup.preferences.ui.enabled"; +const BACKUP_ARCHIVE_ENABLED_PREF_NAME = "browser.backup.archive.enabled"; +const BACKUP_RESTORE_ENABLED_PREF_NAME = "browser.backup.restore.enabled"; + +ChromeUtils.defineESModuleGetters(lazy, { + BackupService: "resource:///modules/backup/BackupService.sys.mjs", +}); var gSyncPane = { get page() { @@ -293,10 +298,9 @@ var gSyncPane = { }, updateBackupUIVisibility() { - const isBackupUIEnabled = Services.prefs.getBoolPref( - BACKUP_UI_ENABLED_PREF, - false - ); + let bs = lazy.BackupService.get(); + let isBackupUIEnabled = + bs.archiveEnabledStatus.enabled || bs.restoreEnabledStatus.enabled; let dataBackupSectionEl = document.getElementById("dataBackupSection"); @@ -308,23 +312,25 @@ var gSyncPane = { let dataBackupGroupEl = document.getElementById("dataBackupGroup"); let backupGroupHeaderEl = document.getElementById("backupCategory"); + dataBackupSectionEl.hidden = !isBackupUIEnabled; dataBackupGroupEl.hidden = !isBackupUIEnabled; backupGroupHeaderEl.hidden = !isBackupUIEnabled; }, _addPrefObservers() { - Services.prefs.addObserver( - BACKUP_UI_ENABLED_PREF, - this.updateBackupUIVisibility + Services.obs.addObserver( + this.updateBackupUIVisibility, + "backup-service-status-updated" ); window.addEventListener( "unload", - () => - Services.prefs.removeObserver( - BACKUP_UI_ENABLED_PREF, - this.updateBackupUIVisibility - ), + () => { + Services.obs.removeObserver( + this.updateBackupUIVisibility, + "backup-service-status-updated" + ); + }, { once: true } ); }, diff --git a/browser/components/preferences/tests/browser_bug731866.js b/browser/components/preferences/tests/browser_bug731866.js @@ -7,8 +7,9 @@ const browserContainersGroupDisabled = !SpecialPowers.getBoolPref( const cookieBannerHandlingDisabled = !SpecialPowers.getBoolPref( "cookiebanners.ui.desktop.enabled" ); -const backupSectionDisabled = !SpecialPowers.getBoolPref( - "browser.backup.preferences.ui.enabled" +const backupSectionDisabled = !( + SpecialPowers.getBoolPref("browser.backup.archive.enabled") || + SpecialPowers.getBoolPref("browser.backup.restore.enabled") ); const ipProtectionExperiment = SpecialPowers.getStringPref( "browser.ipProtection.variant" diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml @@ -4778,14 +4778,6 @@ backupService: description: >- When true, the profile backup service will be initialized soon after startup. - prefsUIEnabled: - type: boolean - setPref: - branch: default - pref: browser.backup.preferences.ui.enabled - description: >- - When true, the section in about:preferences to control the backup - feature is visible. sqlitePagesPerStep: description: >- The number of database pages to backup per step when backing up an