tor-browser

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

MigratorBase.sys.mjs (18471B)


      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 http://mozilla.org/MPL/2.0/. */
      4 
      5 const TOPIC_WILL_IMPORT_BOOKMARKS =
      6  "initial-migration-will-import-default-bookmarks";
      7 const TOPIC_DID_IMPORT_BOOKMARKS =
      8  "initial-migration-did-import-default-bookmarks";
      9 const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete";
     10 
     11 const lazy = {};
     12 
     13 ChromeUtils.defineESModuleGetters(lazy, {
     14  BookmarkHTMLUtils: "resource://gre/modules/BookmarkHTMLUtils.sys.mjs",
     15  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     16  FirefoxProfileMigrator: "resource:///modules/FirefoxProfileMigrator.sys.mjs",
     17  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
     18  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     19 });
     20 
     21 /**
     22 * @typedef {object} MigratorResource
     23 *   A resource returned by a subclass of MigratorBase that can migrate
     24 *   data to this browser.
     25 * @property {number} type
     26 *   A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate
     27 *   what this resource represents. A resource can represent one or more types
     28 *   of data, for example HISTORY and FORMDATA.
     29 * @property {Function} migrate
     30 *   A function that will actually perform the migration of this resource's
     31 *   data into this browser.
     32 */
     33 
     34 /**
     35 * Shared prototype for migrators.
     36 *
     37 * To implement a migrator:
     38 * 1. Import this module.
     39 * 2. Create a subclass of MigratorBase for your new migrator.
     40 * 3. Override the `key` static getter with a unique identifier for the browser
     41 *    that this migrator migrates from.
     42 * 4. If the migrator supports multiple profiles, override the sourceProfiles
     43 *    Here we default for single-profile migrator.
     44 * 5. Implement getResources(aProfile) (see below).
     45 * 6. For startup-only migrators, override ``startupOnlyMigrator``.
     46 * 7. Add the migrator to the MIGRATOR_MODULES structure in MigrationUtils.sys.mjs.
     47 */
     48 export class MigratorBase {
     49  /**
     50   * This must be overridden to return a simple string identifier for the
     51   * migrator, for example "firefox", "chrome", "opera-gx". This key is what
     52   * is used as an identifier when calling MigrationUtils.getMigrator.
     53   *
     54   * @type {string}
     55   */
     56  static get key() {
     57    throw new Error("MigratorBase.key must be overridden.");
     58  }
     59 
     60  /**
     61   * This must be overridden to return a Fluent string ID mapping to the display
     62   * name for this migrator. These strings should be defined in migrationWizard.ftl.
     63   *
     64   * @type {string}
     65   */
     66  static get displayNameL10nID() {
     67    throw new Error("MigratorBase.displayNameL10nID must be overridden.");
     68  }
     69 
     70  /**
     71   * This method should get overridden to return an icon url of the browser
     72   * to be imported from. By default, this will just use the default Favicon
     73   * image.
     74   *
     75   * @type {string}
     76   */
     77  static get brandImage() {
     78    return "chrome://global/skin/icons/defaultFavicon.svg";
     79  }
     80 
     81  /**
     82   * OVERRIDE IF AND ONLY IF the source supports multiple profiles.
     83   *
     84   * Returns array of profile objects from which data may be imported. The object
     85   * should have the following keys:
     86   *   id - a unique string identifier for the profile
     87   *   name - a pretty name to display to the user in the UI
     88   *
     89   * Only profiles from which data can be imported should be listed.  Otherwise
     90   * the behavior of the migration wizard isn't well-defined.
     91   *
     92   * For a single-profile source (e.g. safari, ie), this returns null,
     93   * and not an empty array.  That is the default implementation.
     94   *
     95   * @abstract
     96   * @returns {Promise<object[]|null>}
     97   */
     98  getSourceProfiles() {
     99    return null;
    100  }
    101 
    102  /**
    103   * MUST BE OVERRIDDEN.
    104   *
    105   * Returns an array of "migration resources" objects for the given profile,
    106   * or for the "default" profile, if the migrator does not support multiple
    107   * profiles.
    108   *
    109   * Each migration resource should provide:
    110   * - a ``type`` getter, returning any of the migration resource types (see
    111   *   MigrationUtils.resourceTypes).
    112   *
    113   * - a ``migrate`` method, taking two arguments,
    114   *   aCallback(bool success, object details), for migrating the data for
    115   *   this resource.  It may do its job synchronously or asynchronously.
    116   *   Either way, it must call aCallback(bool aSuccess, object details)
    117   *   when it's done.  In the case of an exception thrown from ``migrate``,
    118   *   it's taken as if aCallback(false, {}) is called. The details
    119   *   argument is sometimes optional, but conditional on how the
    120   *   migration wizard wants to display the migration state for the
    121   *   resource.
    122   *
    123   *   Note: In the case of a simple asynchronous implementation, you may find
    124   *   MigrationUtils.wrapMigrateFunction handy for handling aCallback easily.
    125   *
    126   * For each migration type listed in MigrationUtils.resourceTypes, multiple
    127   * migration resources may be provided.  This practice is useful when the
    128   * data for a certain migration type is independently stored in few
    129   * locations.  For example, the mac version of Safari stores its "reading list"
    130   * bookmarks in a separate property list.
    131   *
    132   * Note that the importation of a particular migration type is reported as
    133   * successful if _any_ of its resources succeeded to import (that is, called,
    134   * ``aCallback(true, {})``).  However, completion-status for a particular migration
    135   * type is reported to the UI only once all of its migrators have called
    136   * aCallback.
    137   *
    138   * NOTE: The returned array should only include resources from which data
    139   * can be imported.  So, for example, before adding a resource for the
    140   * BOOKMARKS migration type, you should check if you should check that the
    141   * bookmarks file exists.
    142   *
    143   * @abstract
    144   * @param {object|string} _aProfile
    145   *  The profile from which data may be imported, or an empty string
    146   *  in the case of a single-profile migrator.
    147   *  In the case of multiple-profiles migrator, it is guaranteed that
    148   *  aProfile is a value returned by the sourceProfiles getter (see
    149   *  above).
    150   * @returns {Promise<MigratorResource[]>|MigratorResource[]}
    151   */
    152  getResources(_aProfile) {
    153    throw new Error("getResources must be overridden");
    154  }
    155 
    156  /**
    157   * OVERRIDE in order to provide an estimate of when the last time was
    158   * that somebody used the browser. It is OK that this is somewhat fuzzy -
    159   * history may not be available (or be wiped or not present due to e.g.
    160   * incognito mode).
    161   *
    162   * If not overridden, the promise will resolve to the Unix epoch.
    163   *
    164   * @returns {Promise<Date>}
    165   *   A Promise that resolves to the last used date.
    166   */
    167  getLastUsedDate() {
    168    return Promise.resolve(new Date(0));
    169  }
    170 
    171  /**
    172   * OVERRIDE IF AND ONLY IF the migrator is a startup-only migrator (For now,
    173   * that is just the Firefox migrator, see bug 737381).  Default: false.
    174   *
    175   * Startup-only migrators are different in two ways:
    176   * - they may only be used during startup.
    177   * - the user-profile is half baked during migration.  The folder exists,
    178   *   but it's only accessible through MigrationUtils.profileStartup.
    179   *   The migrator can call MigrationUtils.profileStartup.doStartup
    180   *   at any point in order to initialize the profile.
    181   *
    182   * @returns {boolean}
    183   *   true if the migrator is start-up only.
    184   */
    185  get startupOnlyMigrator() {
    186    return false;
    187  }
    188 
    189  /**
    190   * Returns true if the migrator is configured to be enabled. This is
    191   * controlled by the `browser.migrate.<BROWSER_KEY>.enabled` boolean
    192   * preference.
    193   *
    194   * @returns {boolean}
    195   *   true if the migrator should be shown in the migration wizard.
    196   */
    197  get enabled() {
    198    let key = this.constructor.key;
    199    return Services.prefs.getBoolPref(`browser.migrate.${key}.enabled`, false);
    200  }
    201 
    202  /**
    203   * Subclasses should implement this if special checks need to be made to determine
    204   * if certain permissions need to be requested before data can be imported.
    205   * The returned Promise resolves to true if the required permissions have
    206   * been granted and a migration could proceed.
    207   *
    208   * @returns {Promise<boolean>}
    209   */
    210  async hasPermissions() {
    211    return Promise.resolve(true);
    212  }
    213 
    214  /**
    215   * Subclasses should implement this if special permissions need to be
    216   * requested from the user or the operating system in order to perform
    217   * a migration with this MigratorBase. This will be called only if
    218   * hasPermissions resolves to false.
    219   *
    220   * The returned Promise will resolve to true if permissions were successfully
    221   * obtained, and false otherwise. Implementors should ensure that if a call
    222   * to getPermissions resolves to true, that the MigratorBase will be able to
    223   * get read access to all of the resources it needs to do a migration.
    224   *
    225   * @param {DOMWindow} _win
    226   *   The top-level DOM window hosting the UI that is requesting the permission.
    227   *   This can be used to, for example, anchor a file picker window to the
    228   *   same window that is hosting the migration UI.
    229   * @returns {Promise<boolean>}
    230   */
    231  async getPermissions(_win) {
    232    return Promise.resolve(true);
    233  }
    234 
    235  /**
    236   * @returns {Promise<boolean|string>}
    237   */
    238  async canGetPermissions() {
    239    return Promise.resolve(false);
    240  }
    241 
    242  /**
    243   * Subclasses should override this and return true if the source browser
    244   * cannot have its passwords imported directly, and if there is a specialized
    245   * flow through the wizard to walk the user through importing from a CSV
    246   * file manually.
    247   */
    248  get showsManualPasswordImport() {
    249    return false;
    250  }
    251 
    252  /**
    253   * This method returns a number that is the bitwise OR of all resource
    254   * types that are available in aProfile. See MigrationUtils.resourceTypes
    255   * for each resource type.
    256   *
    257   * @param {object|string} aProfile
    258   *   The profile from which data may be imported, or an empty string
    259   *   in the case of a single-profile migrator.
    260   * @returns {number}
    261   */
    262  async getMigrateData(aProfile) {
    263    let resources = await this.#getMaybeCachedResources(aProfile);
    264    if (!resources) {
    265      return 0;
    266    }
    267    let types = resources.map(r => r.type);
    268    return types.reduce((a, b) => {
    269      a |= b;
    270      return a;
    271    }, 0);
    272  }
    273 
    274  /**
    275   * @see MigrationUtils
    276   *
    277   * @param {number} aItems
    278   *   A bitfield with bits from MigrationUtils.resourceTypes flipped to indicate
    279   *   what types of resources should be migrated.
    280   * @param {boolean} aStartup
    281   *   True if this migration is occurring during startup.
    282   * @param {object|string} aProfile
    283   *   The other browser profile that is being migrated from.
    284   * @param {Function|null} aProgressCallback
    285   *   An optional callback that will be fired once a resourceType has finished
    286   *   migrating. The callback will be passed the numeric representation of the
    287   *   resource type followed by a boolean indicating whether or not the resource
    288   *   was migrated successfully and optionally an object containing additional
    289   *   details.
    290   */
    291  async migrate(aItems, aStartup, aProfile, aProgressCallback = () => {}) {
    292    let resources = await this.#getMaybeCachedResources(aProfile);
    293    if (!resources.length) {
    294      throw new Error("migrate called for a non-existent source");
    295    }
    296 
    297    if (aItems != lazy.MigrationUtils.resourceTypes.ALL) {
    298      resources = resources.filter(r => aItems & r.type);
    299    }
    300 
    301    // Used to periodically give back control to the main-thread loop.
    302    let unblockMainThread = function () {
    303      return new Promise(resolve => {
    304        Services.tm.dispatchToMainThread(resolve);
    305      });
    306    };
    307 
    308    let browserKey = this.constructor.key;
    309 
    310    let collectQuantityTelemetry = () => {
    311      for (let resourceType of Object.keys(
    312        lazy.MigrationUtils._importQuantities
    313      )) {
    314        let metricName = resourceType + "Quantity";
    315        try {
    316          Glean.browserMigration[metricName][browserKey].accumulateSingleSample(
    317            lazy.MigrationUtils._importQuantities[resourceType]
    318          );
    319        } catch (ex) {
    320          console.error(metricName, ": ", ex);
    321        }
    322      }
    323    };
    324 
    325    let collectMigrationTelemetry = resourceType => {
    326      // We don't want to collect this if the migration is occurring due to a
    327      // profile refresh.
    328      if (this.constructor.key == lazy.FirefoxProfileMigrator.key) {
    329        return;
    330      }
    331 
    332      let prefKey = null;
    333      switch (resourceType) {
    334        case lazy.MigrationUtils.resourceTypes.BOOKMARKS: {
    335          prefKey = "browser.migrate.interactions.bookmarks";
    336          break;
    337        }
    338        case lazy.MigrationUtils.resourceTypes.HISTORY: {
    339          prefKey = "browser.migrate.interactions.history";
    340          break;
    341        }
    342        case lazy.MigrationUtils.resourceTypes.PASSWORDS: {
    343          prefKey = "browser.migrate.interactions.passwords";
    344          break;
    345        }
    346        default: {
    347          return;
    348        }
    349      }
    350 
    351      if (prefKey) {
    352        Services.prefs.setBoolPref(prefKey, true);
    353      }
    354    };
    355 
    356    // Called either directly or through the bookmarks import callback.
    357    let doMigrate = async function () {
    358      let resourcesGroupedByItems = new Map();
    359      resources.forEach(function (resource) {
    360        if (!resourcesGroupedByItems.has(resource.type)) {
    361          resourcesGroupedByItems.set(resource.type, new Set());
    362        }
    363        resourcesGroupedByItems.get(resource.type).add(resource);
    364      });
    365 
    366      if (resourcesGroupedByItems.size == 0) {
    367        throw new Error("No items to import");
    368      }
    369 
    370      let notify = function (aMsg, aItemType) {
    371        Services.obs.notifyObservers(null, aMsg, aItemType);
    372      };
    373 
    374      for (let resourceType of Object.keys(
    375        lazy.MigrationUtils._importQuantities
    376      )) {
    377        lazy.MigrationUtils._importQuantities[resourceType] = 0;
    378      }
    379      notify("Migration:Started");
    380      for (let [migrationType, itemResources] of resourcesGroupedByItems) {
    381        notify("Migration:ItemBeforeMigrate", migrationType);
    382 
    383        let itemSuccess = false;
    384        for (let res of itemResources) {
    385          let completeDeferred = Promise.withResolvers();
    386          let resourceDone = function (aSuccess, details) {
    387            itemResources.delete(res);
    388            itemSuccess |= aSuccess;
    389            if (itemResources.size == 0) {
    390              notify(
    391                itemSuccess
    392                  ? "Migration:ItemAfterMigrate"
    393                  : "Migration:ItemError",
    394                migrationType
    395              );
    396              collectMigrationTelemetry(migrationType);
    397 
    398              aProgressCallback(migrationType, itemSuccess, details);
    399 
    400              resourcesGroupedByItems.delete(migrationType);
    401 
    402              if (resourcesGroupedByItems.size == 0) {
    403                collectQuantityTelemetry();
    404 
    405                notify("Migration:Ended");
    406              }
    407            }
    408            completeDeferred.resolve();
    409          };
    410 
    411          // If migrate throws, an error occurred, and the callback
    412          // (itemMayBeDone) might haven't been called.
    413          try {
    414            res.migrate(resourceDone);
    415          } catch (ex) {
    416            console.error(ex);
    417            resourceDone(false);
    418          }
    419 
    420          await completeDeferred.promise;
    421          await unblockMainThread();
    422        }
    423      }
    424    };
    425 
    426    if (
    427      lazy.MigrationUtils.isStartupMigration &&
    428      !this.startupOnlyMigrator &&
    429      Services.policies.isAllowed("defaultBookmarks")
    430    ) {
    431      lazy.MigrationUtils.profileStartup.doStartup();
    432      // First import the default bookmarks.
    433      // Note: We do not need to do so for the Firefox migrator
    434      // (=startupOnlyMigrator), as it just copies over the places database
    435      // from another profile.
    436      await (async function () {
    437        // Tell whoever cares we're importing default bookmarks.
    438        lazy.BrowserUtils.callModulesFromCategory({
    439          categoryName: TOPIC_WILL_IMPORT_BOOKMARKS,
    440        });
    441 
    442        // Import the default bookmarks. We ignore whether or not we succeed.
    443        await lazy.BookmarkHTMLUtils.importFromURL(
    444          "chrome://browser/content/default-bookmarks.html",
    445          {
    446            replace: true,
    447            source: lazy.PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP,
    448          }
    449        ).catch(console.error);
    450 
    451        // We'll tell places we've imported bookmarks, but before that
    452        // we need to make sure we're going to know when it's finished
    453        // initializing:
    454        let placesInitedPromise = lazy.BrowserUtils.promiseObserved(
    455          TOPIC_PLACES_DEFAULTS_FINISHED
    456        );
    457 
    458        lazy.BrowserUtils.callModulesFromCategory({
    459          categoryName: TOPIC_DID_IMPORT_BOOKMARKS,
    460        });
    461        await placesInitedPromise;
    462        await doMigrate();
    463      })();
    464      return;
    465    }
    466    await doMigrate();
    467  }
    468 
    469  /**
    470   * Checks to see if one or more profiles exist for the browser that this
    471   * migrator migrates from.
    472   *
    473   * @returns {Promise<boolean>}
    474   *   True if one or more profiles exists that this migrator can migrate
    475   *   resources from.
    476   */
    477  async isSourceAvailable() {
    478    if (this.startupOnlyMigrator && !lazy.MigrationUtils.isStartupMigration) {
    479      return false;
    480    }
    481 
    482    // For a single-profile source, check if any data is available.
    483    // For multiple-profiles source, make sure that at least one
    484    // profile is available.
    485    let exists = false;
    486    try {
    487      let profiles = await this.getSourceProfiles();
    488      if (!profiles) {
    489        let resources = await this.#getMaybeCachedResources("");
    490        if (resources && resources.length) {
    491          exists = true;
    492        }
    493      } else {
    494        exists = !!profiles.length;
    495      }
    496    } catch (ex) {
    497      console.error(ex);
    498    }
    499    return exists;
    500  }
    501 
    502  /*** PRIVATE STUFF - DO NOT OVERRIDE ***/
    503 
    504  /**
    505   * Returns resources for a particular profile and then caches them for later
    506   * lookups.
    507   *
    508   * @param {object|string} aProfile
    509   *   The profile that resources are being imported from.
    510   * @returns {Promise<MigrationResource[]>}
    511   */
    512  async #getMaybeCachedResources(aProfile) {
    513    let profileKey = aProfile ? aProfile.id : "";
    514    if (this._resourcesByProfile) {
    515      if (profileKey in this._resourcesByProfile) {
    516        return this._resourcesByProfile[profileKey];
    517      }
    518    } else {
    519      this._resourcesByProfile = {};
    520    }
    521    this._resourcesByProfile[profileKey] = await this.getResources(aProfile);
    522    return this._resourcesByProfile[profileKey];
    523  }
    524 }