summaryrefslogtreecommitdiffstats
path: root/browser/components/migration/ChromeMigrationUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration/ChromeMigrationUtils.sys.mjs')
-rw-r--r--browser/components/migration/ChromeMigrationUtils.sys.mjs469
1 files changed, 469 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..99a8987bef
--- /dev/null
+++ b/browser/components/migration/ChromeMigrationUtils.sys.mjs
@@ -0,0 +1,469 @@
+/* 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} 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);
+ },
+};