tor-browser

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

ChromeProfileMigrator.sys.mjs (38638B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 * vim: sw=2 ts=2 sts=2 et */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 const AUTH_TYPE = {
      8  SCHEME_HTML: 0,
      9  SCHEME_BASIC: 1,
     10  SCHEME_DIGEST: 2,
     11 };
     12 
     13 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     14 import { MigrationUtils } from "resource:///modules/MigrationUtils.sys.mjs";
     15 import { MigratorBase } from "resource:///modules/MigratorBase.sys.mjs";
     16 
     17 const lazy = {};
     18 
     19 ChromeUtils.defineESModuleGetters(lazy, {
     20  ChromeMigrationUtils: "resource:///modules/ChromeMigrationUtils.sys.mjs",
     21  FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
     22  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
     23  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     24  Qihoo360seMigrationUtils: "resource:///modules/360seMigrationUtils.sys.mjs",
     25  MigrationWizardConstants:
     26    "chrome://browser/content/migration/migration-wizard-constants.mjs",
     27 });
     28 
     29 /**
     30 * Converts an array of chrome bookmark objects into one our own places code
     31 * understands.
     32 *
     33 * @param {object[]} items Chrome Bookmark items to be inserted on this parent
     34 * @param {set} bookmarkURLAccumulator Accumulate all imported bookmark urls to be used for importing favicons
     35 * @param {Function} errorAccumulator function that gets called with any errors
     36 *   thrown so we don't drop them on the floor.
     37 * @returns {object[]}
     38 */
     39 function convertBookmarks(items, bookmarkURLAccumulator, errorAccumulator) {
     40  let itemsToInsert = [];
     41  for (let item of items) {
     42    try {
     43      if (item.type == "url") {
     44        if (item.url.trim().startsWith("chrome:")) {
     45          // Skip invalid internal URIs. Creating an actual URI always reports
     46          // messages to the console because Gecko has its own concept of how
     47          // chrome:// URIs should be formed, so we avoid doing that.
     48          continue;
     49        }
     50        if (item.url.trim().startsWith("edge:")) {
     51          // Don't import internal Microsoft Edge URIs as they won't resolve within Firefox.
     52          continue;
     53        }
     54        itemsToInsert.push({ url: item.url, title: item.name });
     55        bookmarkURLAccumulator.add({ url: item.url });
     56      } else if (item.type == "folder") {
     57        let folderItem = {
     58          type: lazy.PlacesUtils.bookmarks.TYPE_FOLDER,
     59          title: item.name,
     60        };
     61        folderItem.children = convertBookmarks(
     62          item.children,
     63          bookmarkURLAccumulator,
     64          errorAccumulator
     65        );
     66        itemsToInsert.push(folderItem);
     67      }
     68    } catch (ex) {
     69      console.error(ex);
     70      errorAccumulator(ex);
     71    }
     72  }
     73  return itemsToInsert;
     74 }
     75 
     76 /**
     77 * Chrome profile migrator. This can also be used as a parent class for
     78 * migrators for browsers that are variants of Chrome.
     79 */
     80 export class ChromeProfileMigrator extends MigratorBase {
     81  /**
     82   * On Ubuntu Linux, when the browser is installed as a Snap package,
     83   * we must request permission to read data from other browsers. We
     84   * make that request by opening up a native file picker in folder
     85   * selection mode and instructing the user to navigate to the folder
     86   * that the other browser's user data resides in.
     87   *
     88   * For Snap packages, this gives the browser read access - but it does
     89   * so through a temporary symlink that does not match the original user
     90   * data path. Effectively, the user data directory is remapped to a
     91   * temporary location on the file system. We record these remaps here,
     92   * keyed on the original data directory.
     93   *
     94   * @type {Map<string, string>}
     95   */
     96  #dataPathRemappings = new Map();
     97 
     98  static get key() {
     99    return "chrome";
    100  }
    101 
    102  static get displayNameL10nID() {
    103    return "migration-wizard-migrator-display-name-chrome";
    104  }
    105 
    106  static get brandImage() {
    107    return "chrome://browser/content/migration/brands/chrome.png";
    108  }
    109 
    110  get _chromeUserDataPathSuffix() {
    111    return "Chrome";
    112  }
    113 
    114  async hasPermissions() {
    115    let dataPath = await this._getChromeUserDataPathIfExists();
    116    if (!dataPath) {
    117      return true;
    118    }
    119 
    120    let localStatePath = PathUtils.join(dataPath, "Local State");
    121    try {
    122      // Read one byte since on snap we can check existence even without being able
    123      // to read the file.
    124      await IOUtils.read(localStatePath, { maxBytes: 1 });
    125      return true;
    126    } catch (ex) {
    127      console.error("No permissions for local state folder.");
    128    }
    129    return false;
    130  }
    131 
    132  async getPermissions(win) {
    133    // Get the original path to the user data and ignore any existing remapping.
    134    // This allows us to set a new remapping if the user navigates the platforms
    135    // filepicker to a different directory on a second permission request attempt.
    136    let originalDataPath = await this._getChromeUserDataPathIfExists(
    137      true /* noRemapping */
    138    );
    139    // Keep prompting the user until they pick something that grants us access
    140    // to Chrome's local state directory.
    141    while (!(await this.hasPermissions())) {
    142      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
    143      fp.init(win?.browsingContext, "", Ci.nsIFilePicker.modeGetFolder);
    144      fp.filterIndex = 1;
    145      // Now wait for the filepicker to open and close. If the user picks
    146      // the local state folder, the OS should grant us read access to everything
    147      // inside, so we don't need to check or do anything else with what's
    148      // returned by the filepicker.
    149      let result = await new Promise(resolve => fp.open(resolve));
    150      // Bail if the user cancels the dialog:
    151      if (result == Ci.nsIFilePicker.returnCancel) {
    152        return false;
    153      }
    154 
    155      let file = fp.file;
    156      if (file && file.path != originalDataPath) {
    157        this.#dataPathRemappings.set(originalDataPath, file.path);
    158      }
    159    }
    160    return true;
    161  }
    162 
    163  async canGetPermissions() {
    164    if (
    165      !Services.prefs.getBoolPref(
    166        "browser.migrate.chrome.get_permissions.enabled"
    167      )
    168    ) {
    169      return false;
    170    }
    171 
    172    if (await MigrationUtils.canGetPermissionsOnPlatform()) {
    173      let dataPath = await this._getChromeUserDataPathIfExists();
    174      if (dataPath) {
    175        let localStatePath = PathUtils.join(dataPath, "Local State");
    176        if (await IOUtils.exists(localStatePath)) {
    177          return dataPath;
    178        }
    179      }
    180    }
    181    return false;
    182  }
    183 
    184  /**
    185   * For Chrome on Windows, we show a specialized flow for importing passwords
    186   * from a CSV file.
    187   *
    188   * @returns {boolean}
    189   */
    190  get showsManualPasswordImport() {
    191    return AppConstants.platform == "win" && this.constructor.key == "chrome";
    192  }
    193 
    194  _keychainServiceName = "Chrome Safe Storage";
    195 
    196  _keychainAccountName = "Chrome";
    197 
    198  /**
    199   * Returns a Promise that resolves to the data path containing the
    200   * Local State and profile directories for this browser.
    201   *
    202   * @param {boolean} [noRemapping=false]
    203   *   Set to true to bypass any remapping that might have occurred on
    204   *   platforms where the data path changes once permission has been
    205   *   granted.
    206   * @returns {Promise<string>}
    207   */
    208  async _getChromeUserDataPathIfExists(noRemapping = false) {
    209    if (this._chromeUserDataPath) {
    210      // Skip looking up any remapping if `noRemapping` was passed. This is
    211      // helpful if the caller needs create a new remapping and overwrite
    212      // an old remapping, as "real" user data path is used as a key for
    213      // the remapping.
    214      if (noRemapping) {
    215        return this._chromeUserDataPath;
    216      }
    217 
    218      let remappedPath = this.#dataPathRemappings.get(this._chromeUserDataPath);
    219      return remappedPath || this._chromeUserDataPath;
    220    }
    221    let path = await lazy.ChromeMigrationUtils.getDataPath(
    222      this._chromeUserDataPathSuffix
    223    );
    224    let exists = path && (await IOUtils.exists(path));
    225    if (exists) {
    226      this._chromeUserDataPath = path;
    227    } else {
    228      this._chromeUserDataPath = null;
    229    }
    230    return this._chromeUserDataPath;
    231  }
    232 
    233  async getResources(aProfile) {
    234    if (!(await this.hasPermissions())) {
    235      return [];
    236    }
    237 
    238    let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
    239    if (chromeUserDataPath) {
    240      let profileFolder = chromeUserDataPath;
    241      if (aProfile) {
    242        profileFolder = PathUtils.join(chromeUserDataPath, aProfile.id);
    243      }
    244      if (await IOUtils.exists(profileFolder)) {
    245        let possibleResourcePromises = [
    246          GetBookmarksResource(profileFolder, this.constructor.key),
    247          GetHistoryResource(profileFolder),
    248          GetFormdataResource(profileFolder),
    249          GetExtensionsResource(aProfile.id, this.constructor.key),
    250        ];
    251        if (lazy.ChromeMigrationUtils.supportsLoginsForPlatform) {
    252          possibleResourcePromises.push(
    253            this._GetPasswordsResource(profileFolder),
    254            this._GetPaymentMethodsResource(profileFolder, this.constructor.key)
    255          );
    256        }
    257 
    258        // Some of these Promises might reject due to things like database
    259        // corruptions. We absorb those rejections here and filter them
    260        // out so that we only try to import the resources that don't appear
    261        // corrupted.
    262        let possibleResources = await Promise.allSettled(
    263          possibleResourcePromises
    264        );
    265        return possibleResources
    266          .filter(promise => {
    267            return promise.status == "fulfilled" && promise.value !== null;
    268          })
    269          .map(promise => promise.value);
    270      }
    271    }
    272    return [];
    273  }
    274 
    275  async getLastUsedDate() {
    276    let sourceProfiles = await this.getSourceProfiles();
    277    if (!sourceProfiles) {
    278      return new Date(0);
    279    }
    280    let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
    281    if (!chromeUserDataPath) {
    282      return new Date(0);
    283    }
    284    let datePromises = sourceProfiles.map(async profile => {
    285      let basePath = PathUtils.join(chromeUserDataPath, profile.id);
    286      let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(
    287        async leafName => {
    288          let path = PathUtils.join(basePath, leafName);
    289          let info = await IOUtils.stat(path).catch(() => null);
    290          return info ? info.lastModified : 0;
    291        }
    292      );
    293      let dates = await Promise.all(fileDatePromises);
    294      return Math.max(...dates);
    295    });
    296    let datesOuter = await Promise.all(datePromises);
    297    datesOuter.push(0);
    298    return new Date(Math.max(...datesOuter));
    299  }
    300 
    301  async getSourceProfiles() {
    302    if ("__sourceProfiles" in this) {
    303      return this.__sourceProfiles;
    304    }
    305 
    306    let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
    307    if (!chromeUserDataPath) {
    308      return [];
    309    }
    310 
    311    let localState;
    312    let profiles = [];
    313    try {
    314      localState = await lazy.ChromeMigrationUtils.getLocalState(
    315        this._chromeUserDataPathSuffix,
    316        chromeUserDataPath
    317      );
    318      let info_cache = localState.profile.info_cache;
    319      for (let profileFolderName in info_cache) {
    320        profiles.push({
    321          id: profileFolderName,
    322          name: info_cache[profileFolderName].name || profileFolderName,
    323        });
    324      }
    325    } catch (e) {
    326      // Avoid reporting NotFoundErrors from trying to get local state.
    327      if (localState || e.name != "NotFoundError") {
    328        console.error("Error detecting Chrome profiles: ", e);
    329      }
    330 
    331      // If we didn't have permission to read the local state, return the
    332      // empty array. The user might have the opportunity to request
    333      // permission using `hasPermission` and `getPermission`.
    334      if (e.name == "NotAllowedError") {
    335        return [];
    336      }
    337 
    338      // If we weren't able to detect any profiles above, fallback to the Default profile.
    339      let defaultProfilePath = PathUtils.join(chromeUserDataPath, "Default");
    340      if (await IOUtils.exists(defaultProfilePath)) {
    341        profiles = [
    342          {
    343            id: "Default",
    344            name: "Default",
    345          },
    346        ];
    347      }
    348    }
    349 
    350    let profileResources = await Promise.all(
    351      profiles.map(async profile => ({
    352        profile,
    353        resources: await this.getResources(profile),
    354      }))
    355    );
    356 
    357    // Only list profiles from which any data can be imported
    358    this.__sourceProfiles = profileResources
    359      .filter(({ resources }) => {
    360        return resources && !!resources.length;
    361      }, this)
    362      .map(({ profile }) => profile);
    363    return this.__sourceProfiles;
    364  }
    365 
    366  async _GetPasswordsResource(aProfileFolder) {
    367    let loginPath = PathUtils.join(aProfileFolder, "Login Data");
    368    if (!(await IOUtils.exists(loginPath))) {
    369      return null;
    370    }
    371 
    372    let tempFilePath = null;
    373    if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) {
    374      tempFilePath = await IOUtils.createUniqueFile(
    375        PathUtils.tempDir,
    376        "Login Data"
    377      );
    378      await IOUtils.copy(loginPath, tempFilePath);
    379      loginPath = tempFilePath;
    380    }
    381 
    382    let {
    383      _chromeUserDataPathSuffix,
    384      _keychainServiceName,
    385      _keychainAccountName,
    386      _keychainMockPassphrase = null,
    387    } = this;
    388 
    389    let countQuery = `SELECT COUNT(*) FROM logins WHERE blacklisted_by_user = 0`;
    390 
    391    let countRows = await MigrationUtils.getRowsFromDBWithoutLocks(
    392      loginPath,
    393      "Chrome passwords",
    394      countQuery
    395    );
    396 
    397    if (!countRows[0].getResultByName("COUNT(*)")) {
    398      return null;
    399    }
    400 
    401    return {
    402      type: MigrationUtils.resourceTypes.PASSWORDS,
    403 
    404      async migrate(aCallback) {
    405        let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
    406          loginPath,
    407          "Chrome passwords",
    408          `SELECT origin_url, action_url, username_element, username_value,
    409          password_element, password_value, signon_realm, scheme, date_created,
    410          times_used FROM logins WHERE blacklisted_by_user = 0`
    411        )
    412          .catch(ex => {
    413            console.error(ex);
    414            aCallback(false);
    415          })
    416          .finally(() => {
    417            return tempFilePath && IOUtils.remove(tempFilePath);
    418          });
    419 
    420        // If the promise was rejected we will have already called aCallback,
    421        // so we can just return here.
    422        if (!rows) {
    423          return;
    424        }
    425 
    426        // If there are no relevant rows, return before initializing crypto and
    427        // thus prompting for Keychain access on macOS.
    428        if (!rows.length) {
    429          aCallback(true);
    430          return;
    431        }
    432 
    433        let loginCrypto;
    434        try {
    435          if (AppConstants.platform == "win") {
    436            let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
    437              "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
    438            );
    439            loginCrypto = new ChromeWindowsLoginCrypto(
    440              _chromeUserDataPathSuffix
    441            );
    442          } else if (AppConstants.platform == "macosx") {
    443            let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
    444              "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
    445            );
    446            loginCrypto = new ChromeMacOSLoginCrypto(
    447              _keychainServiceName,
    448              _keychainAccountName,
    449              _keychainMockPassphrase
    450            );
    451          } else {
    452            aCallback(false);
    453            return;
    454          }
    455        } catch (ex) {
    456          // Handle the user canceling Keychain access or other OSCrypto errors.
    457          console.error(ex);
    458          aCallback(false);
    459          return;
    460        }
    461 
    462        let logins = [];
    463        let fallbackCreationDate = new Date();
    464        const kValidSchemes = new Set(["https", "http", "ftp"]);
    465        for (let row of rows) {
    466          try {
    467            let origin_url = lazy.NetUtil.newURI(
    468              row.getResultByName("origin_url")
    469            );
    470            // Ignore entries for non-http(s)/ftp URLs because we likely can't
    471            // use them anyway.
    472            if (!kValidSchemes.has(origin_url.scheme)) {
    473              continue;
    474            }
    475            let loginInfo = {
    476              username: row.getResultByName("username_value"),
    477              password: await loginCrypto.decryptData(
    478                row.getResultByName("password_value"),
    479                null
    480              ),
    481              origin: origin_url.prePath,
    482              formActionOrigin: null,
    483              httpRealm: null,
    484              usernameElement: row.getResultByName("username_element"),
    485              passwordElement: row.getResultByName("password_element"),
    486              timeCreated: lazy.ChromeMigrationUtils.chromeTimeToDate(
    487                row.getResultByName("date_created") + 0,
    488                fallbackCreationDate
    489              ).getTime(),
    490              timesUsed: row.getResultByName("times_used") + 0,
    491            };
    492 
    493            switch (row.getResultByName("scheme")) {
    494              case AUTH_TYPE.SCHEME_HTML: {
    495                let action_url = row.getResultByName("action_url");
    496                if (!action_url) {
    497                  // If there is no action_url, store the wildcard "" value.
    498                  // See the `formActionOrigin` IDL comments.
    499                  loginInfo.formActionOrigin = "";
    500                  break;
    501                }
    502                let action_uri = lazy.NetUtil.newURI(action_url);
    503                if (!kValidSchemes.has(action_uri.scheme)) {
    504                  continue; // This continues the outer for loop.
    505                }
    506                loginInfo.formActionOrigin = action_uri.prePath;
    507                break;
    508              }
    509              case AUTH_TYPE.SCHEME_BASIC:
    510              case AUTH_TYPE.SCHEME_DIGEST:
    511                // signon_realm format is URIrealm, so we need remove URI
    512                loginInfo.httpRealm = row
    513                  .getResultByName("signon_realm")
    514                  .substring(loginInfo.origin.length + 1);
    515                break;
    516              default:
    517                throw new Error(
    518                  "Login data scheme type not supported: " +
    519                    row.getResultByName("scheme")
    520                );
    521            }
    522            logins.push(loginInfo);
    523          } catch (e) {
    524            console.error(e);
    525          }
    526        }
    527        try {
    528          if (logins.length) {
    529            await MigrationUtils.insertLoginsWrapper(logins);
    530          }
    531        } catch (e) {
    532          console.error(e);
    533        }
    534        if (crypto.finalize) {
    535          crypto.finalize();
    536        }
    537        aCallback(true);
    538      },
    539    };
    540  }
    541  async _GetPaymentMethodsResource(aProfileFolder, aBrowserKey = "chrome") {
    542    if (
    543      !Services.prefs.getBoolPref(
    544        "browser.migrate.chrome.payment_methods.enabled",
    545        false
    546      )
    547    ) {
    548      return null;
    549    }
    550 
    551    // We no longer support importing payment methods from Chrome or Edge on
    552    // Windows.
    553    if (
    554      AppConstants.platform == "win" &&
    555      (aBrowserKey == "chrome" || aBrowserKey == "chromium-edge")
    556    ) {
    557      return null;
    558    }
    559 
    560    let paymentMethodsPath = PathUtils.join(aProfileFolder, "Web Data");
    561 
    562    if (!(await IOUtils.exists(paymentMethodsPath))) {
    563      return null;
    564    }
    565 
    566    let tempFilePath = null;
    567    if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) {
    568      tempFilePath = await IOUtils.createUniqueFile(
    569        PathUtils.tempDir,
    570        "Web Data"
    571      );
    572      await IOUtils.copy(paymentMethodsPath, tempFilePath);
    573      paymentMethodsPath = tempFilePath;
    574    }
    575 
    576    let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
    577      paymentMethodsPath,
    578      "Chrome Credit Cards",
    579      "SELECT name_on_card, card_number_encrypted, expiration_month, expiration_year FROM credit_cards"
    580    )
    581      .catch(ex => {
    582        console.error(ex);
    583      })
    584      .finally(() => {
    585        return tempFilePath && IOUtils.remove(tempFilePath);
    586      });
    587 
    588    if (!rows?.length) {
    589      return null;
    590    }
    591 
    592    let {
    593      _chromeUserDataPathSuffix,
    594      _keychainServiceName,
    595      _keychainAccountName,
    596      _keychainMockPassphrase = null,
    597    } = this;
    598 
    599    return {
    600      type: MigrationUtils.resourceTypes.PAYMENT_METHODS,
    601 
    602      async migrate(aCallback) {
    603        let loginCrypto;
    604        try {
    605          if (AppConstants.platform == "win") {
    606            let { ChromeWindowsLoginCrypto } = ChromeUtils.importESModule(
    607              "resource:///modules/ChromeWindowsLoginCrypto.sys.mjs"
    608            );
    609            loginCrypto = new ChromeWindowsLoginCrypto(
    610              _chromeUserDataPathSuffix
    611            );
    612          } else if (AppConstants.platform == "macosx") {
    613            let { ChromeMacOSLoginCrypto } = ChromeUtils.importESModule(
    614              "resource:///modules/ChromeMacOSLoginCrypto.sys.mjs"
    615            );
    616            loginCrypto = new ChromeMacOSLoginCrypto(
    617              _keychainServiceName,
    618              _keychainAccountName,
    619              _keychainMockPassphrase
    620            );
    621          } else {
    622            aCallback(false);
    623            return;
    624          }
    625        } catch (ex) {
    626          // Handle the user canceling Keychain access or other OSCrypto errors.
    627          console.error(ex);
    628          aCallback(false);
    629          return;
    630        }
    631 
    632        let cards = [];
    633        for (let row of rows) {
    634          try {
    635            cards.push({
    636              "cc-name": row.getResultByName("name_on_card"),
    637              "cc-number": await loginCrypto.decryptData(
    638                row.getResultByName("card_number_encrypted"),
    639                null
    640              ),
    641              "cc-exp-month": parseInt(
    642                row.getResultByName("expiration_month"),
    643                10
    644              ),
    645              "cc-exp-year": parseInt(
    646                row.getResultByName("expiration_year"),
    647                10
    648              ),
    649            });
    650          } catch (e) {
    651            console.error(e);
    652          }
    653        }
    654 
    655        await MigrationUtils.insertCreditCardsWrapper(cards);
    656        aCallback(true);
    657      },
    658    };
    659  }
    660 }
    661 
    662 async function GetBookmarksResource(aProfileFolder, aBrowserKey) {
    663  let bookmarksPath = PathUtils.join(aProfileFolder, "Bookmarks");
    664  let faviconsPath = PathUtils.join(aProfileFolder, "Favicons");
    665 
    666  if (aBrowserKey === "chromium-360se") {
    667    let localState = {};
    668    try {
    669      localState = await lazy.ChromeMigrationUtils.getLocalState("360 SE");
    670    } catch (ex) {
    671      console.error(ex);
    672    }
    673 
    674    let alternativeBookmarks =
    675      await lazy.Qihoo360seMigrationUtils.getAlternativeBookmarks({
    676        bookmarksPath,
    677        localState,
    678      });
    679    if (alternativeBookmarks.resource) {
    680      return alternativeBookmarks.resource;
    681    }
    682 
    683    bookmarksPath = alternativeBookmarks.path;
    684  }
    685 
    686  if (!(await IOUtils.exists(bookmarksPath))) {
    687    return null;
    688  }
    689 
    690  let tempFilePath = null;
    691  if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) {
    692    tempFilePath = await IOUtils.createUniqueFile(
    693      PathUtils.tempDir,
    694      "Favicons"
    695    );
    696    await IOUtils.copy(faviconsPath, tempFilePath);
    697    faviconsPath = tempFilePath;
    698  }
    699 
    700  // check to read JSON bookmarks structure and see if any bookmarks exist else return null
    701  // Parse Chrome bookmark file that is JSON format
    702  let bookmarkJSON = await IOUtils.readJSON(bookmarksPath);
    703  let other = bookmarkJSON.roots.other.children.length;
    704  let bookmarkBar = bookmarkJSON.roots.bookmark_bar.children.length;
    705  let synced = bookmarkJSON.roots.synced.children.length;
    706 
    707  if (!other && !bookmarkBar && !synced) {
    708    return null;
    709  }
    710  return {
    711    type: MigrationUtils.resourceTypes.BOOKMARKS,
    712 
    713    migrate(aCallback) {
    714      return (async function () {
    715        let gotErrors = false;
    716        let errorGatherer = function () {
    717          gotErrors = true;
    718        };
    719 
    720        let faviconRows = [];
    721        try {
    722          faviconRows = await MigrationUtils.getRowsFromDBWithoutLocks(
    723            faviconsPath,
    724            "Chrome Bookmark Favicons",
    725            `select fav.id, fav.url, map.page_url, bit.image_data FROM favicons as fav
    726              INNER JOIN favicon_bitmaps bit ON (fav.id = bit.icon_id)
    727              INNER JOIN icon_mapping map ON (map.icon_id = bit.icon_id)`
    728          );
    729        } catch (ex) {
    730          console.error(ex);
    731        } finally {
    732          if (tempFilePath) {
    733            await IOUtils.remove(tempFilePath);
    734          }
    735        }
    736 
    737        // Create Hashmap for favicons
    738        let faviconMap = new Map();
    739        for (let faviconRow of faviconRows) {
    740          // First, try to normalize the URI:
    741          try {
    742            let uri = lazy.NetUtil.newURI(
    743              faviconRow.getResultByName("page_url")
    744            );
    745            faviconMap.set(uri.spec, {
    746              faviconData: faviconRow.getResultByName("image_data"),
    747              uri,
    748            });
    749          } catch (e) {
    750            // Couldn't parse the URI, so just skip it.
    751            continue;
    752          }
    753        }
    754 
    755        let roots = bookmarkJSON.roots;
    756        let bookmarkURLAccumulator = new Set();
    757 
    758        // Importing bookmark bar items
    759        if (roots.bookmark_bar.children && roots.bookmark_bar.children.length) {
    760          // Toolbar
    761          let parentGuid = lazy.PlacesUtils.bookmarks.toolbarGuid;
    762          let bookmarks = convertBookmarks(
    763            roots.bookmark_bar.children,
    764            bookmarkURLAccumulator,
    765            errorGatherer
    766          );
    767          await MigrationUtils.insertManyBookmarksWrapper(
    768            bookmarks,
    769            parentGuid
    770          );
    771        }
    772 
    773        // Importing Other Bookmarks items
    774        if (roots.other.children && roots.other.children.length) {
    775          // Other Bookmarks
    776          let parentGuid = lazy.PlacesUtils.bookmarks.unfiledGuid;
    777          let bookmarks = convertBookmarks(
    778            roots.other.children,
    779            bookmarkURLAccumulator,
    780            errorGatherer
    781          );
    782          await MigrationUtils.insertManyBookmarksWrapper(
    783            bookmarks,
    784            parentGuid
    785          );
    786        }
    787 
    788        // Importing synced Bookmarks items
    789        if (roots.synced.children && roots.synced.children.length) {
    790          // Synced  Bookmarks
    791          let parentGuid = lazy.PlacesUtils.bookmarks.unfiledGuid;
    792          let bookmarks = convertBookmarks(
    793            roots.synced.children,
    794            bookmarkURLAccumulator,
    795            errorGatherer
    796          );
    797          await MigrationUtils.insertManyBookmarksWrapper(
    798            bookmarks,
    799            parentGuid
    800          );
    801        }
    802 
    803        // Find all favicons with associated bookmarks
    804        let favicons = [];
    805        for (let bookmark of bookmarkURLAccumulator) {
    806          try {
    807            let uri = lazy.NetUtil.newURI(bookmark.url);
    808            let favicon = faviconMap.get(uri.spec);
    809            if (favicon) {
    810              favicons.push(favicon);
    811            }
    812          } catch (e) {
    813            // Couldn't parse the bookmark URI, so just skip
    814            continue;
    815          }
    816        }
    817 
    818        // Import Bookmark Favicons
    819        MigrationUtils.insertManyFavicons(favicons).catch(console.error);
    820 
    821        if (gotErrors) {
    822          throw new Error("The migration included errors.");
    823        }
    824      })().then(
    825        () => aCallback(true),
    826        () => aCallback(false)
    827      );
    828    },
    829  };
    830 }
    831 
    832 async function GetHistoryResource(aProfileFolder) {
    833  let historyPath = PathUtils.join(aProfileFolder, "History");
    834  if (!(await IOUtils.exists(historyPath))) {
    835    return null;
    836  }
    837 
    838  let tempFilePath = null;
    839  if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) {
    840    tempFilePath = await IOUtils.createUniqueFile(PathUtils.tempDir, "History");
    841    await IOUtils.copy(historyPath, tempFilePath);
    842    historyPath = tempFilePath;
    843  }
    844 
    845  let countQuery = "SELECT COUNT(*) FROM urls WHERE hidden = 0";
    846 
    847  let countRows = await MigrationUtils.getRowsFromDBWithoutLocks(
    848    historyPath,
    849    "Chrome history",
    850    countQuery
    851  );
    852  if (!countRows[0].getResultByName("COUNT(*)")) {
    853    return null;
    854  }
    855  return {
    856    type: MigrationUtils.resourceTypes.HISTORY,
    857 
    858    migrate(aCallback) {
    859      (async function () {
    860        const LIMIT = Services.prefs.getIntPref(
    861          "browser.migrate.chrome.history.limit"
    862        );
    863 
    864        let query =
    865          "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
    866        let maxAge = lazy.ChromeMigrationUtils.dateToChromeTime(
    867          Date.now() - MigrationUtils.HISTORY_MAX_AGE_IN_MILLISECONDS
    868        );
    869        query += " AND last_visit_time > " + maxAge;
    870 
    871        if (LIMIT) {
    872          query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
    873        }
    874 
    875        let rows;
    876        try {
    877          rows = await MigrationUtils.getRowsFromDBWithoutLocks(
    878            historyPath,
    879            "Chrome history",
    880            query
    881          );
    882        } finally {
    883          if (tempFilePath) {
    884            await IOUtils.remove(tempFilePath);
    885          }
    886        }
    887 
    888        let pageInfos = [];
    889        let fallbackVisitDate = new Date();
    890        for (let row of rows) {
    891          try {
    892            // if having typed_count, we changes transition type to typed.
    893            let transition = lazy.PlacesUtils.history.TRANSITIONS.LINK;
    894            if (row.getResultByName("typed_count") > 0) {
    895              transition = lazy.PlacesUtils.history.TRANSITIONS.TYPED;
    896            }
    897 
    898            pageInfos.push({
    899              title: row.getResultByName("title"),
    900              url: new URL(row.getResultByName("url")),
    901              visits: [
    902                {
    903                  transition,
    904                  date: lazy.ChromeMigrationUtils.chromeTimeToDate(
    905                    row.getResultByName("last_visit_time"),
    906                    fallbackVisitDate
    907                  ),
    908                },
    909              ],
    910            });
    911          } catch (e) {
    912            console.error(e);
    913          }
    914        }
    915 
    916        if (pageInfos.length) {
    917          await MigrationUtils.insertVisitsWrapper(pageInfos);
    918        }
    919      })().then(
    920        () => {
    921          aCallback(true);
    922        },
    923        ex => {
    924          console.error(ex);
    925          aCallback(false);
    926        }
    927      );
    928    },
    929  };
    930 }
    931 
    932 async function GetFormdataResource(aProfileFolder) {
    933  let formdataPath = PathUtils.join(aProfileFolder, "Web Data");
    934  if (!(await IOUtils.exists(formdataPath))) {
    935    return null;
    936  }
    937  let countQuery = "SELECT COUNT(*) FROM autofill";
    938 
    939  let tempFilePath = null;
    940  if (MigrationUtils.IS_LINUX_SNAP_PACKAGE) {
    941    tempFilePath = await IOUtils.createUniqueFile(
    942      PathUtils.tempDir,
    943      "Web Data"
    944    );
    945    await IOUtils.copy(formdataPath, tempFilePath);
    946    formdataPath = tempFilePath;
    947  }
    948 
    949  let countRows = await MigrationUtils.getRowsFromDBWithoutLocks(
    950    formdataPath,
    951    "Chrome formdata",
    952    countQuery
    953  );
    954  if (!countRows[0].getResultByName("COUNT(*)")) {
    955    return null;
    956  }
    957  return {
    958    type: MigrationUtils.resourceTypes.FORMDATA,
    959 
    960    async migrate(aCallback) {
    961      let query =
    962        "SELECT name, value, count, date_created, date_last_used FROM autofill";
    963      let rows;
    964 
    965      try {
    966        rows = await MigrationUtils.getRowsFromDBWithoutLocks(
    967          formdataPath,
    968          "Chrome formdata",
    969          query
    970        );
    971      } finally {
    972        if (tempFilePath) {
    973          await IOUtils.remove(tempFilePath);
    974        }
    975      }
    976 
    977      let addOps = [];
    978      for (let row of rows) {
    979        try {
    980          let fieldname = row.getResultByName("name");
    981          let value = row.getResultByName("value");
    982          if (fieldname && value) {
    983            addOps.push({
    984              op: "add",
    985              fieldname,
    986              value,
    987              timesUsed: row.getResultByName("count"),
    988              firstUsed: row.getResultByName("date_created") * 1000,
    989              lastUsed: row.getResultByName("date_last_used") * 1000,
    990            });
    991          }
    992        } catch (e) {
    993          console.error(e);
    994        }
    995      }
    996 
    997      try {
    998        await lazy.FormHistory.update(addOps);
    999      } catch (e) {
   1000        console.error(e);
   1001        aCallback(false);
   1002        return;
   1003      }
   1004 
   1005      aCallback(true);
   1006    },
   1007  };
   1008 }
   1009 
   1010 async function GetExtensionsResource(aProfileId, aBrowserKey = "chrome") {
   1011  if (
   1012    !Services.prefs.getBoolPref(
   1013      "browser.migrate.chrome.extensions.enabled",
   1014      false
   1015    )
   1016  ) {
   1017    return null;
   1018  }
   1019  let extensions = await lazy.ChromeMigrationUtils.getExtensionList(aProfileId);
   1020  if (!extensions.length || aBrowserKey !== "chrome") {
   1021    return null;
   1022  }
   1023 
   1024  return {
   1025    type: MigrationUtils.resourceTypes.EXTENSIONS,
   1026    async migrate(callback) {
   1027      let ids = extensions.map(extension => extension.id);
   1028      let [progressValue, importedExtensions] =
   1029        await MigrationUtils.installExtensionsWrapper(aBrowserKey, ids);
   1030      let details = {
   1031        progressValue,
   1032        totalExtensions: extensions,
   1033        importedExtensions,
   1034      };
   1035      if (
   1036        progressValue == lazy.MigrationWizardConstants.PROGRESS_VALUE.INFO ||
   1037        progressValue == lazy.MigrationWizardConstants.PROGRESS_VALUE.SUCCESS
   1038      ) {
   1039        callback(true, details);
   1040      } else {
   1041        callback(false);
   1042      }
   1043    },
   1044  };
   1045 }
   1046 
   1047 /**
   1048 * Chromium migrator
   1049 */
   1050 export class ChromiumProfileMigrator extends ChromeProfileMigrator {
   1051  static get key() {
   1052    return "chromium";
   1053  }
   1054 
   1055  static get displayNameL10nID() {
   1056    return "migration-wizard-migrator-display-name-chromium";
   1057  }
   1058 
   1059  static get brandImage() {
   1060    return "chrome://browser/content/migration/brands/chromium.png";
   1061  }
   1062 
   1063  _chromeUserDataPathSuffix = "Chromium";
   1064  _keychainServiceName = "Chromium Safe Storage";
   1065  _keychainAccountName = "Chromium";
   1066 }
   1067 
   1068 /**
   1069 * Chrome Canary
   1070 * Not available on Linux
   1071 */
   1072 export class CanaryProfileMigrator extends ChromeProfileMigrator {
   1073  static get key() {
   1074    return "canary";
   1075  }
   1076 
   1077  static get displayNameL10nID() {
   1078    return "migration-wizard-migrator-display-name-canary";
   1079  }
   1080 
   1081  static get brandImage() {
   1082    return "chrome://browser/content/migration/brands/canary.png";
   1083  }
   1084 
   1085  get _chromeUserDataPathSuffix() {
   1086    return "Canary";
   1087  }
   1088 
   1089  get _keychainServiceName() {
   1090    return "Chromium Safe Storage";
   1091  }
   1092 
   1093  get _keychainAccountName() {
   1094    return "Chromium";
   1095  }
   1096 }
   1097 
   1098 /**
   1099 * Chrome Dev - Linux only (not available in Mac and Windows)
   1100 */
   1101 export class ChromeDevMigrator extends ChromeProfileMigrator {
   1102  static get key() {
   1103    return "chrome-dev";
   1104  }
   1105 
   1106  static get displayNameL10nID() {
   1107    return "migration-wizard-migrator-display-name-chrome-dev";
   1108  }
   1109 
   1110  _chromeUserDataPathSuffix = "Chrome Dev";
   1111  _keychainServiceName = "Chromium Safe Storage";
   1112  _keychainAccountName = "Chromium";
   1113 }
   1114 
   1115 /**
   1116 * Chrome Beta migrator
   1117 */
   1118 export class ChromeBetaMigrator extends ChromeProfileMigrator {
   1119  static get key() {
   1120    return "chrome-beta";
   1121  }
   1122 
   1123  static get displayNameL10nID() {
   1124    return "migration-wizard-migrator-display-name-chrome-beta";
   1125  }
   1126 
   1127  _chromeUserDataPathSuffix = "Chrome Beta";
   1128  _keychainServiceName = "Chromium Safe Storage";
   1129  _keychainAccountName = "Chromium";
   1130 }
   1131 
   1132 /**
   1133 * Brave migrator
   1134 */
   1135 export class BraveProfileMigrator extends ChromeProfileMigrator {
   1136  static get key() {
   1137    return "brave";
   1138  }
   1139 
   1140  static get displayNameL10nID() {
   1141    return "migration-wizard-migrator-display-name-brave";
   1142  }
   1143 
   1144  static get brandImage() {
   1145    return "chrome://browser/content/migration/brands/brave.png";
   1146  }
   1147 
   1148  _chromeUserDataPathSuffix = "Brave";
   1149  _keychainServiceName = "Brave Browser Safe Storage";
   1150  _keychainAccountName = "Brave Browser";
   1151 }
   1152 
   1153 /**
   1154 * Edge (Chromium-based) migrator
   1155 */
   1156 export class ChromiumEdgeMigrator extends ChromeProfileMigrator {
   1157  static get key() {
   1158    return "chromium-edge";
   1159  }
   1160 
   1161  static get displayNameL10nID() {
   1162    return "migration-wizard-migrator-display-name-chromium-edge";
   1163  }
   1164 
   1165  static get brandImage() {
   1166    return "chrome://browser/content/migration/brands/edge.png";
   1167  }
   1168 
   1169  _chromeUserDataPathSuffix = "Edge";
   1170  _keychainServiceName = "Microsoft Edge Safe Storage";
   1171  _keychainAccountName = "Microsoft Edge";
   1172 }
   1173 
   1174 /**
   1175 * Edge Beta (Chromium-based) migrator
   1176 */
   1177 export class ChromiumEdgeBetaMigrator extends ChromeProfileMigrator {
   1178  static get key() {
   1179    return "chromium-edge-beta";
   1180  }
   1181 
   1182  static get displayNameL10nID() {
   1183    return "migration-wizard-migrator-display-name-chromium-edge-beta";
   1184  }
   1185 
   1186  static get brandImage() {
   1187    return "chrome://browser/content/migration/brands/edgebeta.png";
   1188  }
   1189 
   1190  _chromeUserDataPathSuffix = "Edge Beta";
   1191  _keychainServiceName = "Microsoft Edge Safe Storage";
   1192  _keychainAccountName = "Microsoft Edge";
   1193 }
   1194 
   1195 /**
   1196 * Chromium 360 migrator
   1197 */
   1198 export class Chromium360seMigrator extends ChromeProfileMigrator {
   1199  static get key() {
   1200    return "chromium-360se";
   1201  }
   1202 
   1203  static get displayNameL10nID() {
   1204    return "migration-wizard-migrator-display-name-chromium-360se";
   1205  }
   1206 
   1207  static get brandImage() {
   1208    return "chrome://browser/content/migration/brands/360.png";
   1209  }
   1210 
   1211  _chromeUserDataPathSuffix = "360 SE";
   1212  _keychainServiceName = "Microsoft Edge Safe Storage";
   1213  _keychainAccountName = "Microsoft Edge";
   1214 }
   1215 
   1216 /**
   1217 * Opera migrator
   1218 */
   1219 export class OperaProfileMigrator extends ChromeProfileMigrator {
   1220  static get key() {
   1221    return "opera";
   1222  }
   1223 
   1224  static get displayNameL10nID() {
   1225    return "migration-wizard-migrator-display-name-opera";
   1226  }
   1227 
   1228  static get brandImage() {
   1229    return "chrome://browser/content/migration/brands/opera.png";
   1230  }
   1231 
   1232  _chromeUserDataPathSuffix = "Opera";
   1233  _keychainServiceName = "Opera Safe Storage";
   1234  _keychainAccountName = "Opera";
   1235 
   1236  async getSourceProfiles() {
   1237    let detectedProfiles = await super.getSourceProfiles();
   1238    if (Array.isArray(detectedProfiles) && !detectedProfiles.length) {
   1239      // We might be attempting from a version of Opera that doesn't support
   1240      // profiles yet, so try returning null to see if the profile data
   1241      // exists in the data directory.
   1242      return null;
   1243    }
   1244    return detectedProfiles;
   1245  }
   1246 }
   1247 
   1248 /**
   1249 * Opera GX migrator
   1250 */
   1251 export class OperaGXProfileMigrator extends ChromeProfileMigrator {
   1252  static get key() {
   1253    return "opera-gx";
   1254  }
   1255 
   1256  static get displayNameL10nID() {
   1257    return "migration-wizard-migrator-display-name-opera-gx";
   1258  }
   1259 
   1260  static get brandImage() {
   1261    return "chrome://browser/content/migration/brands/operagx.png";
   1262  }
   1263 
   1264  _chromeUserDataPathSuffix = "Opera GX";
   1265  _keychainServiceName = "Opera Safe Storage";
   1266  _keychainAccountName = "Opera";
   1267 
   1268  getSourceProfiles() {
   1269    return null;
   1270  }
   1271 }
   1272 
   1273 /**
   1274 * Vivaldi migrator
   1275 */
   1276 export class VivaldiProfileMigrator extends ChromeProfileMigrator {
   1277  static get key() {
   1278    return "vivaldi";
   1279  }
   1280 
   1281  static get displayNameL10nID() {
   1282    return "migration-wizard-migrator-display-name-vivaldi";
   1283  }
   1284 
   1285  static get brandImage() {
   1286    return "chrome://browser/content/migration/brands/vivaldi.png";
   1287  }
   1288 
   1289  _chromeUserDataPathSuffix = "Vivaldi";
   1290  _keychainServiceName = "Vivaldi Safe Storage";
   1291  _keychainAccountName = "Vivaldi";
   1292 }