Mypal68/browser/components/migration/ChromeProfileMigrator.jsm
2024-11-25 17:24:41 +02:00

581 lines
18 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/. */
"use strict";
const AUTH_TYPE = {
SCHEME_HTML: 0,
SCHEME_BASIC: 1,
SCHEME_DIGEST: 2,
};
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { ChromeMigrationUtils } = ChromeUtils.import(
"resource:///modules/ChromeMigrationUtils.jsm"
);
const { MigrationUtils, MigratorPrototype } = ChromeUtils.import(
"resource:///modules/MigrationUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"OSCrypto",
"resource://gre/modules/OSCrypto.jsm"
);
/**
* Converts an array of chrome bookmark objects into one our own places code
* understands.
*
* @param items
* bookmark items to be inserted on this parent
* @param errorAccumulator
* function that gets called with any errors thrown so we don't drop them on the floor.
*/
function convertBookmarks(items, errorAccumulator) {
let itemsToInsert = [];
for (let item of items) {
try {
if (item.type == "url") {
if (item.url.trim().startsWith("chrome:")) {
// Skip invalid chrome URIs. Creating an actual URI always reports
// messages to the console, so we avoid doing that.
continue;
}
itemsToInsert.push({ url: item.url, title: item.name });
} else if (item.type == "folder") {
let folderItem = {
type: PlacesUtils.bookmarks.TYPE_FOLDER,
title: item.name,
};
folderItem.children = convertBookmarks(item.children, errorAccumulator);
itemsToInsert.push(folderItem);
}
} catch (ex) {
Cu.reportError(ex);
errorAccumulator(ex);
}
}
return itemsToInsert;
}
function ChromeProfileMigrator() {
this._chromeUserDataPathSuffix = "Chrome";
}
ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
ChromeProfileMigrator.prototype._getChromeUserDataPathIfExists = async function() {
if (this._chromeUserDataPath) {
return this._chromeUserDataPath;
}
let path = ChromeMigrationUtils.getDataPath(this._chromeUserDataPathSuffix);
let exists = await OS.File.exists(path);
if (exists) {
this._chromeUserDataPath = path;
} else {
this._chromeUserDataPath = null;
}
return this._chromeUserDataPath;
};
ChromeProfileMigrator.prototype.getResources = async function Chrome_getResources(
aProfile
) {
let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
if (chromeUserDataPath) {
let profileFolder = OS.Path.join(chromeUserDataPath, aProfile.id);
if (await OS.File.exists(profileFolder)) {
let possibleResourcePromises = [
GetBookmarksResource(profileFolder),
GetHistoryResource(profileFolder),
GetCookiesResource(profileFolder),
];
if (AppConstants.platform == "win") {
possibleResourcePromises.push(
GetWindowsPasswordsResource(profileFolder)
);
}
let possibleResources = await Promise.all(possibleResourcePromises);
return possibleResources.filter(r => r != null);
}
}
return [];
};
ChromeProfileMigrator.prototype.getLastUsedDate = async function Chrome_getLastUsedDate() {
let sourceProfiles = await this.getSourceProfiles();
let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
if (!chromeUserDataPath) {
return new Date(0);
}
let datePromises = sourceProfiles.map(async profile => {
let basePath = OS.Path.join(chromeUserDataPath, profile.id);
let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(
async leafName => {
let path = OS.Path.join(basePath, leafName);
let info = await OS.File.stat(path).catch(() => null);
return info ? info.lastModificationDate : 0;
}
);
let dates = await Promise.all(fileDatePromises);
return Math.max(...dates);
});
let datesOuter = await Promise.all(datePromises);
datesOuter.push(0);
return new Date(Math.max(...datesOuter));
};
ChromeProfileMigrator.prototype.getSourceProfiles = async function Chrome_getSourceProfiles() {
if ("__sourceProfiles" in this) {
return this.__sourceProfiles;
}
let chromeUserDataPath = await this._getChromeUserDataPathIfExists();
if (!chromeUserDataPath) {
return [];
}
let profiles = [];
try {
let localState = await ChromeMigrationUtils.getLocalState(
this._chromeUserDataPathSuffix
);
let info_cache = localState.profile.info_cache;
for (let profileFolderName in info_cache) {
profiles.push({
id: profileFolderName,
name: info_cache[profileFolderName].name || profileFolderName,
});
}
} catch (e) {
Cu.reportError("Error detecting Chrome profiles: " + e);
// If we weren't able to detect any profiles above, fallback to the Default profile.
let defaultProfilePath = OS.Path.join(chromeUserDataPath, "Default");
if (await OS.File.exists(defaultProfilePath)) {
profiles = [
{
id: "Default",
name: "Default",
},
];
}
}
let profileResources = await Promise.all(
profiles.map(async profile => ({
profile,
resources: await this.getResources(profile),
}))
);
// Only list profiles from which any data can be imported
this.__sourceProfiles = profileResources
.filter(({ resources }) => {
return resources && resources.length > 0;
}, this)
.map(({ profile }) => profile);
return this.__sourceProfiles;
};
Object.defineProperty(ChromeProfileMigrator.prototype, "sourceLocked", {
get: function Chrome_sourceLocked() {
// There is an exclusive lock on some SQLite databases. Assume they are locked for now.
return true;
},
});
async function GetBookmarksResource(aProfileFolder) {
let bookmarksPath = OS.Path.join(aProfileFolder, "Bookmarks");
if (!(await OS.File.exists(bookmarksPath))) {
return null;
}
return {
type: MigrationUtils.resourceTypes.BOOKMARKS,
migrate(aCallback) {
return (async function() {
let gotErrors = false;
let errorGatherer = function() {
gotErrors = true;
};
// Parse Chrome bookmark file that is JSON format
let bookmarkJSON = await OS.File.read(bookmarksPath, {
encoding: "UTF-8",
});
let roots = JSON.parse(bookmarkJSON).roots;
// Importing bookmark bar items
if (
roots.bookmark_bar.children &&
roots.bookmark_bar.children.length > 0
) {
// Toolbar
let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
let bookmarks = convertBookmarks(
roots.bookmark_bar.children,
errorGatherer
);
if (!MigrationUtils.isStartupMigration) {
parentGuid = await MigrationUtils.createImportedBookmarksFolder(
"Chrome",
parentGuid
);
}
await MigrationUtils.insertManyBookmarksWrapper(
bookmarks,
parentGuid
);
}
// Importing bookmark menu items
if (roots.other.children && roots.other.children.length > 0) {
// Bookmark menu
let parentGuid = PlacesUtils.bookmarks.menuGuid;
let bookmarks = convertBookmarks(roots.other.children, errorGatherer);
if (!MigrationUtils.isStartupMigration) {
parentGuid = await MigrationUtils.createImportedBookmarksFolder(
"Chrome",
parentGuid
);
}
await MigrationUtils.insertManyBookmarksWrapper(
bookmarks,
parentGuid
);
}
if (gotErrors) {
throw new Error("The migration included errors.");
}
})().then(() => aCallback(true), () => aCallback(false));
},
};
}
async function GetHistoryResource(aProfileFolder) {
let historyPath = OS.Path.join(aProfileFolder, "History");
if (!(await OS.File.exists(historyPath))) {
return null;
}
return {
type: MigrationUtils.resourceTypes.HISTORY,
migrate(aCallback) {
(async function() {
const MAX_AGE_IN_DAYS = Services.prefs.getIntPref(
"browser.migrate.chrome.history.maxAgeInDays"
);
const LIMIT = Services.prefs.getIntPref(
"browser.migrate.chrome.history.limit"
);
let query =
"SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
if (MAX_AGE_IN_DAYS) {
let maxAge = ChromeMigrationUtils.dateToChromeTime(
Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000
);
query += " AND last_visit_time > " + maxAge;
}
if (LIMIT) {
query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
}
let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
historyPath,
"Chrome history",
query
);
let pageInfos = [];
for (let row of rows) {
try {
// if having typed_count, we changes transition type to typed.
let transition = PlacesUtils.history.TRANSITIONS.LINK;
if (row.getResultByName("typed_count") > 0) {
transition = PlacesUtils.history.TRANSITIONS.TYPED;
}
pageInfos.push({
title: row.getResultByName("title"),
url: new URL(row.getResultByName("url")),
visits: [
{
transition,
date: ChromeMigrationUtils.chromeTimeToDate(
row.getResultByName("last_visit_time")
),
},
],
});
} catch (e) {
Cu.reportError(e);
}
}
if (pageInfos.length > 0) {
await MigrationUtils.insertVisitsWrapper(pageInfos);
}
})().then(
() => {
aCallback(true);
},
ex => {
Cu.reportError(ex);
aCallback(false);
}
);
},
};
}
async function GetCookiesResource(aProfileFolder) {
let cookiesPath = OS.Path.join(aProfileFolder, "Cookies");
if (!(await OS.File.exists(cookiesPath))) {
return null;
}
return {
type: MigrationUtils.resourceTypes.COOKIES,
async migrate(aCallback) {
// We don't support decrypting cookies yet so only import plaintext ones.
let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
cookiesPath,
"Chrome cookies",
`SELECT host_key, name, value, path, expires_utc, secure, httponly, encrypted_value
FROM cookies
WHERE length(encrypted_value) = 0`
).catch(ex => {
Cu.reportError(ex);
aCallback(false);
});
// If the promise was rejected we will have already called aCallback,
// so we can just return here.
if (!rows) {
return;
}
for (let row of rows) {
let host_key = row.getResultByName("host_key");
if (host_key.match(/^\./)) {
// 1st character of host_key may be ".", so we have to remove it
host_key = host_key.substr(1);
}
try {
let expiresUtc =
ChromeMigrationUtils.chromeTimeToDate(
row.getResultByName("expires_utc")
) / 1000;
Services.cookies.add(
host_key,
row.getResultByName("path"),
row.getResultByName("name"),
row.getResultByName("value"),
row.getResultByName("secure"),
row.getResultByName("httponly"),
false,
parseInt(expiresUtc),
{},
Ci.nsICookie.SAMESITE_NONE
);
} catch (e) {
Cu.reportError(e);
}
}
aCallback(true);
},
};
}
async function GetWindowsPasswordsResource(aProfileFolder) {
let loginPath = OS.Path.join(aProfileFolder, "Login Data");
if (!(await OS.File.exists(loginPath))) {
return null;
}
return {
type: MigrationUtils.resourceTypes.PASSWORDS,
async migrate(aCallback) {
let rows = await MigrationUtils.getRowsFromDBWithoutLocks(
loginPath,
"Chrome passwords",
`SELECT origin_url, action_url, username_element, username_value,
password_element, password_value, signon_realm, scheme, date_created,
times_used FROM logins WHERE blacklisted_by_user = 0`
).catch(ex => {
Cu.reportError(ex);
aCallback(false);
});
// If the promise was rejected we will have already called aCallback,
// so we can just return here.
if (!rows) {
return;
}
let crypto = new OSCrypto();
let logins = [];
for (let row of rows) {
try {
let origin_url = NetUtil.newURI(row.getResultByName("origin_url"));
// Ignore entries for non-http(s)/ftp URLs because we likely can't
// use them anyway.
const kValidSchemes = new Set(["https", "http", "ftp"]);
if (!kValidSchemes.has(origin_url.scheme)) {
continue;
}
let loginInfo = {
username: row.getResultByName("username_value"),
password: crypto.decryptData(
crypto.arrayToString(row.getResultByName("password_value")),
null
),
origin: origin_url.prePath,
formActionOrigin: null,
httpRealm: null,
usernameElement: row.getResultByName("username_element"),
passwordElement: row.getResultByName("password_element"),
timeCreated: ChromeMigrationUtils.chromeTimeToDate(
row.getResultByName("date_created") + 0
).getTime(),
timesUsed: row.getResultByName("times_used") + 0,
};
switch (row.getResultByName("scheme")) {
case AUTH_TYPE.SCHEME_HTML:
let action_url = row.getResultByName("action_url");
if (!action_url) {
// If there is no action_url, store the wildcard "" value.
// See the `formActionOrigin` IDL comments.
loginInfo.formActionOrigin = "";
break;
}
let action_uri = NetUtil.newURI(action_url);
if (!kValidSchemes.has(action_uri.scheme)) {
continue; // This continues the outer for loop.
}
loginInfo.formActionOrigin = action_uri.prePath;
break;
case AUTH_TYPE.SCHEME_BASIC:
case AUTH_TYPE.SCHEME_DIGEST:
// signon_realm format is URIrealm, so we need remove URI
loginInfo.httpRealm = row
.getResultByName("signon_realm")
.substring(loginInfo.origin.length + 1);
break;
default:
throw new Error(
"Login data scheme type not supported: " +
row.getResultByName("scheme")
);
}
logins.push(loginInfo);
} catch (e) {
Cu.reportError(e);
}
}
try {
if (logins.length > 0) {
await MigrationUtils.insertLoginsWrapper(logins);
}
} catch (e) {
Cu.reportError(e);
}
crypto.finalize();
aCallback(true);
},
};
}
ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator";
ChromeProfileMigrator.prototype.contractID =
"@mozilla.org/profile/migrator;1?app=browser&type=chrome";
ChromeProfileMigrator.prototype.classID = Components.ID(
"{4cec1de4-1671-4fc3-a53e-6c539dc77a26}"
);
/**
* Chromium migration
**/
function ChromiumProfileMigrator() {
this._chromeUserDataPathSuffix = "Chromium";
}
ChromiumProfileMigrator.prototype = Object.create(
ChromeProfileMigrator.prototype
);
ChromiumProfileMigrator.prototype.classDescription =
"Chromium Profile Migrator";
ChromiumProfileMigrator.prototype.contractID =
"@mozilla.org/profile/migrator;1?app=browser&type=chromium";
ChromiumProfileMigrator.prototype.classID = Components.ID(
"{8cece922-9720-42de-b7db-7cef88cb07ca}"
);
var EXPORTED_SYMBOLS = ["ChromeProfileMigrator", "ChromiumProfileMigrator"];
/**
* Chrome Canary
* Not available on Linux
**/
function CanaryProfileMigrator() {
this._chromeUserDataPathSuffix = "Canary";
}
CanaryProfileMigrator.prototype = Object.create(
ChromeProfileMigrator.prototype
);
CanaryProfileMigrator.prototype.classDescription =
"Chrome Canary Profile Migrator";
CanaryProfileMigrator.prototype.contractID =
"@mozilla.org/profile/migrator;1?app=browser&type=canary";
CanaryProfileMigrator.prototype.classID = Components.ID(
"{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}"
);
if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
EXPORTED_SYMBOLS.push("CanaryProfileMigrator");
}
/**
* Chrome Dev / Unstable and Beta. Only separate from `regular` chrome on Linux
*/
if (AppConstants.platform != "win" && AppConstants.platform != "macosx") {
function ChromeDevMigrator() {
this._chromeUserDataPathSuffix = "Chrome Dev";
}
ChromeDevMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
ChromeDevMigrator.prototype.classDescription = "Chrome Dev Profile Migrator";
ChromeDevMigrator.prototype.contractID =
"@mozilla.org/profile/migrator;1?app=browser&type=chrome-dev";
ChromeDevMigrator.prototype.classID = Components.ID(
"{7370a02a-4886-42c3-a4ec-d48c726ec30a}"
);
function ChromeBetaMigrator() {
this._chromeUserDataPathSuffix = "Chrome Beta";
}
ChromeBetaMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
ChromeBetaMigrator.prototype.classDescription =
"Chrome Beta Profile Migrator";
ChromeBetaMigrator.prototype.contractID =
"@mozilla.org/profile/migrator;1?app=browser&type=chrome-beta";
ChromeBetaMigrator.prototype.classID = Components.ID(
"{47f75963-840b-4950-a1f0-d9c1864f8b8e}"
);
EXPORTED_SYMBOLS.push("ChromeDevMigrator", "ChromeBetaMigrator");
}