From ccd602ba69356c1f3837a2a41161a858d4d5c9d5 Mon Sep 17 00:00:00 2001 From: kpatenio Date: Wed, 10 Apr 2024 21:21:17 +0000 Subject: [PATCH] Bug 1885609 - implement backup method for PlacesBackupResource. r=backup-reviewers,places-reviewers,mak,mconley Implements `PlacesBackupResource.backup` to store a copy of `places.sqlite` and `favicons.sqlite` in the staging folder. If users don't want history remembered or use permanent private browsing mode, we will backup bookmarks instead to a file called `bookmarks.json`. Automatic backup is not yet implemented, so to test changes locally, go to `chrome://browser/content/backup/debug.html` to view where we store the staging folder and to manually run the backup methods for all available backup resources. Backup files for `PlacesBackupResource` should be under the `places` subfolder in the staging folder. Differential Revision: https://phabricator.services.mozilla.com/D206532 --- .../resources/PlacesBackupResource.sys.mjs | 63 +++++ .../xpcshell/test_PlacesBackupResource.js | 226 ++++++++++++++++++ .../tests/xpcshell/test_measurements.js | 56 ----- .../backup/tests/xpcshell/xpcshell.toml | 2 + 4 files changed, 291 insertions(+), 56 deletions(-) create mode 100644 browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js diff --git a/browser/components/backup/resources/PlacesBackupResource.sys.mjs b/browser/components/backup/resources/PlacesBackupResource.sys.mjs index e3335937931f..1955406f5179 100644 --- a/browser/components/backup/resources/PlacesBackupResource.sys.mjs +++ b/browser/components/backup/resources/PlacesBackupResource.sys.mjs @@ -3,6 +3,28 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "isBrowsingHistoryEnabled", + "places.history.enabled", + true +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "isSanitizeOnShutdownEnabled", + "privacy.sanitize.sanitizeOnShutdown", + false +); /** * Class representing Places database related files within a user profile. @@ -16,6 +38,47 @@ export class PlacesBackupResource extends BackupResource { return false; } + async backup(stagingPath, profilePath = PathUtils.profileDir) { + const sqliteDatabases = ["places.sqlite", "favicons.sqlite"]; + let canBackupHistory = + !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing && + !lazy.isSanitizeOnShutdownEnabled && + lazy.isBrowsingHistoryEnabled; + + /** + * Do not backup places.sqlite and favicons.sqlite if users have history disabled, want history cleared on shutdown or are using permanent private browsing mode. + * Instead, export all existing bookmarks to a compressed JSON file that we can read when restoring the backup. + */ + if (!canBackupHistory) { + let bookmarksBackupFile = PathUtils.join( + stagingPath, + "bookmarks.jsonlz4" + ); + await lazy.BookmarkJSONUtils.exportToFile(bookmarksBackupFile, { + compress: true, + }); + return { bookmarksOnly: true }; + } + + for (let fileName of sqliteDatabases) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + let connection; + + try { + connection = await lazy.Sqlite.openConnection({ + path: sourcePath, + readOnly: true, + }); + + await connection.backup(destPath); + } finally { + await connection.close(); + } + } + return null; + } + async measure(profilePath = PathUtils.profileDir) { let placesDBPath = PathUtils.join(profilePath, "places.sqlite"); let faviconsDBPath = PathUtils.join(profilePath, "favicons.sqlite"); diff --git a/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js new file mode 100644 index 000000000000..de97281372a2 --- /dev/null +++ b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. +https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { PlacesBackupResource } = ChromeUtils.importESModule( + "resource:///modules/backup/PlacesBackupResource.sys.mjs" +); +const { PrivateBrowsingUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PrivateBrowsingUtils.sys.mjs" +); + +const HISTORY_ENABLED_PREF = "places.history.enabled"; +const SANITIZE_ON_SHUTDOWN_PREF = "privacy.sanitize.sanitizeOnShutdown"; + +registerCleanupFunction(() => { + /** + * Even though test_backup_no_saved_history clears user prefs too, + * clear them here as well in case that test fails and we don't + * reach the end of the test, which handles the cleanup. + */ + Services.prefs.clearUserPref(HISTORY_ENABLED_PREF); + Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF); +}); + +/** + * Tests that we can measure Places DB related files in the profile directory. + */ +add_task(async function test_measure() { + Services.fog.testResetFOG(); + + const EXPECTED_PLACES_DB_SIZE = 5240; + const EXPECTED_FAVICONS_DB_SIZE = 5240; + + // Create resource files in temporary directory + const tempDir = PathUtils.tempDir; + let tempPlacesDBPath = PathUtils.join(tempDir, "places.sqlite"); + let tempFaviconsDBPath = PathUtils.join(tempDir, "favicons.sqlite"); + await createKilobyteSizedFile(tempPlacesDBPath, EXPECTED_PLACES_DB_SIZE); + await createKilobyteSizedFile(tempFaviconsDBPath, EXPECTED_FAVICONS_DB_SIZE); + + let placesBackupResource = new PlacesBackupResource(); + await placesBackupResource.measure(tempDir); + + let placesMeasurement = Glean.browserBackup.placesSize.testGetValue(); + let faviconsMeasurement = Glean.browserBackup.faviconsSize.testGetValue(); + let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); + + // Compare glean vs telemetry measurements + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.places_size", + placesMeasurement, + "Glean and telemetry measurements for places.sqlite should be equal" + ); + TelemetryTestUtils.assertScalar( + scalars, + "browser.backup.favicons_size", + faviconsMeasurement, + "Glean and telemetry measurements for favicons.sqlite should be equal" + ); + + // Compare glean measurements vs actual file sizes + Assert.equal( + placesMeasurement, + EXPECTED_PLACES_DB_SIZE, + "Should have collected the correct glean measurement for places.sqlite" + ); + Assert.equal( + faviconsMeasurement, + EXPECTED_FAVICONS_DB_SIZE, + "Should have collected the correct glean measurement for favicons.sqlite" + ); + + await maybeRemovePath(tempPlacesDBPath); + await maybeRemovePath(tempFaviconsDBPath); +}); + +/** + * Tests that the backup method correctly copies places.sqlite and + * favicons.sqlite from the profile directory into the staging directory. + */ +add_task(async function test_backup() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.calledTwice, + "Backup should have been called twice" + ); + Assert.ok( + fakeConnection.backup.firstCall.calledWith( + PathUtils.join(stagingPath, "places.sqlite") + ), + "places.sqlite should have been backed up first" + ); + Assert.ok( + fakeConnection.backup.secondCall.calledWith( + PathUtils.join(stagingPath, "favicons.sqlite") + ), + "favicons.sqlite should have been backed up second" + ); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); + +/** + * Tests that the backup method correctly creates a compressed bookmarks JSON file when users + * don't want history saved, even on shutdown. + */ +add_task(async function test_backup_no_saved_history() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + + /** + * First verify that remember history pref alone affects backup file type for places, + * despite sanitize on shutdown pref value. + */ + Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, false); + Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, false); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with remember history disabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + await IOUtils.remove(PathUtils.join(stagingPath, "bookmarks.jsonlz4")); + + /** + * Now verify that the sanitize shutdown pref alone affects backup file type for places, + * even if the user is okay with remembering history while browsing. + */ + Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, true); + Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, true); + + fakeConnection.backup.resetHistory(); + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with sanitize shutdown enabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); + Services.prefs.clearUserPref(HISTORY_ENABLED_PREF); + Services.prefs.clearUserPref(SANITIZE_ON_SHUTDOWN_PREF); +}); + +/** + * Tests that the backup method correctly creates a compressed bookmarks JSON file when + * permanent private browsing mode is enabled. + */ +add_task(async function test_backup_private_browsing() { + let sandbox = sinon.createSandbox(); + + let placesBackupResource = new PlacesBackupResource(); + let sourcePath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-source-test" + ); + let stagingPath = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "PlacesBackupResource-staging-test" + ); + + let fakeConnection = { + backup: sandbox.stub().resolves(true), + close: sandbox.stub().resolves(true), + }; + sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + sandbox.stub(PrivateBrowsingUtils, "permanentPrivateBrowsing").value(true); + + await placesBackupResource.backup(stagingPath, sourcePath); + + Assert.ok( + fakeConnection.backup.notCalled, + "No sqlite connections should have been made with permanent private browsing enabled" + ); + await assertFilesExist(stagingPath, [{ path: "bookmarks.jsonlz4" }]); + + await maybeRemovePath(stagingPath); + await maybeRemovePath(sourcePath); + + sandbox.restore(); +}); diff --git a/browser/components/backup/tests/xpcshell/test_measurements.js b/browser/components/backup/tests/xpcshell/test_measurements.js index 6714d7825124..0dece6b37079 100644 --- a/browser/components/backup/tests/xpcshell/test_measurements.js +++ b/browser/components/backup/tests/xpcshell/test_measurements.js @@ -6,9 +6,6 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const { CredentialsAndSecurityBackupResource } = ChromeUtils.importESModule( "resource:///modules/backup/CredentialsAndSecurityBackupResource.sys.mjs" ); -const { PlacesBackupResource } = ChromeUtils.importESModule( - "resource:///modules/backup/PlacesBackupResource.sys.mjs" -); const { AddonsBackupResource } = ChromeUtils.importESModule( "resource:///modules/backup/AddonsBackupResource.sys.mjs" ); @@ -79,59 +76,6 @@ add_task(async function test_profDDiskSpace() { ); }); -/** - * Tests that we can measure Places DB related files in the profile directory. - */ -add_task(async function test_placesBackupResource() { - Services.fog.testResetFOG(); - - const EXPECTED_PLACES_DB_SIZE = 5240; - const EXPECTED_FAVICONS_DB_SIZE = 5240; - - // Create resource files in temporary directory - const tempDir = PathUtils.tempDir; - let tempPlacesDBPath = PathUtils.join(tempDir, "places.sqlite"); - let tempFaviconsDBPath = PathUtils.join(tempDir, "favicons.sqlite"); - await createKilobyteSizedFile(tempPlacesDBPath, EXPECTED_PLACES_DB_SIZE); - await createKilobyteSizedFile(tempFaviconsDBPath, EXPECTED_FAVICONS_DB_SIZE); - - let placesBackupResource = new PlacesBackupResource(); - await placesBackupResource.measure(tempDir); - - let placesMeasurement = Glean.browserBackup.placesSize.testGetValue(); - let faviconsMeasurement = Glean.browserBackup.faviconsSize.testGetValue(); - let scalars = TelemetryTestUtils.getProcessScalars("parent", false, false); - - // Compare glean vs telemetry measurements - TelemetryTestUtils.assertScalar( - scalars, - "browser.backup.places_size", - placesMeasurement, - "Glean and telemetry measurements for places.sqlite should be equal" - ); - TelemetryTestUtils.assertScalar( - scalars, - "browser.backup.favicons_size", - faviconsMeasurement, - "Glean and telemetry measurements for favicons.sqlite should be equal" - ); - - // Compare glean measurements vs actual file sizes - Assert.equal( - placesMeasurement, - EXPECTED_PLACES_DB_SIZE, - "Should have collected the correct glean measurement for places.sqlite" - ); - Assert.equal( - faviconsMeasurement, - EXPECTED_FAVICONS_DB_SIZE, - "Should have collected the correct glean measurement for favicons.sqlite" - ); - - await IOUtils.remove(tempPlacesDBPath); - await IOUtils.remove(tempFaviconsDBPath); -}); - /** * Tests that we can measure credentials related files in the profile directory. */ diff --git a/browser/components/backup/tests/xpcshell/xpcshell.toml b/browser/components/backup/tests/xpcshell/xpcshell.toml index fc0da5e2dd88..07e517f1f205 100644 --- a/browser/components/backup/tests/xpcshell/xpcshell.toml +++ b/browser/components/backup/tests/xpcshell/xpcshell.toml @@ -11,6 +11,8 @@ support-files = ["data/test_xulstore.json"] ["test_MiscDataBackupResource.js"] +["test_PlacesBackupResource.js"] + ["test_PreferencesBackupResource.js"] ["test_createBackup.js"]