/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { BackupResource, bytesToFuzzyKilobytes, } from "resource:///modules/backup/BackupResource.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { return console.createInstance({ prefix: "SessionStoreBackupResource", maxLogLevel: Services.prefs.getBoolPref("browser.backup.log", false) ? "Debug" : "Warn", }); }); XPCOMUtils.defineLazyPreferenceGetter( lazy, "TAB_FLUSH_TIMEOUT", "browser.backup.tab-flush-timeout", 5000 ); /** * Class representing Session store related files within a user profile. */ export class SessionStoreBackupResource extends BackupResource { // Allow creator to provide a "SessionStore" object, so we can use mocks in // testing. Passing `null` means use the real service. constructor(sessionStore = null) { super(); this._sessionStore = sessionStore; } 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; } get #sessionStore() { return this._sessionStore || lazy.SessionStore; } get filteredSessionStoreState() { let sessionStoreState = this.#sessionStore.getCurrentState(true); // Preserving session cookies in a backup used on a different machine // may break behavior for websites. So we leave them out of the backup. sessionStoreState.cookies = []; // Remove session storage. if (sessionStoreState.windows) { // We don't want to backup private windows sessionStoreState.windows = sessionStoreState.windows.filter( w => !w?.isPrivate ); sessionStoreState.windows.forEach(win => { if (win.tabs) { win.tabs.forEach(tab => delete tab.storage); } if (win._closedTabs) { win._closedTabs.forEach(closedTab => delete closedTab.state.storage); } }); } if (sessionStoreState.savedGroups) { sessionStoreState.savedGroups.forEach(group => { if (group.tabs) { group.tabs.forEach(tab => delete tab.state.storage); } }); } return sessionStoreState; } async backup( stagingPath, profilePath = PathUtils.profileDir, _isEncrypting = false ) { // Flush tab state so backups receive the correct url to restore. await Promise.race([ Promise.allSettled( lazy.BrowserWindowTracker.orderedWindows.map( lazy.TabStateFlusher.flushWindow ) ), new Promise((_, reject) => lazy.setTimeout(reject, lazy.TAB_FLUSH_TIMEOUT, { timeout: true }) ), ]).catch(e => { if (e?.timeout) { lazy.logConsole.warn("Timed out waiting while flushing tab state."); } else { lazy.logConsole.error( "Unrecognized error while flushing tab state.", e ); } }); let sessionStorePath = PathUtils.join(stagingPath, "sessionstore.jsonlz4"); await IOUtils.writeJSON(sessionStorePath, this.filteredSessionStoreState, { compress: true, }); await BackupResource.copyFiles(profilePath, stagingPath, [ "sessionstore-backups", ]); return null; } async recover(_manifestEntry, recoveryPath, destProfilePath) { await BackupResource.copyFiles(recoveryPath, destProfilePath, [ "sessionstore.jsonlz4", "sessionstore-backups", ]); return null; } async measure(profilePath = PathUtils.profileDir) { // Get the current state of the session store JSON and // measure it's uncompressed size. let sessionStoreJson = this.#sessionStore.getCurrentState(true); let sessionStoreSize = new TextEncoder().encode( JSON.stringify(sessionStoreJson) ).byteLength; let sessionStoreNearestTenKb = bytesToFuzzyKilobytes(sessionStoreSize); Glean.browserBackup.sessionStoreSize.set(sessionStoreNearestTenKb); let sessionStoreBackupsDirectoryPath = PathUtils.join( profilePath, "sessionstore-backups" ); let sessionStoreBackupsDirectorySize = await BackupResource.getDirectorySize(sessionStoreBackupsDirectoryPath); Glean.browserBackup.sessionStoreBackupsDirectorySize.set( sessionStoreBackupsDirectorySize ); } }