tor-browser

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

BackupResource.sys.mjs (13855B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  Sqlite: "resource://gre/modules/Sqlite.sys.mjs",
     11  BackupError: "resource:///modules/backup/BackupError.mjs",
     12  ERRORS: "chrome://browser/content/backup/backup-constants.mjs",
     13  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     14 });
     15 
     16 XPCOMUtils.defineLazyPreferenceGetter(
     17  lazy,
     18  "isBrowsingHistoryEnabled",
     19  "places.history.enabled",
     20  true
     21 );
     22 
     23 XPCOMUtils.defineLazyPreferenceGetter(
     24  lazy,
     25  "isSanitizeOnShutdownEnabled",
     26  "privacy.sanitize.sanitizeOnShutdown",
     27  false
     28 );
     29 
     30 XPCOMUtils.defineLazyPreferenceGetter(
     31  lazy,
     32  "isHistoryClearedOnShutdown2",
     33  "privacy.clearOnShutdown_v2.browsingHistoryAndDownloads",
     34  false
     35 );
     36 
     37 XPCOMUtils.defineLazyPreferenceGetter(
     38  lazy,
     39  "useOldClearHistoryDialog",
     40  "privacy.sanitize.useOldClearHistoryDialog",
     41  false
     42 );
     43 
     44 XPCOMUtils.defineLazyPreferenceGetter(
     45  lazy,
     46  "isHistoryClearedOnShutdown",
     47  "privacy.clearOnShutdown.history",
     48  false
     49 );
     50 
     51 // Convert from bytes to kilobytes (not kibibytes).
     52 export const BYTES_IN_KB = 1000;
     53 
     54 /**
     55 * Convert bytes to the nearest multiple of 10 kilobytes to make the measurements fuzzier.
     56 * Returns 1 if size is < 5 kB.
     57 *
     58 * @param {number} bytes - size in bytes.
     59 * @returns {number} - size in kilobytes, rounded to the nearest multiple of 10
     60 */
     61 export function bytesToFuzzyKilobytes(bytes) {
     62  let sizeInKb = Math.ceil(bytes / BYTES_IN_KB);
     63  let nearestTenKb = Math.round(sizeInKb / 10) * 10;
     64  return Math.max(nearestTenKb, 1);
     65 }
     66 
     67 /**
     68 * An abstract class representing a set of data within a user profile
     69 * that can be persisted to a separate backup archive file, and restored
     70 * to a new user profile from that backup archive file.
     71 */
     72 export class BackupResource {
     73  /**
     74   * This must be overridden to return a simple string identifier for the
     75   * resource, for example "places" or "extensions". This key is used as
     76   * a unique identifier for the resource.
     77   *
     78   * @type {string}
     79   */
     80  static get key() {
     81    throw new lazy.BackupError(
     82      "BackupResource::key needs to be overridden.",
     83      lazy.ERRORS.INTERNAL_ERROR
     84    );
     85  }
     86 
     87  /**
     88   * This must be overridden to return a boolean indicating whether the
     89   * resource requires encryption when being backed up. Encryption should be
     90   * required for particularly sensitive data, such as passwords / credentials,
     91   * cookies, or payment methods. If you're not sure, talk to someone from the
     92   * Privacy team.
     93   *
     94   * @type {boolean}
     95   */
     96  static get requiresEncryption() {
     97    throw new lazy.BackupError(
     98      "BackupResource::requiresEncryption needs to be overridden.",
     99      lazy.ERRORS.INTERNAL_ERROR
    100    );
    101  }
    102 
    103  /**
    104   * This can be overridden to return a number indicating the priority the
    105   * resource should have in the backup order.
    106   *
    107   * Resources with a higher priority will be backed up first.
    108   * The default priority of 0 indicates it can be processed in any order.
    109   *
    110   * @returns {number}
    111   */
    112  static get priority() {
    113    return 0;
    114  }
    115 
    116  /**
    117   * Get the size of a file.
    118   *
    119   * @param {string} filePath - path to a file.
    120   * @returns {Promise<number|null>} - the size of the file in kilobytes, or null if the
    121   * file does not exist, the path is a directory or the size is unknown.
    122   */
    123  static async getFileSize(filePath) {
    124    if (!(await IOUtils.exists(filePath))) {
    125      return null;
    126    }
    127 
    128    let { size } = await IOUtils.stat(filePath);
    129 
    130    if (size < 0) {
    131      return null;
    132    }
    133 
    134    let nearestTenKb = bytesToFuzzyKilobytes(size);
    135 
    136    return nearestTenKb;
    137  }
    138 
    139  /**
    140   * Get the total size of a directory.
    141   *
    142   * @param {string} directoryPath - path to a directory.
    143   * @param {object}   options - A set of additional optional parameters.
    144   * @param {Function} [options.shouldExclude] - an optional callback which based on file path and file type should return true
    145   * if the file should be excluded from the computed directory size.
    146   * @returns {Promise<number|null>} - the size of all descendants of the directory in kilobytes, or null if the
    147   * directory does not exist, the path is not a directory or the size is unknown.
    148   */
    149  static async getDirectorySize(
    150    directoryPath,
    151    { shouldExclude = () => false } = {}
    152  ) {
    153    if (!(await IOUtils.exists(directoryPath))) {
    154      return null;
    155    }
    156 
    157    let { type } = await IOUtils.stat(directoryPath);
    158 
    159    if (type != "directory") {
    160      return null;
    161    }
    162 
    163    let children = await IOUtils.getChildren(directoryPath, {
    164      ignoreAbsent: true,
    165    });
    166 
    167    let size = 0;
    168    for (const childFilePath of children) {
    169      let { size: childSize, type: childType } =
    170        await IOUtils.stat(childFilePath);
    171 
    172      if (shouldExclude(childFilePath, childType, directoryPath)) {
    173        continue;
    174      }
    175 
    176      if (childSize >= 0) {
    177        let nearestTenKb = bytesToFuzzyKilobytes(childSize);
    178 
    179        size += nearestTenKb;
    180      }
    181 
    182      if (childType == "directory") {
    183        let childDirectorySize = await this.getDirectorySize(childFilePath, {
    184          shouldExclude,
    185        });
    186        if (Number.isInteger(childDirectorySize)) {
    187          size += childDirectorySize;
    188        }
    189      }
    190    }
    191 
    192    return size;
    193  }
    194 
    195  /**
    196   * Copy a set of SQLite databases safely from a source directory to a
    197   * destination directory. A new read-only connection is opened for each
    198   * database, and then a backup is created. If the source database does not
    199   * exist, it is ignored.
    200   *
    201   * @param {string} sourcePath
    202   *   Path to the source directory of the SQLite databases.
    203   * @param {string} destPath
    204   *   Path to the destination directory where the SQLite databases should be
    205   *   copied to.
    206   * @param {Array<string>} sqliteDatabases
    207   *   An array of filenames of the SQLite databases to copy.
    208   * @returns {Promise<undefined>}
    209   */
    210  static async copySqliteDatabases(sourcePath, destPath, sqliteDatabases) {
    211    for (let fileName of sqliteDatabases) {
    212      let sourceFilePath = PathUtils.join(sourcePath, fileName);
    213 
    214      if (!(await IOUtils.exists(sourceFilePath))) {
    215        continue;
    216      }
    217 
    218      let destFilePath = PathUtils.join(destPath, fileName);
    219      let connection;
    220 
    221      try {
    222        connection = await lazy.Sqlite.openConnection({
    223          path: sourceFilePath,
    224          readOnly: true,
    225        });
    226 
    227        await connection.backup(
    228          destFilePath,
    229          BackupResource.SQLITE_PAGES_PER_STEP,
    230          BackupResource.SQLITE_STEP_DELAY_MS
    231        );
    232      } finally {
    233        await connection?.close();
    234      }
    235    }
    236  }
    237 
    238  /**
    239   * A helper function to copy a set of files from a source directory to a
    240   * destination directory. Callers should ensure that the source files can be
    241   * copied safely before invoking this function. Files that do not exist will
    242   * be ignored. Callers that wish to copy SQLite databases should use
    243   * copySqliteDatabases() instead.
    244   *
    245   * @param {string} sourcePath
    246   *   Path to the source directory of the files to be copied.
    247   * @param {string} destPath
    248   *   Path to the destination directory where the files should be
    249   *   copied to.
    250   * @param {string[]} fileNames
    251   *   An array of filenames of the files to copy.
    252   * @returns {Promise<undefined>}
    253   */
    254  static async copyFiles(sourcePath, destPath, fileNames) {
    255    for (let fileName of fileNames) {
    256      let sourceFilePath = PathUtils.join(sourcePath, fileName);
    257      let destFilePath = PathUtils.join(destPath, fileName);
    258      if (await IOUtils.exists(sourceFilePath)) {
    259        await IOUtils.copy(sourceFilePath, destFilePath, { recursive: true });
    260      }
    261    }
    262  }
    263 
    264  /**
    265   * Returns true if the browser is configured in such a way that backing up
    266   * things related to browsing history is allowed. Otherwise, returns false.
    267   *
    268   * @returns {boolean}
    269   */
    270 
    271  /**
    272   * Returns true if the resource is enabled for backup based on different
    273   * browser preferences and configurations. Otherwise, returns false.
    274   *
    275   * @returns {boolean}
    276   */
    277  static get canBackupResource() {
    278    // This is meant to be overridden if a resource requires checks; default is true.
    279    return true;
    280  }
    281 
    282  /**
    283   * Helper function to see if we are going to be backing up and restoring places.sqlite
    284   *
    285   * @returns {boolean}
    286   */
    287  static get backingUpPlaces() {
    288    if (
    289      lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
    290      !lazy.isBrowsingHistoryEnabled
    291    ) {
    292      return false;
    293    }
    294 
    295    if (!lazy.isSanitizeOnShutdownEnabled) {
    296      return true;
    297    }
    298 
    299    if (!lazy.useOldClearHistoryDialog) {
    300      if (lazy.isHistoryClearedOnShutdown2) {
    301        return false;
    302      }
    303    } else if (lazy.isHistoryClearedOnShutdown) {
    304      return false;
    305    }
    306 
    307    return true;
    308  }
    309 
    310  constructor() {}
    311 
    312  /**
    313   * This must be overridden to record telemetry on the size of any
    314   * data associated with this BackupResource.
    315   *
    316   * @param {string} profilePath - path to a profile directory.
    317   * @returns {Promise<undefined>}
    318   */
    319  // eslint-disable-next-line no-unused-vars
    320  async measure(profilePath) {
    321    throw new lazy.BackupError(
    322      "BackupResource::measure needs to be overridden.",
    323      lazy.ERRORS.INTERNAL_ERROR
    324    );
    325  }
    326 
    327  /**
    328   * Perform a safe copy of the datastores that this resource manages and write
    329   * them into the backup database. The Promise should resolve with an object
    330   * that can be serialized to JSON, as it will be written to the manifest file.
    331   * This same object will be deserialized and passed to restore() when
    332   * restoring the backup. This object can be null if no additional information
    333   * is needed to restore the backup.
    334   *
    335   * @param {string} stagingPath
    336   *   The path to the staging folder where copies of the datastores for this
    337   *   BackupResource should be written to.
    338   * @param {string} [profilePath=null]
    339   *   This is null if the backup is being run on the currently running user
    340   *   profile. If, however, the backup is being run on a different user profile
    341   *   (for example, it's being run from a BackgroundTask on a user profile that
    342   *   just shut down, or during test), then this is a string set to that user
    343   *   profile path.
    344   * @param {boolean} [isEncrypting=false]
    345   *   True if the backup is being encrypted. A BackupResource may not require
    346   *   encryption, but might still choose to behave differently when encrypting,
    347   *   so this flag can be used to support that kind of behaviour.
    348   *
    349   * @returns {Promise<object|null>}
    350   */
    351  // eslint-disable-next-line no-unused-vars
    352  async backup(stagingPath, profilePath = null, isEncrypting = false) {
    353    throw new lazy.BackupError(
    354      "BackupResource::backup must be overridden",
    355      lazy.ERRORS.INTERNAL_ERROR
    356    );
    357  }
    358 
    359  /**
    360   * Recovers the datastores that this resource manages from a backup archive
    361   * that has been decompressed into the recoveryPath. A pre-existing unlocked
    362   * user profile should be available to restore into, and destProfilePath
    363   * should point at its location on the file system.
    364   *
    365   * This method is not expected to be running in an app connected to the
    366   * destProfilePath. If the BackupResource needs to run some operations
    367   * while attached to the recovery profile, it should do that work inside of
    368   * postRecovery(). If data needs to be transferred to postRecovery(), it
    369   * should be passed as a JSON serializable object in the return value of this
    370   * method.
    371   *
    372   * @see BackupResource.postRecovery()
    373   * @param {object|null} manifestEntry
    374   *   The object that was returned by the backup() method when the backup was
    375   *   created. This object can be null if no additional information was needed
    376   *   for recovery.
    377   * @param {string} recoveryPath
    378   *   The path to the resource directory where the backup archive has been
    379   *   decompressed.
    380   * @param {string} destProfilePath
    381   *   The path to the profile directory where the backup should be restored to.
    382   * @returns {Promise<object|null>}
    383   *   This should return a JSON serializable object that will be passed to
    384   *   postRecovery() if any data needs to be passed to it. This object can be
    385   *   null if no additional information is needed for postRecovery().
    386   */
    387  // eslint-disable-next-line no-unused-vars
    388  async recover(manifestEntry, recoveryPath, destProfilePath) {
    389    throw new lazy.BackupError(
    390      "BackupResource::recover must be overridden",
    391      lazy.ERRORS.INTERNAL_ERROR
    392    );
    393  }
    394 
    395  /**
    396   * Perform any post-recovery operations that need to be done after the
    397   * recovery has been completed and the recovered profile has been attached
    398   * to.
    399   *
    400   * This method is running in an app connected to the recovered profile. The
    401   * profile is locked, but this postRecovery method can be used to insert
    402   * data into connected datastores, or perform any other operations that can
    403   * only occur within the context of the recovered profile.
    404   *
    405   * @see BackupResource.recover()
    406   * @param {object|null} postRecoveryEntry
    407   *  The object that was returned by the recover() method when the recovery
    408   *  was originally done. This object can be null if no additional information
    409   *  is needed for post-recovery.
    410   */
    411  // eslint-disable-next-line no-unused-vars
    412  async postRecovery(postRecoveryEntry) {
    413    // no-op by default
    414  }
    415 }
    416 
    417 XPCOMUtils.defineLazyPreferenceGetter(
    418  BackupResource,
    419  "SQLITE_PAGES_PER_STEP",
    420  "browser.backup.sqlite.pages_per_step",
    421  5
    422 );
    423 
    424 XPCOMUtils.defineLazyPreferenceGetter(
    425  BackupResource,
    426  "SQLITE_STEP_DELAY_MS",
    427  "browser.backup.sqlite.step_delay_ms",
    428  250
    429 );