tor-browser

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

ChromeMigrationUtils.sys.mjs (17689B)


      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 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
     11  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
     12 });
     13 
     14 const S100NS_FROM1601TO1970 = 0x19db1ded53e8000;
     15 const S100NS_PER_MS = 10;
     16 
     17 export var ChromeMigrationUtils = {
     18  // Supported browsers with importable logins.
     19  CONTEXTUAL_LOGIN_IMPORT_BROWSERS: ["chrome", "chromium-edge", "chromium"],
     20 
     21  _extensionVersionDirectoryNames: {},
     22 
     23  // The cache for the locale strings.
     24  // For example, the data could be:
     25  // {
     26  //   "profile-id-1": {
     27  //     "extension-id-1": {
     28  //       "name": {
     29  //         "message": "Fake App 1"
     30  //       }
     31  //   },
     32  // }
     33  _extensionLocaleStrings: {},
     34 
     35  get supportsLoginsForPlatform() {
     36    return ["macosx", "win"].includes(AppConstants.platform);
     37  },
     38 
     39  /**
     40   * Get all extensions installed in a specific profile.
     41   *
     42   * @param {string} profileId - A Chrome user profile ID. For example, "Profile 1".
     43   * @returns {Array} All installed Chrome extensions information.
     44   */
     45  async getExtensionList(profileId) {
     46    if (profileId === undefined) {
     47      profileId = await this.getLastUsedProfileId();
     48    }
     49    let path = await this.getExtensionPath(profileId);
     50    let extensionList = [];
     51    try {
     52      for (const child of await IOUtils.getChildren(path)) {
     53        const info = await IOUtils.stat(child);
     54        if (info.type === "directory") {
     55          const name = PathUtils.filename(child);
     56          let extensionInformation = await this.getExtensionInformation(
     57            name,
     58            profileId
     59          );
     60          if (extensionInformation) {
     61            extensionList.push(extensionInformation);
     62          }
     63        }
     64      }
     65    } catch (ex) {
     66      console.error(ex);
     67    }
     68    return extensionList;
     69  },
     70 
     71  /**
     72   * Get information of a specific Chrome extension.
     73   *
     74   * @param {string} extensionId - The extension ID.
     75   * @param {string} profileId - The user profile's ID.
     76   * @returns {object} The Chrome extension information.
     77   */
     78  async getExtensionInformation(extensionId, profileId) {
     79    if (profileId === undefined) {
     80      profileId = await this.getLastUsedProfileId();
     81    }
     82    let extensionInformation = null;
     83    try {
     84      let manifestPath = await this.getExtensionPath(profileId);
     85      manifestPath = PathUtils.join(manifestPath, extensionId);
     86      // If there are multiple sub-directories in the extension directory,
     87      // read the files in the latest directory.
     88      let directories =
     89        await this._getSortedByVersionSubDirectoryNames(manifestPath);
     90      if (!directories[0]) {
     91        return null;
     92      }
     93 
     94      manifestPath = PathUtils.join(
     95        manifestPath,
     96        directories[0],
     97        "manifest.json"
     98      );
     99      let manifest = await IOUtils.readJSON(manifestPath);
    100      // No app attribute means this is a Chrome extension not a Chrome app.
    101      if (!manifest.app) {
    102        const DEFAULT_LOCALE = manifest.default_locale;
    103        let name = await this._getLocaleString(
    104          manifest.name,
    105          DEFAULT_LOCALE,
    106          extensionId,
    107          profileId
    108        );
    109        let description = await this._getLocaleString(
    110          manifest.description,
    111          DEFAULT_LOCALE,
    112          extensionId,
    113          profileId
    114        );
    115        if (name) {
    116          extensionInformation = {
    117            id: extensionId,
    118            name,
    119            description,
    120          };
    121        } else {
    122          throw new Error("Cannot read the Chrome extension's name property.");
    123        }
    124      }
    125    } catch (ex) {
    126      console.error(ex);
    127    }
    128    return extensionInformation;
    129  },
    130 
    131  /**
    132   * Get the manifest's locale string.
    133   *
    134   * @param {string} key - The key of a locale string, for example __MSG_name__.
    135   * @param {string} locale - The specific language of locale string.
    136   * @param {string} extensionId - The extension ID.
    137   * @param {string} profileId - The user profile's ID.
    138   * @returns {string|null} The locale string.
    139   */
    140  async _getLocaleString(key, locale, extensionId, profileId) {
    141    if (typeof key !== "string") {
    142      console.debug("invalid manifest key");
    143      return null;
    144    }
    145    // Return the key string if it is not a locale key.
    146    // The key string starts with "__MSG_" and ends with "__".
    147    // For example, "__MSG_name__".
    148    // https://developer.chrome.com/apps/i18n
    149    if (!key.startsWith("__MSG_") || !key.endsWith("__")) {
    150      return key;
    151    }
    152 
    153    let localeString = null;
    154    try {
    155      let localeFile;
    156      if (
    157        this._extensionLocaleStrings[profileId] &&
    158        this._extensionLocaleStrings[profileId][extensionId]
    159      ) {
    160        localeFile = this._extensionLocaleStrings[profileId][extensionId];
    161      } else {
    162        if (!this._extensionLocaleStrings[profileId]) {
    163          this._extensionLocaleStrings[profileId] = {};
    164        }
    165        let localeFilePath = await this.getExtensionPath(profileId);
    166        localeFilePath = PathUtils.join(localeFilePath, extensionId);
    167        let directories =
    168          await this._getSortedByVersionSubDirectoryNames(localeFilePath);
    169        // If there are multiple sub-directories in the extension directory,
    170        // read the files in the latest directory.
    171        localeFilePath = PathUtils.join(
    172          localeFilePath,
    173          directories[0],
    174          "_locales",
    175          locale,
    176          "messages.json"
    177        );
    178        localeFile = await IOUtils.readJSON(localeFilePath);
    179        this._extensionLocaleStrings[profileId][extensionId] = localeFile;
    180      }
    181      const PREFIX_LENGTH = 6;
    182      const SUFFIX_LENGTH = 2;
    183      // Get the locale key from the string with locale prefix and suffix.
    184      // For example, it will get the "name" sub-string from the "__MSG_name__" string.
    185      key = key.substring(PREFIX_LENGTH, key.length - SUFFIX_LENGTH);
    186      if (localeFile[key] && localeFile[key].message) {
    187        localeString = localeFile[key].message;
    188      }
    189    } catch (ex) {
    190      console.error(ex);
    191    }
    192    return localeString;
    193  },
    194 
    195  /**
    196   * Check that a specific extension is installed or not.
    197   *
    198   * @param {string} extensionId - The extension ID.
    199   * @param {string} profileId - The user profile's ID.
    200   * @returns {boolean} Return true if the extension is installed otherwise return false.
    201   */
    202  async isExtensionInstalled(extensionId, profileId) {
    203    if (profileId === undefined) {
    204      profileId = await this.getLastUsedProfileId();
    205    }
    206    let extensionPath = await this.getExtensionPath(profileId);
    207    let isInstalled = await IOUtils.exists(
    208      PathUtils.join(extensionPath, extensionId)
    209    );
    210    return isInstalled;
    211  },
    212 
    213  /**
    214   * Get the last used user profile's ID.
    215   *
    216   * @returns {string} The last used user profile's ID.
    217   */
    218  async getLastUsedProfileId() {
    219    let localState = await this.getLocalState();
    220    return localState ? localState.profile.last_used : "Default";
    221  },
    222 
    223  /**
    224   * Get the local state file content.
    225   *
    226   * @param {string} chromeProjectName
    227   *   The type of Chrome data we're looking for (Chromium, Canary, etc.)
    228   * @param {string} [dataPath=undefined]
    229   *   The data path that should be used as the parent directory when getting
    230   *   the local state. If not supplied, the data path is calculated using
    231   *   getDataPath and the chromeProjectName.
    232   * @returns {object} The JSON-based content.
    233   */
    234  async getLocalState(chromeProjectName = "Chrome", dataPath) {
    235    let localState = null;
    236    try {
    237      if (!dataPath) {
    238        dataPath = await this.getDataPath(chromeProjectName);
    239      }
    240      let localStatePath = PathUtils.join(dataPath, "Local State");
    241      localState = JSON.parse(await IOUtils.readUTF8(localStatePath));
    242    } catch (ex) {
    243      // Don't report the error if it's just a file not existing.
    244      if (ex.name != "NotFoundError") {
    245        console.error(ex);
    246      }
    247      throw ex;
    248    }
    249    return localState;
    250  },
    251 
    252  /**
    253   * Get the path of Chrome extension directory.
    254   *
    255   * @param {string} profileId - The user profile's ID.
    256   * @returns {string} The path of Chrome extension directory.
    257   */
    258  async getExtensionPath(profileId) {
    259    return PathUtils.join(await this.getDataPath(), profileId, "Extensions");
    260  },
    261 
    262  /**
    263   * Get the path of an application data directory.
    264   *
    265   * @param {string} chromeProjectName - The Chrome project name, e.g. "Chrome", "Canary", etc.
    266   *                                     Defaults to "Chrome".
    267   * @returns {string} The path of application data directory.
    268   */
    269  async getDataPath(chromeProjectName = "Chrome") {
    270    const SNAP_REAL_HOME = "SNAP_REAL_HOME";
    271 
    272    const SUB_DIRECTORIES = {
    273      win: {
    274        Brave: [
    275          ["LocalAppData", "BraveSoftware", "Brave-Browser", "User Data"],
    276        ],
    277        Chrome: [["LocalAppData", "Google", "Chrome", "User Data"]],
    278        "Chrome Beta": [["LocalAppData", "Google", "Chrome Beta", "User Data"]],
    279        Chromium: [["LocalAppData", "Chromium", "User Data"]],
    280        Canary: [["LocalAppData", "Google", "Chrome SxS", "User Data"]],
    281        Edge: [["LocalAppData", "Microsoft", "Edge", "User Data"]],
    282        "Edge Beta": [["LocalAppData", "Microsoft", "Edge Beta", "User Data"]],
    283        "360 SE": [["AppData", "360se6", "User Data"]],
    284        Opera: [["AppData", "Opera Software", "Opera Stable"]],
    285        "Opera GX": [["AppData", "Opera Software", "Opera GX Stable"]],
    286        Vivaldi: [["LocalAppData", "Vivaldi", "User Data"]],
    287      },
    288      macosx: {
    289        Brave: [
    290          ["ULibDir", "Application Support", "BraveSoftware", "Brave-Browser"],
    291        ],
    292        Chrome: [["ULibDir", "Application Support", "Google", "Chrome"]],
    293        Chromium: [["ULibDir", "Application Support", "Chromium"]],
    294        Canary: [["ULibDir", "Application Support", "Google", "Chrome Canary"]],
    295        Edge: [["ULibDir", "Application Support", "Microsoft Edge"]],
    296        "Edge Beta": [
    297          ["ULibDir", "Application Support", "Microsoft Edge Beta"],
    298        ],
    299        "Opera GX": [
    300          ["ULibDir", "Application Support", "com.operasoftware.OperaGX"],
    301        ],
    302        Opera: [["ULibDir", "Application Support", "com.operasoftware.Opera"]],
    303        Vivaldi: [["ULibDir", "Application Support", "Vivaldi"]],
    304      },
    305      linux: {
    306        Brave: [["Home", ".config", "BraveSoftware", "Brave-Browser"]],
    307        Chrome: [["Home", ".config", "google-chrome"]],
    308        "Chrome Beta": [["Home", ".config", "google-chrome-beta"]],
    309        "Chrome Dev": [["Home", ".config", "google-chrome-unstable"]],
    310        Chromium: [
    311          ["Home", ".config", "chromium"],
    312 
    313          // If we're installed normally, we can look for Chromium installed
    314          // as a Snap on Ubuntu Linux by looking here.
    315          ["Home", "snap", "chromium", "common", "chromium"],
    316 
    317          // If we're installed as a Snap, "Home" is a special place that
    318          // the Snap environment has given us, and the Chromium data is
    319          // not within it. We want to, instead, start at the path set
    320          // on the environment variable "SNAP_REAL_HOME".
    321          // See: https://snapcraft.io/docs/environment-variables#heading--snap-real-home
    322          [SNAP_REAL_HOME, "snap", "chromium", "common", "chromium"],
    323        ],
    324        // Opera GX is not available on Linux.
    325        // Canary is not available on Linux.
    326        // Edge is not available on Linux.
    327        Opera: [["Home", ".config", "opera"]],
    328        Vivaldi: [["Home", ".config", "vivaldi"]],
    329      },
    330    };
    331    let options = SUB_DIRECTORIES[AppConstants.platform][chromeProjectName];
    332    if (!options) {
    333      return null;
    334    }
    335 
    336    for (let subfolders of options) {
    337      let rootDir = subfolders[0];
    338      try {
    339        let targetPath;
    340 
    341        if (rootDir == SNAP_REAL_HOME) {
    342          targetPath = Services.env.get("SNAP_REAL_HOME");
    343        } else if (rootDir === "Home" && Services.env.get("BB_ORIGINAL_HOME")) {
    344          targetPath = Services.env.get("BB_ORIGINAL_HOME");
    345        } else {
    346          targetPath = Services.dirsvc.get(rootDir, Ci.nsIFile).path;
    347        }
    348 
    349        targetPath = PathUtils.join(targetPath, ...subfolders.slice(1));
    350        if (await IOUtils.exists(targetPath)) {
    351          return targetPath;
    352        }
    353      } catch (ex) {
    354        // The path logic here shouldn't error, so log it:
    355        console.error(ex);
    356      }
    357    }
    358    return null;
    359  },
    360 
    361  /**
    362   * Get the directory objects sorted by version number.
    363   *
    364   * @param {string} path - The path to the extension directory.
    365   * otherwise return all file/directory object.
    366   * @returns {Array} The file/directory object array.
    367   */
    368  async _getSortedByVersionSubDirectoryNames(path) {
    369    if (this._extensionVersionDirectoryNames[path]) {
    370      return this._extensionVersionDirectoryNames[path];
    371    }
    372 
    373    let entries = [];
    374    try {
    375      for (const child of await IOUtils.getChildren(path)) {
    376        const info = await IOUtils.stat(child);
    377        if (info.type === "directory") {
    378          const name = PathUtils.filename(child);
    379          entries.push(name);
    380        }
    381      }
    382    } catch (ex) {
    383      console.error(ex);
    384      entries = [];
    385    }
    386 
    387    // The directory name is the version number string of the extension.
    388    // For example, it could be "1.0_0", "1.0_1", "1.0_2", 1.1_0, 1.1_1, or 1.1_2.
    389    // The "1.0_1" strings mean that the "1.0_0" directory is existed and you install the version 1.0 again.
    390    // https://chromium.googlesource.com/chromium/src/+/0b58a813992b539a6b555ad7959adfad744b095a/chrome/common/extensions/extension_file_util_unittest.cc
    391    entries.sort((a, b) => Services.vc.compare(b, a));
    392 
    393    this._extensionVersionDirectoryNames[path] = entries;
    394    return entries;
    395  },
    396 
    397  /**
    398   * Convert Chrome time format to Date object. Google Chrome uses FILETIME / 10 as time.
    399   * FILETIME is based on the same structure of Windows.
    400   *
    401   * @param {number} aTime Chrome time
    402   * @param {string|number|Date} aFallbackValue a date or timestamp (valid argument
    403   *   for the Date constructor) that will be used if the chrometime value passed is
    404   *   invalid.
    405   * @returns {Date} converted Date object
    406   */
    407  chromeTimeToDate(aTime, aFallbackValue) {
    408    // The date value may be 0 in some cases. Because of the subtraction below,
    409    // that'd generate a date before the unix epoch, which can upset consumers
    410    // due to the unix timestamp then being negative. Catch this case:
    411    if (!aTime) {
    412      return new Date(aFallbackValue);
    413    }
    414    return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
    415  },
    416 
    417  /**
    418   * Convert Date object to Chrome time format. For details on Chrome time, see
    419   * chromeTimeToDate.
    420   *
    421   * @param {Date|number} aDate Date object or integer equivalent
    422   * @returns {number} Chrome time
    423   */
    424  dateToChromeTime(aDate) {
    425    return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
    426  },
    427 
    428  /**
    429   * Returns an array of chromium browser ids that have importable logins.
    430   */
    431  _importableLoginsCache: null,
    432  async getImportableLogins(formOrigin) {
    433    // Only provide importable if we actually support importing.
    434    if (!this.supportsLoginsForPlatform) {
    435      return undefined;
    436    }
    437 
    438    // Lazily fill the cache with all importable login browsers.
    439    if (!this._importableLoginsCache) {
    440      this._importableLoginsCache = new Map();
    441 
    442      // Just handle these chromium-based browsers for now.
    443      for (const browserId of this.CONTEXTUAL_LOGIN_IMPORT_BROWSERS) {
    444        // Skip if there's no profile data.
    445        const migrator = await lazy.MigrationUtils.getMigrator(browserId);
    446        if (!migrator) {
    447          continue;
    448        }
    449 
    450        // Check each profile for logins.
    451        const dataPath = await migrator._getChromeUserDataPathIfExists();
    452        for (const profile of await migrator.getSourceProfiles()) {
    453          const path = PathUtils.join(dataPath, profile.id, "Login Data");
    454          // Skip if login data is missing.
    455          if (!(await IOUtils.exists(path))) {
    456            console.error(`Missing file at ${path}`);
    457            continue;
    458          }
    459 
    460          try {
    461            for (const row of await lazy.MigrationUtils.getRowsFromDBWithoutLocks(
    462              path,
    463              `Importable ${browserId} logins`,
    464              `SELECT origin_url
    465               FROM logins
    466               WHERE blacklisted_by_user = 0`
    467            )) {
    468              const url = row.getString(0);
    469              try {
    470                // Initialize an array if it doesn't exist for the origin yet.
    471                const origin = lazy.LoginHelper.getLoginOrigin(url);
    472                const entries = this._importableLoginsCache.get(origin) || [];
    473                if (!entries.length) {
    474                  this._importableLoginsCache.set(origin, entries);
    475                }
    476 
    477                // Add the browser if it doesn't exist yet.
    478                if (!entries.includes(browserId)) {
    479                  entries.push(browserId);
    480                }
    481              } catch (ex) {
    482                console.error(
    483                  `Failed to process importable url ${url} from ${browserId}`,
    484                  ex
    485                );
    486              }
    487            }
    488          } catch (ex) {
    489            console.error(
    490              `Failed to get importable logins from ${browserId}`,
    491              ex
    492            );
    493          }
    494        }
    495      }
    496    }
    497    return this._importableLoginsCache.get(formOrigin);
    498  },
    499 };