diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:50 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:34:50 +0000 |
commit | def92d1b8e9d373e2f6f27c366d578d97d8960c6 (patch) | |
tree | 2ef34b9ad8bb9a9220e05d60352558b15f513894 /browser/components/backup/resources | |
parent | Adding debian version 125.0.3-1. (diff) | |
download | firefox-def92d1b8e9d373e2f6f27c366d578d97d8960c6.tar.xz firefox-def92d1b8e9d373e2f6f27c366d578d97d8960c6.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/backup/resources')
9 files changed, 618 insertions, 11 deletions
diff --git a/browser/components/backup/resources/AddonsBackupResource.sys.mjs b/browser/components/backup/resources/AddonsBackupResource.sys.mjs new file mode 100644 index 0000000000..83b97ed2f2 --- /dev/null +++ b/browser/components/backup/resources/AddonsBackupResource.sys.mjs @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Backup for addons and extensions files and data. + */ +export class AddonsBackupResource extends BackupResource { + static get key() { + return "addons"; + } + + static get requiresEncryption() { + return false; + } + + async measure(profilePath = PathUtils.profileDir) { + // Report the total size of the extension json files. + const jsonFiles = [ + "extensions.json", + "extension-settings.json", + "extension-preferences.json", + "addonStartup.json.lz4", + ]; + let extensionsJsonSize = 0; + for (const filePath of jsonFiles) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + extensionsJsonSize += resourceSize; + } + } + Glean.browserBackup.extensionsJsonSize.set(extensionsJsonSize); + + // Report the size of permissions store data, if present. + let extensionStorePermissionsDataPath = PathUtils.join( + profilePath, + "extension-store-permissions", + "data.safe.bin" + ); + let extensionStorePermissionsDataSize = await BackupResource.getFileSize( + extensionStorePermissionsDataPath + ); + if (Number.isInteger(extensionStorePermissionsDataSize)) { + Glean.browserBackup.extensionStorePermissionsDataSize.set( + extensionStorePermissionsDataSize + ); + } + + // Report the size of extensions storage sync database. + let storageSyncPath = PathUtils.join(profilePath, "storage-sync-v2.sqlite"); + let storageSyncSize = await BackupResource.getFileSize(storageSyncPath); + Glean.browserBackup.storageSyncSize.set(storageSyncSize); + + // Report the total size of XPI files in the extensions directory. + let extensionsXpiDirectoryPath = PathUtils.join(profilePath, "extensions"); + let extensionsXpiDirectorySize = await BackupResource.getDirectorySize( + extensionsXpiDirectoryPath, + { + shouldExclude: (filePath, fileType) => + fileType !== "regular" || !filePath.endsWith(".xpi"), + } + ); + Glean.browserBackup.extensionsXpiDirectorySize.set( + extensionsXpiDirectorySize + ); + + // Report the total size of the browser extension data. + let browserExtensionDataPath = PathUtils.join( + profilePath, + "browser-extension-data" + ); + let browserExtensionDataSize = await BackupResource.getDirectorySize( + browserExtensionDataPath + ); + Glean.browserBackup.browserExtensionDataSize.set(browserExtensionDataSize); + + // Report the size of all moz-extension IndexedDB databases. + let defaultStoragePath = PathUtils.join(profilePath, "storage", "default"); + let extensionsStorageSize = await BackupResource.getDirectorySize( + defaultStoragePath, + { + shouldExclude: (filePath, _fileType, parentPath) => { + if ( + parentPath == defaultStoragePath && + !PathUtils.filename(filePath).startsWith("moz-extension") + ) { + return true; + } + return false; + }, + } + ); + if (Number.isInteger(extensionsStorageSize)) { + Glean.browserBackup.extensionsStorageSize.set(extensionsStorageSize); + } + } +} diff --git a/browser/components/backup/resources/BackupResource.sys.mjs b/browser/components/backup/resources/BackupResource.sys.mjs index bde3f0669c..d851eb5199 100644 --- a/browser/components/backup/resources/BackupResource.sys.mjs +++ b/browser/components/backup/resources/BackupResource.sys.mjs @@ -3,7 +3,19 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Convert from bytes to kilobytes (not kibibytes). -const BYTES_IN_KB = 1000; +export const BYTES_IN_KB = 1000; + +/** + * Convert bytes to the nearest 10th kilobyte to make the measurements fuzzier. + * + * @param {number} bytes - size in bytes. + * @returns {number} - size in kilobytes rounded to the nearest 10th kilobyte. + */ +export function bytesToFuzzyKilobytes(bytes) { + let sizeInKb = Math.ceil(bytes / BYTES_IN_KB); + let nearestTenthKb = Math.round(sizeInKb / 10) * 10; + return Math.max(nearestTenthKb, 1); +} /** * An abstract class representing a set of data within a user profile @@ -23,6 +35,21 @@ export class BackupResource { } /** + * This must be overridden to return a boolean indicating whether the + * resource requires encryption when being backed up. Encryption should be + * required for particularly sensitive data, such as passwords / credentials, + * cookies, or payment methods. If you're not sure, talk to someone from the + * Privacy team. + * + * @type {boolean} + */ + static get requiresEncryption() { + throw new Error( + "BackupResource::requiresEncryption needs to be overridden." + ); + } + + /** * Get the size of a file. * * @param {string} filePath - path to a file. @@ -40,21 +67,25 @@ export class BackupResource { return null; } - let sizeInKb = Math.ceil(size / BYTES_IN_KB); - // Make the measurement fuzzier by rounding to the nearest 10kb. - let nearestTenthKb = Math.round(sizeInKb / 10) * 10; + let nearestTenthKb = bytesToFuzzyKilobytes(size); - return Math.max(nearestTenthKb, 1); + return nearestTenthKb; } /** * Get the total size of a directory. * * @param {string} directoryPath - path to a directory. + * @param {object} options - A set of additional optional parameters. + * @param {Function} [options.shouldExclude] - an optional callback which based on file path and file type should return true + * if the file should be excluded from the computed directory size. * @returns {Promise<number|null>} - the size of all descendants of the directory in kilobytes, or null if the * directory does not exist, the path is not a directory or the size is unknown. */ - static async getDirectorySize(directoryPath) { + static async getDirectorySize( + directoryPath, + { shouldExclude = () => false } = {} + ) { if (!(await IOUtils.exists(directoryPath))) { return null; } @@ -75,15 +106,20 @@ export class BackupResource { childFilePath ); + if (shouldExclude(childFilePath, childType, directoryPath)) { + continue; + } + if (childSize >= 0) { - let sizeInKb = Math.ceil(childSize / BYTES_IN_KB); - // Make the measurement fuzzier by rounding to the nearest 10kb. - let nearestTenthKb = Math.round(sizeInKb / 10) * 10; - size += Math.max(nearestTenthKb, 1); + let nearestTenthKb = bytesToFuzzyKilobytes(childSize); + + size += nearestTenthKb; } if (childType == "directory") { - let childDirectorySize = await this.getDirectorySize(childFilePath); + let childDirectorySize = await this.getDirectorySize(childFilePath, { + shouldExclude, + }); if (Number.isInteger(childDirectorySize)) { size += childDirectorySize; } @@ -106,4 +142,29 @@ export class BackupResource { async measure(profilePath) { throw new Error("BackupResource::measure needs to be overridden."); } + + /** + * Perform a safe copy of the resource(s) and write them into the backup + * database. The Promise should resolve with an object that can be serialized + * to JSON, as it will be written to the manifest file. This same object will + * be deserialized and passed to restore() when restoring the backup. This + * object can be null if no additional information is needed to restore the + * backup. + * + * @param {string} stagingPath + * The path to the staging folder where copies of the datastores for this + * BackupResource should be written to. + * @param {string} [profilePath=null] + * This is null if the backup is being run on the currently running user + * profile. If, however, the backup is being run on a different user profile + * (for example, it's being run from a BackgroundTask on a user profile that + * just shut down, or during test), then this is a string set to that user + * profile path. + * + * @returns {Promise<object|null>} + */ + // eslint-disable-next-line no-unused-vars + async backup(stagingPath, profilePath = null) { + throw new Error("BackupResource::backup must be overridden"); + } } diff --git a/browser/components/backup/resources/CookiesBackupResource.sys.mjs b/browser/components/backup/resources/CookiesBackupResource.sys.mjs new file mode 100644 index 0000000000..8b988fd532 --- /dev/null +++ b/browser/components/backup/resources/CookiesBackupResource.sys.mjs @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Class representing Cookies database within a user profile. + */ +export class CookiesBackupResource extends BackupResource { + static get key() { + return "cookies"; + } + + static get requiresEncryption() { + return true; + } + + async measure(profilePath = PathUtils.profileDir) { + let cookiesDBPath = PathUtils.join(profilePath, "cookies.sqlite"); + let cookiesSize = await BackupResource.getFileSize(cookiesDBPath); + + Glean.browserBackup.cookiesSize.set(cookiesSize); + } +} diff --git a/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs new file mode 100644 index 0000000000..89069de826 --- /dev/null +++ b/browser/components/backup/resources/CredentialsAndSecurityBackupResource.sys.mjs @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Class representing files needed for logins, payment methods and form autofill within a user profile. + */ +export class CredentialsAndSecurityBackupResource extends BackupResource { + static get key() { + return "credentials_and_security"; + } + + static get requiresEncryption() { + return true; + } + + async measure(profilePath = PathUtils.profileDir) { + const securityFiles = ["cert9.db", "pkcs11.txt"]; + let securitySize = 0; + + for (let filePath of securityFiles) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + securitySize += resourceSize; + } + } + + Glean.browserBackup.securityDataSize.set(securitySize); + + const credentialsFiles = [ + "key4.db", + "logins.json", + "logins-backup.json", + "autofill-profiles.json", + "credentialstate.sqlite", + "signedInUser.json", + ]; + let credentialsSize = 0; + + for (let filePath of credentialsFiles) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + credentialsSize += resourceSize; + } + } + + Glean.browserBackup.credentialsDataSize.set(credentialsSize); + } +} diff --git a/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs new file mode 100644 index 0000000000..cb314eb34d --- /dev/null +++ b/browser/components/backup/resources/FormHistoryBackupResource.sys.mjs @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +/** + * Class representing Form history database within a user profile. + */ +export class FormHistoryBackupResource extends BackupResource { + static get key() { + return "formhistory"; + } + + static get requiresEncryption() { + return false; + } + + async measure(profilePath = PathUtils.profileDir) { + let formHistoryDBPath = PathUtils.join(profilePath, "formhistory.sqlite"); + let formHistorySize = await BackupResource.getFileSize(formHistoryDBPath); + + Glean.browserBackup.formHistorySize.set(formHistorySize); + } +} diff --git a/browser/components/backup/resources/MiscDataBackupResource.sys.mjs b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs new file mode 100644 index 0000000000..97224f0e31 --- /dev/null +++ b/browser/components/backup/resources/MiscDataBackupResource.sys.mjs @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", +}); + +/** + * Class representing miscellaneous files for telemetry, site storage, + * media device origin mapping, chrome privileged IndexedDB databases, + * and Mozilla Accounts within a user profile. + */ +export class MiscDataBackupResource extends BackupResource { + static get key() { + return "miscellaneous"; + } + + static get requiresEncryption() { + return false; + } + + async backup(stagingPath, profilePath = PathUtils.profileDir) { + const files = [ + "times.json", + "enumerate_devices.txt", + "SiteSecurityServiceState.bin", + ]; + + for (let fileName of files) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + if (await IOUtils.exists(sourcePath)) { + await IOUtils.copy(sourcePath, destPath, { recursive: true }); + } + } + + const sqliteDatabases = ["protections.sqlite"]; + + 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(); + } + } + + // Bug 1890585 - we don't currently have the ability to copy the + // chrome-privileged IndexedDB databases under storage/permanent/chrome, so + // we'll just skip that for now. + + return null; + } + + async measure(profilePath = PathUtils.profileDir) { + const files = [ + "times.json", + "enumerate_devices.txt", + "protections.sqlite", + "SiteSecurityServiceState.bin", + ]; + + let fullSize = 0; + + for (let filePath of files) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + fullSize += resourceSize; + } + } + + let chromeIndexedDBDirPath = PathUtils.join( + profilePath, + "storage", + "permanent", + "chrome" + ); + let chromeIndexedDBDirSize = await BackupResource.getDirectorySize( + chromeIndexedDBDirPath + ); + if (Number.isInteger(chromeIndexedDBDirSize)) { + fullSize += chromeIndexedDBDirSize; + } + + Glean.browserBackup.miscDataSize.set(fullSize); + } +} diff --git a/browser/components/backup/resources/PlacesBackupResource.sys.mjs b/browser/components/backup/resources/PlacesBackupResource.sys.mjs new file mode 100644 index 0000000000..1955406f51 --- /dev/null +++ b/browser/components/backup/resources/PlacesBackupResource.sys.mjs @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * 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. + */ +export class PlacesBackupResource extends BackupResource { + static get key() { + return "places"; + } + + static get requiresEncryption() { + 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"); + let placesDBSize = await BackupResource.getFileSize(placesDBPath); + let faviconsDBSize = await BackupResource.getFileSize(faviconsDBPath); + + Glean.browserBackup.placesSize.set(placesDBSize); + Glean.browserBackup.faviconsSize.set(faviconsDBSize); + } +} diff --git a/browser/components/backup/resources/PreferencesBackupResource.sys.mjs b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs new file mode 100644 index 0000000000..012c0bf91e --- /dev/null +++ b/browser/components/backup/resources/PreferencesBackupResource.sys.mjs @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs"; +import { Sqlite } from "resource://gre/modules/Sqlite.sys.mjs"; + +/** + * Class representing files that modify preferences and permissions within a user profile. + */ +export class PreferencesBackupResource extends BackupResource { + static get key() { + return "preferences"; + } + + static get requiresEncryption() { + return false; + } + + async backup(stagingPath, profilePath = PathUtils.profileDir) { + // These are files that can be simply copied into the staging folder using + // IOUtils.copy. + const simpleCopyFiles = [ + "xulstore.json", + "containers.json", + "handlers.json", + "search.json.mozlz4", + "user.js", + "chrome", + ]; + + for (let fileName of simpleCopyFiles) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + if (await IOUtils.exists(sourcePath)) { + await IOUtils.copy(sourcePath, destPath, { recursive: true }); + } + } + + const sqliteDatabases = ["permissions.sqlite", "content-prefs.sqlite"]; + + for (let fileName of sqliteDatabases) { + let sourcePath = PathUtils.join(profilePath, fileName); + let destPath = PathUtils.join(stagingPath, fileName); + let connection; + + try { + connection = await Sqlite.openConnection({ + path: sourcePath, + }); + + await connection.backup(destPath); + } finally { + await connection.close(); + } + } + + // prefs.js is a special case - we have a helper function to flush the + // current prefs state to disk off of the main thread. + let prefsDestPath = PathUtils.join(stagingPath, "prefs.js"); + let prefsDestFile = await IOUtils.getFile(prefsDestPath); + await Services.prefs.backupPrefFile(prefsDestFile); + + return null; + } + + async measure(profilePath = PathUtils.profileDir) { + const files = [ + "prefs.js", + "xulstore.json", + "permissions.sqlite", + "content-prefs.sqlite", + "containers.json", + "handlers.json", + "search.json.mozlz4", + "user.js", + ]; + let fullSize = 0; + + for (let filePath of files) { + let resourcePath = PathUtils.join(profilePath, filePath); + let resourceSize = await BackupResource.getFileSize(resourcePath); + if (Number.isInteger(resourceSize)) { + fullSize += resourceSize; + } + } + + const chromeDirectoryPath = PathUtils.join(profilePath, "chrome"); + let chromeDirectorySize = await BackupResource.getDirectorySize( + chromeDirectoryPath + ); + if (Number.isInteger(chromeDirectorySize)) { + fullSize += chromeDirectorySize; + } + + Glean.browserBackup.preferencesSize.set(fullSize); + } +} diff --git a/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs new file mode 100644 index 0000000000..fa5dcca848 --- /dev/null +++ b/browser/components/backup/resources/SessionStoreBackupResource.sys.mjs @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { + BackupResource, + bytesToFuzzyKilobytes, +} from "resource:///modules/backup/BackupResource.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", +}); + +/** + * Class representing Session store related files within a user profile. + */ +export class SessionStoreBackupResource extends BackupResource { + static get key() { + return "sessionstore"; + } + + static get requiresEncryption() { + // Session store data does not require encryption, but if encryption is + // disabled, then session cookies will be cleared from the backup before + // writing it to the disk. + return false; + } + + async measure(profilePath = PathUtils.profileDir) { + // Get the current state of the session store JSON and + // measure it's uncompressed size. + let sessionStoreJson = lazy.SessionStore.getCurrentState(true); + let sessionStoreSize = new TextEncoder().encode( + JSON.stringify(sessionStoreJson) + ).byteLength; + let sessionStoreNearestTenthKb = bytesToFuzzyKilobytes(sessionStoreSize); + + Glean.browserBackup.sessionStoreSize.set(sessionStoreNearestTenthKb); + + let sessionStoreBackupsDirectoryPath = PathUtils.join( + profilePath, + "sessionstore-backups" + ); + let sessionStoreBackupsDirectorySize = + await BackupResource.getDirectorySize(sessionStoreBackupsDirectoryPath); + + Glean.browserBackup.sessionStoreBackupsDirectorySize.set( + sessionStoreBackupsDirectorySize + ); + } +} |