diff options
Diffstat (limited to 'browser/components/migration/ChromeMigrationUtils.sys.mjs')
-rw-r--r-- | browser/components/migration/ChromeMigrationUtils.sys.mjs | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/browser/components/migration/ChromeMigrationUtils.sys.mjs b/browser/components/migration/ChromeMigrationUtils.sys.mjs new file mode 100644 index 0000000000..88ae3addfc --- /dev/null +++ b/browser/components/migration/ChromeMigrationUtils.sys.mjs @@ -0,0 +1,499 @@ +/* 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", +}); + +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|null} The locale string. + */ + async _getLocaleString(key, locale, extensionId, profileId) { + if (typeof key !== "string") { + console.debug("invalid manifest key"); + return null; + } + // 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} chromeProjectName + * The type of Chrome data we're looking for (Chromium, Canary, etc.) + * @param {string} [dataPath=undefined] + * The data path that should be used as the parent directory when getting + * the local state. If not supplied, the data path is calculated using + * getDataPath and the chromeProjectName. + * @returns {object} The JSON-based content. + */ + async getLocalState(chromeProjectName = "Chrome", dataPath) { + let localState = null; + try { + if (!dataPath) { + dataPath = await this.getDataPath(chromeProjectName); + } + let localStatePath = PathUtils.join(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 SNAP_REAL_HOME = "SNAP_REAL_HOME"; + + 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"], + + // If we're installed normally, we can look for Chromium installed + // as a Snap on Ubuntu Linux by looking here. + ["Home", "snap", "chromium", "common", "chromium"], + + // If we're installed as a Snap, "Home" is a special place that + // the Snap environment has given us, and the Chromium data is + // not within it. We want to, instead, start at the path set + // on the environment variable "SNAP_REAL_HOME". + // See: https://snapcraft.io/docs/environment-variables#heading--snap-real-home + [SNAP_REAL_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; + + if (rootDir == SNAP_REAL_HOME) { + targetPath = Services.env.get("SNAP_REAL_HOME"); + } else { + 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); + }, +}; |