tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 519a0057957012f50903ec5edc5b3f964dafc864
parent 33e94c6ee9937ddcafadbf26e3d227510b189eba
Author: David P. <daparks@mozilla.com>
Date:   Wed,  3 Dec 2025 00:28:29 +0000

Bug 1996786: Filter downloads from places.sqlite backup r=places-reviewers,kpatenio,mak

It doesn't make sense to backup downloads since the common use case is to
restore the profile on a new OS install or another machine.

Differential Revision: https://phabricator.services.mozilla.com/D270774

Diffstat:
Mbrowser/components/backup/resources/PlacesBackupResource.sys.mjs | 7+++++++
Mbrowser/components/backup/tests/xpcshell/test_PlacesBackupResource.js | 8++++++++
Mtoolkit/components/places/PlacesDBUtils.sys.mjs | 38++++++++++++++++++++++++++++++++++++++
Atoolkit/components/places/tests/unit/test_places.sqlite | 0
Atoolkit/components/places/tests/unit/test_removeDownloadsMetadataFromDb.js | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/places/tests/unit/xpcshell.toml | 5+++++
6 files changed, 156 insertions(+), 0 deletions(-)

diff --git a/browser/components/backup/resources/PlacesBackupResource.sys.mjs b/browser/components/backup/resources/PlacesBackupResource.sys.mjs @@ -9,6 +9,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BookmarkJSONUtils: "resource://gre/modules/BookmarkJSONUtils.sys.mjs", + PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs", }); const BOOKMARKS_BACKUP_FILENAME = "bookmarks.jsonlz4"; @@ -69,6 +70,12 @@ export class PlacesBackupResource extends BackupResource { ]; await Promise.all(timedCopies); + // Now that both databases are copied, open the places db copy to remove + // downloaded files, since they won't be valid in the restored profile. + await lazy.PlacesDBUtils.removeDownloadsMetadataFromDb( + PathUtils.join(stagingPath, "places.sqlite") + ); + return null; } diff --git a/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js b/browser/components/backup/tests/xpcshell/test_PlacesBackupResource.js @@ -9,6 +9,9 @@ const { BookmarkJSONUtils } = ChromeUtils.importESModule( const { PlacesBackupResource } = ChromeUtils.importESModule( "resource:///modules/backup/PlacesBackupResource.sys.mjs" ); +const { PlacesDBUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesDBUtils.sys.mjs" +); registerCleanupFunction(() => { /** @@ -109,6 +112,7 @@ add_task(async function test_backup() { close: sandbox.stub().resolves(true), }; sandbox.stub(Sqlite, "openConnection").returns(fakeConnection); + sandbox.stub(PlacesDBUtils, "removeDownloadsMetadataFromDb"); let manifestEntry = await placesBackupResource.backup( stagingPath, @@ -121,6 +125,10 @@ add_task(async function test_backup() { ); Assert.ok( + PlacesDBUtils.removeDownloadsMetadataFromDb.calledOnce, + "PlacesDBUtils.removeDownloadsMetadataFromDb was called" + ); + Assert.ok( fakeConnection.backup.calledTwice, "Backup should have been called twice" ); diff --git a/toolkit/components/places/PlacesDBUtils.sys.mjs b/toolkit/components/places/PlacesDBUtils.sys.mjs @@ -1358,6 +1358,44 @@ export var PlacesDBUtils = { } return tasksMap; }, + + /** + * Helper used by FxBackup to remove downloads metadata from a copy of the Places + * database, for profile migration to another install (such as on another + * machine). + * + * @param {string} placesDbPath Full path to places.sqlite database to filter + * downloads metadata from. + */ + async removeDownloadsMetadataFromDb(placesDbPath) { + // Don't create the database if it doesn't exist. + if (!(await IOUtils.exists(placesDbPath))) { + return; + } + + let connection; + try { + connection = await lazy.Sqlite.openConnection({ + path: placesDbPath, + }); + const removeDownloads = ` + -- Find download annotations + WITH found_annos AS ( + SELECT a.id AS anno_id + FROM moz_annos a + JOIN moz_anno_attributes attr + ON a.anno_attribute_id = attr.id + WHERE INSTR(attr.name, 'downloads/') = 1 + ) + -- Delete downloads from moz_annos but leave the URLs in moz_places history + DELETE FROM moz_annos + WHERE id IN (SELECT anno_id FROM found_annos); + `; + await connection.execute(removeDownloads); + } finally { + await connection?.close(); + } + }, }; async function integrity(dbName) { diff --git a/toolkit/components/places/tests/unit/test_places.sqlite b/toolkit/components/places/tests/unit/test_places.sqlite Binary files differ. diff --git a/toolkit/components/places/tests/unit/test_removeDownloadsMetadataFromDb.js b/toolkit/components/places/tests/unit/test_removeDownloadsMetadataFromDb.js @@ -0,0 +1,98 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ + +ChromeUtils.defineESModuleGetters(this, { + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +const selectDownloads = ` + -- Find annotation + WITH found_annos AS ( + SELECT a.id AS anno_id, a.anno_attribute_id + FROM moz_annos a + JOIN moz_anno_attributes attr + ON a.anno_attribute_id = attr.id + WHERE attr.name IN ('downloads/destinationFileURI', 'downloads/metaData') + ) + -- Find downloads in moz_annos + SELECT * FROM moz_annos + WHERE id IN (SELECT anno_id FROM found_annos); +`; + +add_task(async function test_removeDownloadsMetadataFromDb() { + Services.prefs.setStringPref("toolkit.sqlitejsm.loglevel", "Debug"); + // Confirm that test_places.sqlite has the download information. + const testDbPath = PathUtils.join(do_get_cwd().path, "test_places.sqlite"); + let dbConnection; + try { + dbConnection = await Sqlite.openConnection({ + path: testDbPath, + readOnly: true, + }); + let rows = await dbConnection.execute(selectDownloads, null, null); + const expectedResultsList = [ + { id: 3, isFilename: true, filename: "20MB\(3\).zip" }, + { id: 4, isFilename: false }, + { id: 5, isFilename: true, filename: "50MB.zip" }, + { id: 6, isFilename: false }, + { id: 11, isFilename: true, filename: "5MB\(3\).zip" }, + { id: 12, isFilename: true, filename: "10MB\(2\).zip" }, + { id: 13, isFilename: false }, + { id: 14, isFilename: false }, + ]; + for (let i = 0; i < expectedResultsList.length; i++) { + const row = rows[i]; + const expectedResults = expectedResultsList[i]; + Assert.equal( + row.getResultByIndex(0), + expectedResults.id, + `id match #${i}` + ); + if (expectedResults.isFilename) { + Assert.ok( + row.getResultByIndex(3).endsWith(expectedResults.filename), + `filename match #${i}` + ); + } else { + Assert.equal( + JSON.parse(row.getResultByIndex(3)).state, + 1, + `metadata state match #${i}` + ); + } + } + } finally { + await dbConnection?.close(); + } + + // Make a copy of test_places.sqlite and remove the download entries from it. + const testDbCopyDir = await IOUtils.createUniqueDirectory( + PathUtils.tempDir, + "test_removeDownloadsMetadataFromDb" + ); + registerCleanupFunction(async () => { + await IOUtils.remove(testDbCopyDir, { recursive: true }); + }); + const testDbCopyPath = PathUtils.join( + testDbCopyDir, + "copy_test_places.sqlite" + ); + await IOUtils.copy(testDbPath, testDbCopyPath); + await PlacesDBUtils.removeDownloadsMetadataFromDb(testDbCopyPath); + + // Confirm that copy_test_places.sqlite does not have download information. + try { + dbConnection = await Sqlite.openConnection({ + path: testDbCopyPath, + readOnly: true, + }); + let rows = await dbConnection.execute(selectDownloads, null, null); + Assert.equal( + rows.length, + 0, + "moz_annos had no downloads or download metadata" + ); + } finally { + await dbConnection?.close(); + } +}); diff --git a/toolkit/components/places/tests/unit/xpcshell.toml b/toolkit/components/places/tests/unit/xpcshell.toml @@ -211,6 +211,11 @@ requesttimeoutfactor = 3 # Slow on Mac debug ["test_promiseBookmarksTree.js"] +["test_removeDownloadsMetadataFromDb.js"] +support-files = [ + "test_places.sqlite", +] + ["test_resolveNullBookmarkTitles.js"] ["test_result_sort.js"]