forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			474 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			474 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						|
 | 
						|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetters(lazy, {
 | 
						|
  LoginHelper: "resource://gre/modules/LoginHelper.jsm",
 | 
						|
});
 | 
						|
 | 
						|
const S100NS_FROM1601TO1970 = 0x19db1ded53e8000;
 | 
						|
const S100NS_PER_MS = 10;
 | 
						|
 | 
						|
export var ChromeMigrationUtils = {
 | 
						|
  // Supported browsers with importable logins.
 | 
						|
  CONTEXTUAL_LOGIN_IMPORT_BROWSERS: ["chrome", "chromium-edge", "chromium"],
 | 
						|
 | 
						|
  _extensionVersionDirectoryNames: {},
 | 
						|
 | 
						|
  // The cache for the locale strings.
 | 
						|
  // For example, the data could be:
 | 
						|
  // {
 | 
						|
  //   "profile-id-1": {
 | 
						|
  //     "extension-id-1": {
 | 
						|
  //       "name": {
 | 
						|
  //         "message": "Fake App 1"
 | 
						|
  //       }
 | 
						|
  //   },
 | 
						|
  // }
 | 
						|
  _extensionLocaleStrings: {},
 | 
						|
 | 
						|
  get supportsLoginsForPlatform() {
 | 
						|
    return ["macosx", "win"].includes(AppConstants.platform);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get all extensions installed in a specific profile.
 | 
						|
   *
 | 
						|
   * @param {string} profileId - A Chrome user profile ID. For example, "Profile 1".
 | 
						|
   * @returns {Array} All installed Chrome extensions information.
 | 
						|
   */
 | 
						|
  async getExtensionList(profileId) {
 | 
						|
    if (profileId === undefined) {
 | 
						|
      profileId = await this.getLastUsedProfileId();
 | 
						|
    }
 | 
						|
    let path = await this.getExtensionPath(profileId);
 | 
						|
    let extensionList = [];
 | 
						|
    try {
 | 
						|
      for (const child of await IOUtils.getChildren(path)) {
 | 
						|
        const info = await IOUtils.stat(child);
 | 
						|
        if (info.type === "directory") {
 | 
						|
          const name = PathUtils.filename(child);
 | 
						|
          let extensionInformation = await this.getExtensionInformation(
 | 
						|
            name,
 | 
						|
            profileId
 | 
						|
          );
 | 
						|
          if (extensionInformation) {
 | 
						|
            extensionList.push(extensionInformation);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } catch (ex) {
 | 
						|
      console.error(ex);
 | 
						|
    }
 | 
						|
    return extensionList;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get information of a specific Chrome extension.
 | 
						|
   *
 | 
						|
   * @param {string} extensionId - The extension ID.
 | 
						|
   * @param {string} profileId - The user profile's ID.
 | 
						|
   * @returns {object} The Chrome extension information.
 | 
						|
   */
 | 
						|
  async getExtensionInformation(extensionId, profileId) {
 | 
						|
    if (profileId === undefined) {
 | 
						|
      profileId = await this.getLastUsedProfileId();
 | 
						|
    }
 | 
						|
    let extensionInformation = null;
 | 
						|
    try {
 | 
						|
      let manifestPath = await this.getExtensionPath(profileId);
 | 
						|
      manifestPath = PathUtils.join(manifestPath, extensionId);
 | 
						|
      // If there are multiple sub-directories in the extension directory,
 | 
						|
      // read the files in the latest directory.
 | 
						|
      let directories = await this._getSortedByVersionSubDirectoryNames(
 | 
						|
        manifestPath
 | 
						|
      );
 | 
						|
      if (!directories[0]) {
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
 | 
						|
      manifestPath = PathUtils.join(
 | 
						|
        manifestPath,
 | 
						|
        directories[0],
 | 
						|
        "manifest.json"
 | 
						|
      );
 | 
						|
      let manifest = await IOUtils.readJSON(manifestPath);
 | 
						|
      // No app attribute means this is a Chrome extension not a Chrome app.
 | 
						|
      if (!manifest.app) {
 | 
						|
        const DEFAULT_LOCALE = manifest.default_locale;
 | 
						|
        let name = await this._getLocaleString(
 | 
						|
          manifest.name,
 | 
						|
          DEFAULT_LOCALE,
 | 
						|
          extensionId,
 | 
						|
          profileId
 | 
						|
        );
 | 
						|
        let description = await this._getLocaleString(
 | 
						|
          manifest.description,
 | 
						|
          DEFAULT_LOCALE,
 | 
						|
          extensionId,
 | 
						|
          profileId
 | 
						|
        );
 | 
						|
        if (name) {
 | 
						|
          extensionInformation = {
 | 
						|
            id: extensionId,
 | 
						|
            name,
 | 
						|
            description,
 | 
						|
          };
 | 
						|
        } else {
 | 
						|
          throw new Error("Cannot read the Chrome extension's name property.");
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } catch (ex) {
 | 
						|
      console.error(ex);
 | 
						|
    }
 | 
						|
    return extensionInformation;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the manifest's locale string.
 | 
						|
   *
 | 
						|
   * @param {string} key - The key of a locale string, for example __MSG_name__.
 | 
						|
   * @param {string} locale - The specific language of locale string.
 | 
						|
   * @param {string} extensionId - The extension ID.
 | 
						|
   * @param {string} profileId - The user profile's ID.
 | 
						|
   * @returns {string} The locale string.
 | 
						|
   */
 | 
						|
  async _getLocaleString(key, locale, extensionId, profileId) {
 | 
						|
    // Return the key string if it is not a locale key.
 | 
						|
    // The key string starts with "__MSG_" and ends with "__".
 | 
						|
    // For example, "__MSG_name__".
 | 
						|
    // https://developer.chrome.com/apps/i18n
 | 
						|
    if (!key.startsWith("__MSG_") || !key.endsWith("__")) {
 | 
						|
      return key;
 | 
						|
    }
 | 
						|
 | 
						|
    let localeString = null;
 | 
						|
    try {
 | 
						|
      let localeFile;
 | 
						|
      if (
 | 
						|
        this._extensionLocaleStrings[profileId] &&
 | 
						|
        this._extensionLocaleStrings[profileId][extensionId]
 | 
						|
      ) {
 | 
						|
        localeFile = this._extensionLocaleStrings[profileId][extensionId];
 | 
						|
      } else {
 | 
						|
        if (!this._extensionLocaleStrings[profileId]) {
 | 
						|
          this._extensionLocaleStrings[profileId] = {};
 | 
						|
        }
 | 
						|
        let localeFilePath = await this.getExtensionPath(profileId);
 | 
						|
        localeFilePath = PathUtils.join(localeFilePath, extensionId);
 | 
						|
        let directories = await this._getSortedByVersionSubDirectoryNames(
 | 
						|
          localeFilePath
 | 
						|
        );
 | 
						|
        // If there are multiple sub-directories in the extension directory,
 | 
						|
        // read the files in the latest directory.
 | 
						|
        localeFilePath = PathUtils.join(
 | 
						|
          localeFilePath,
 | 
						|
          directories[0],
 | 
						|
          "_locales",
 | 
						|
          locale,
 | 
						|
          "messages.json"
 | 
						|
        );
 | 
						|
        localeFile = await IOUtils.readJSON(localeFilePath);
 | 
						|
        this._extensionLocaleStrings[profileId][extensionId] = localeFile;
 | 
						|
      }
 | 
						|
      const PREFIX_LENGTH = 6;
 | 
						|
      const SUFFIX_LENGTH = 2;
 | 
						|
      // Get the locale key from the string with locale prefix and suffix.
 | 
						|
      // For example, it will get the "name" sub-string from the "__MSG_name__" string.
 | 
						|
      key = key.substring(PREFIX_LENGTH, key.length - SUFFIX_LENGTH);
 | 
						|
      if (localeFile[key] && localeFile[key].message) {
 | 
						|
        localeString = localeFile[key].message;
 | 
						|
      }
 | 
						|
    } catch (ex) {
 | 
						|
      console.error(ex);
 | 
						|
    }
 | 
						|
    return localeString;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Check that a specific extension is installed or not.
 | 
						|
   *
 | 
						|
   * @param {string} extensionId - The extension ID.
 | 
						|
   * @param {string} profileId - The user profile's ID.
 | 
						|
   * @returns {boolean} Return true if the extension is installed otherwise return false.
 | 
						|
   */
 | 
						|
  async isExtensionInstalled(extensionId, profileId) {
 | 
						|
    if (profileId === undefined) {
 | 
						|
      profileId = await this.getLastUsedProfileId();
 | 
						|
    }
 | 
						|
    let extensionPath = await this.getExtensionPath(profileId);
 | 
						|
    let isInstalled = await IOUtils.exists(
 | 
						|
      PathUtils.join(extensionPath, extensionId)
 | 
						|
    );
 | 
						|
    return isInstalled;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the last used user profile's ID.
 | 
						|
   *
 | 
						|
   * @returns {string} The last used user profile's ID.
 | 
						|
   */
 | 
						|
  async getLastUsedProfileId() {
 | 
						|
    let localState = await this.getLocalState();
 | 
						|
    return localState ? localState.profile.last_used : "Default";
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the local state file content.
 | 
						|
   *
 | 
						|
   * @param {string} dataPath the type of Chrome data we're looking for (Chromium, Canary, etc.)
 | 
						|
   * @returns {object} The JSON-based content.
 | 
						|
   */
 | 
						|
  async getLocalState(dataPath = "Chrome") {
 | 
						|
    let localState = null;
 | 
						|
    try {
 | 
						|
      let localStatePath = PathUtils.join(
 | 
						|
        await this.getDataPath(dataPath),
 | 
						|
        "Local State"
 | 
						|
      );
 | 
						|
      localState = JSON.parse(await IOUtils.readUTF8(localStatePath));
 | 
						|
    } catch (ex) {
 | 
						|
      // Don't report the error if it's just a file not existing.
 | 
						|
      if (ex.name != "NotFoundError") {
 | 
						|
        console.error(ex);
 | 
						|
      }
 | 
						|
      throw ex;
 | 
						|
    }
 | 
						|
    return localState;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the path of Chrome extension directory.
 | 
						|
   *
 | 
						|
   * @param {string} profileId - The user profile's ID.
 | 
						|
   * @returns {string} The path of Chrome extension directory.
 | 
						|
   */
 | 
						|
  async getExtensionPath(profileId) {
 | 
						|
    return PathUtils.join(await this.getDataPath(), profileId, "Extensions");
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the path of an application data directory.
 | 
						|
   *
 | 
						|
   * @param {string} chromeProjectName - The Chrome project name, e.g. "Chrome", "Canary", etc.
 | 
						|
   *                                     Defaults to "Chrome".
 | 
						|
   * @returns {string} The path of application data directory.
 | 
						|
   */
 | 
						|
  async getDataPath(chromeProjectName = "Chrome") {
 | 
						|
    const SUB_DIRECTORIES = {
 | 
						|
      win: {
 | 
						|
        Brave: [
 | 
						|
          ["LocalAppData", "BraveSoftware", "Brave-Browser", "User Data"],
 | 
						|
        ],
 | 
						|
        Chrome: [["LocalAppData", "Google", "Chrome", "User Data"]],
 | 
						|
        "Chrome Beta": [["LocalAppData", "Google", "Chrome Beta", "User Data"]],
 | 
						|
        Chromium: [["LocalAppData", "Chromium", "User Data"]],
 | 
						|
        Canary: [["LocalAppData", "Google", "Chrome SxS", "User Data"]],
 | 
						|
        Edge: [["LocalAppData", "Microsoft", "Edge", "User Data"]],
 | 
						|
        "Edge Beta": [["LocalAppData", "Microsoft", "Edge Beta", "User Data"]],
 | 
						|
        "360 SE": [["AppData", "360se6", "User Data"]],
 | 
						|
        Opera: [["AppData", "Opera Software", "Opera Stable"]],
 | 
						|
        "Opera GX": [["AppData", "Opera Software", "Opera GX Stable"]],
 | 
						|
        Vivaldi: [["LocalAppData", "Vivaldi", "User Data"]],
 | 
						|
      },
 | 
						|
      macosx: {
 | 
						|
        Brave: [
 | 
						|
          ["ULibDir", "Application Support", "BraveSoftware", "Brave-Browser"],
 | 
						|
        ],
 | 
						|
        Chrome: [["ULibDir", "Application Support", "Google", "Chrome"]],
 | 
						|
        Chromium: [["ULibDir", "Application Support", "Chromium"]],
 | 
						|
        Canary: [["ULibDir", "Application Support", "Google", "Chrome Canary"]],
 | 
						|
        Edge: [["ULibDir", "Application Support", "Microsoft Edge"]],
 | 
						|
        "Edge Beta": [
 | 
						|
          ["ULibDir", "Application Support", "Microsoft Edge Beta"],
 | 
						|
        ],
 | 
						|
        "Opera GX": [
 | 
						|
          ["ULibDir", "Application Support", "com.operasoftware.OperaGX"],
 | 
						|
        ],
 | 
						|
        Opera: [["ULibDir", "Application Support", "com.operasoftware.Opera"]],
 | 
						|
        Vivaldi: [["ULibDir", "Application Support", "Vivaldi"]],
 | 
						|
      },
 | 
						|
      linux: {
 | 
						|
        Brave: [["Home", ".config", "BraveSoftware", "Brave-Browser"]],
 | 
						|
        Chrome: [["Home", ".config", "google-chrome"]],
 | 
						|
        "Chrome Beta": [["Home", ".config", "google-chrome-beta"]],
 | 
						|
        "Chrome Dev": [["Home", ".config", "google-chrome-unstable"]],
 | 
						|
        Chromium: [
 | 
						|
          ["Home", ".config", "chromium"],
 | 
						|
          ["Home", "snap", "chromium", "common", "chromium"],
 | 
						|
        ],
 | 
						|
        // Opera GX is not available on Linux.
 | 
						|
        // Canary is not available on Linux.
 | 
						|
        // Edge is not available on Linux.
 | 
						|
        Opera: [["Home", ".config", "opera"]],
 | 
						|
        Vivaldi: [["Home", ".config", "vivaldi"]],
 | 
						|
      },
 | 
						|
    };
 | 
						|
    let options = SUB_DIRECTORIES[AppConstants.platform][chromeProjectName];
 | 
						|
    if (!options) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let subfolders of options) {
 | 
						|
      let rootDir = subfolders[0];
 | 
						|
      try {
 | 
						|
        let targetPath = Services.dirsvc.get(rootDir, Ci.nsIFile).path;
 | 
						|
        targetPath = PathUtils.join(targetPath, ...subfolders.slice(1));
 | 
						|
        if (await IOUtils.exists(targetPath)) {
 | 
						|
          return targetPath;
 | 
						|
        }
 | 
						|
      } catch (ex) {
 | 
						|
        // The path logic here shouldn't error, so log it:
 | 
						|
        console.error(ex);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get the directory objects sorted by version number.
 | 
						|
   *
 | 
						|
   * @param {string} path - The path to the extension directory.
 | 
						|
   * otherwise return all file/directory object.
 | 
						|
   * @returns {Array} The file/directory object array.
 | 
						|
   */
 | 
						|
  async _getSortedByVersionSubDirectoryNames(path) {
 | 
						|
    if (this._extensionVersionDirectoryNames[path]) {
 | 
						|
      return this._extensionVersionDirectoryNames[path];
 | 
						|
    }
 | 
						|
 | 
						|
    let entries = [];
 | 
						|
    try {
 | 
						|
      for (const child of await IOUtils.getChildren(path)) {
 | 
						|
        const info = await IOUtils.stat(child);
 | 
						|
        if (info.type === "directory") {
 | 
						|
          const name = PathUtils.filename(child);
 | 
						|
          entries.push(name);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } catch (ex) {
 | 
						|
      console.error(ex);
 | 
						|
      entries = [];
 | 
						|
    }
 | 
						|
 | 
						|
    // The directory name is the version number string of the extension.
 | 
						|
    // For example, it could be "1.0_0", "1.0_1", "1.0_2", 1.1_0, 1.1_1, or 1.1_2.
 | 
						|
    // The "1.0_1" strings mean that the "1.0_0" directory is existed and you install the version 1.0 again.
 | 
						|
    // https://chromium.googlesource.com/chromium/src/+/0b58a813992b539a6b555ad7959adfad744b095a/chrome/common/extensions/extension_file_util_unittest.cc
 | 
						|
    entries.sort((a, b) => Services.vc.compare(b, a));
 | 
						|
 | 
						|
    this._extensionVersionDirectoryNames[path] = entries;
 | 
						|
    return entries;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Convert Chrome time format to Date object. Google Chrome uses FILETIME / 10 as time.
 | 
						|
   * FILETIME is based on the same structure of Windows.
 | 
						|
   *
 | 
						|
   * @param {number} aTime Chrome time
 | 
						|
   * @param {string|number|Date} aFallbackValue a date or timestamp (valid argument
 | 
						|
   *   for the Date constructor) that will be used if the chrometime value passed is
 | 
						|
   *   invalid.
 | 
						|
   * @returns {Date} converted Date object
 | 
						|
   */
 | 
						|
  chromeTimeToDate(aTime, aFallbackValue) {
 | 
						|
    // The date value may be 0 in some cases. Because of the subtraction below,
 | 
						|
    // that'd generate a date before the unix epoch, which can upset consumers
 | 
						|
    // due to the unix timestamp then being negative. Catch this case:
 | 
						|
    if (!aTime) {
 | 
						|
      return new Date(aFallbackValue);
 | 
						|
    }
 | 
						|
    return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Convert Date object to Chrome time format. For details on Chrome time, see
 | 
						|
   * chromeTimeToDate.
 | 
						|
   *
 | 
						|
   * @param {Date|number} aDate Date object or integer equivalent
 | 
						|
   * @returns {number} Chrome time
 | 
						|
   */
 | 
						|
  dateToChromeTime(aDate) {
 | 
						|
    return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns an array of chromium browser ids that have importable logins.
 | 
						|
   */
 | 
						|
  _importableLoginsCache: null,
 | 
						|
  async getImportableLogins(formOrigin) {
 | 
						|
    // Only provide importable if we actually support importing.
 | 
						|
    if (!this.supportsLoginsForPlatform) {
 | 
						|
      return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    // Lazily fill the cache with all importable login browsers.
 | 
						|
    if (!this._importableLoginsCache) {
 | 
						|
      this._importableLoginsCache = new Map();
 | 
						|
 | 
						|
      // Just handle these chromium-based browsers for now.
 | 
						|
      for (const browserId of this.CONTEXTUAL_LOGIN_IMPORT_BROWSERS) {
 | 
						|
        // Skip if there's no profile data.
 | 
						|
        const migrator = await lazy.MigrationUtils.getMigrator(browserId);
 | 
						|
        if (!migrator) {
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        // Check each profile for logins.
 | 
						|
        const dataPath = await migrator._getChromeUserDataPathIfExists();
 | 
						|
        for (const profile of await migrator.getSourceProfiles()) {
 | 
						|
          const path = PathUtils.join(dataPath, profile.id, "Login Data");
 | 
						|
          // Skip if login data is missing.
 | 
						|
          if (!(await IOUtils.exists(path))) {
 | 
						|
            console.error(`Missing file at ${path}`);
 | 
						|
            continue;
 | 
						|
          }
 | 
						|
 | 
						|
          try {
 | 
						|
            for (const row of await lazy.MigrationUtils.getRowsFromDBWithoutLocks(
 | 
						|
              path,
 | 
						|
              `Importable ${browserId} logins`,
 | 
						|
              `SELECT origin_url
 | 
						|
               FROM logins
 | 
						|
               WHERE blacklisted_by_user = 0`
 | 
						|
            )) {
 | 
						|
              const url = row.getString(0);
 | 
						|
              try {
 | 
						|
                // Initialize an array if it doesn't exist for the origin yet.
 | 
						|
                const origin = lazy.LoginHelper.getLoginOrigin(url);
 | 
						|
                const entries = this._importableLoginsCache.get(origin) || [];
 | 
						|
                if (!entries.length) {
 | 
						|
                  this._importableLoginsCache.set(origin, entries);
 | 
						|
                }
 | 
						|
 | 
						|
                // Add the browser if it doesn't exist yet.
 | 
						|
                if (!entries.includes(browserId)) {
 | 
						|
                  entries.push(browserId);
 | 
						|
                }
 | 
						|
              } catch (ex) {
 | 
						|
                console.error(
 | 
						|
                  `Failed to process importable url ${url} from ${browserId} ${ex}`
 | 
						|
                );
 | 
						|
              }
 | 
						|
            }
 | 
						|
          } catch (ex) {
 | 
						|
            console.error(
 | 
						|
              `Failed to get importable logins from ${browserId} ${ex}`
 | 
						|
            );
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return this._importableLoginsCache.get(formOrigin);
 | 
						|
  },
 | 
						|
};
 |