forked from mirrors/gecko-dev
This goes through the previous changes in the dependencies of bug 877389, and does two things: 1) Remove instances of \n 2) Change reporting of exceptions so that they are passed as separate arguments. This should result in an improved display of the exception in the browser console, should it occur. Differential Revision: https://phabricator.services.mozilla.com/D180843
471 lines
16 KiB
JavaScript
471 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 { 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);
|
|
},
|
|
};
|