gecko-dev/browser/components/migration/ChromeProfileMigrator.js
Doug Thayer efdd84d094 Bug 862127 - Make migration interfaces more async r=Gijs
In order to clean up sync IO within our profile migrators, we
need to have async interfaces for those parts which are currently
doing sync IO. This converts the sync interfaces and adjusts most
of the call sites (migration.js call site changes are addressed
in a separate patch to break it out a bit).


MozReview-Commit-ID: 2Kcrxco4iYr

--HG--
extra : rebase_source : 53d123c435e43622a999a7e849dddbe1919061e0
2018-01-12 09:06:21 -08:00

501 lines
18 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* vim: sw=2 ts=2 sts=2 et */
/* 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 { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
const S100NS_PER_MS = 10;
const AUTH_TYPE = {
SCHEME_HTML: 0,
SCHEME_BASIC: 1,
SCHEME_DIGEST: 2,
};
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource:///modules/ChromeMigrationUtils.jsm");
Cu.import("resource:///modules/MigrationUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
"resource://gre/modules/OSCrypto.jsm");
/**
* Convert Chrome time format to Date object
*
* @param aTime
* Chrome time
* @return converted Date object
* @note Google Chrome uses FILETIME / 10 as time.
* FILETIME is based on same structure of Windows.
*/
function chromeTimeToDate(aTime) {
return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
}
/**
* Convert Date object to Chrome time format
*
* @param aDate
* Date object or integer equivalent
* @return Chrome time
* @note For details on Chrome time, see chromeTimeToDate.
*/
function dateToChromeTime(aDate) {
return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
}
/**
* 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() {
let path = ChromeMigrationUtils.getDataPath("Chrome");
let chromeUserDataFolder = new FileUtils.File(path);
this._chromeUserDataFolder = chromeUserDataFolder.exists() ?
chromeUserDataFolder : null;
}
ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
ChromeProfileMigrator.prototype.getResources =
function Chrome_getResources(aProfile) {
if (this._chromeUserDataFolder) {
let profileFolder = this._chromeUserDataFolder.clone();
profileFolder.append(aProfile.id);
if (profileFolder.exists()) {
let possibleResources = [
GetBookmarksResource(profileFolder),
GetHistoryResource(profileFolder),
GetCookiesResource(profileFolder),
];
if (AppConstants.platform == "win") {
possibleResources.push(GetWindowsPasswordsResource(profileFolder));
}
return possibleResources.filter(r => r != null);
}
}
return [];
};
ChromeProfileMigrator.prototype.getLastUsedDate =
function Chrome_getLastUsedDate() {
let datePromises = this.sourceProfiles.map(profile => {
let basePath = OS.Path.join(this._chromeUserDataFolder.path, profile.id);
let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(leafName => {
let path = OS.Path.join(basePath, leafName);
return OS.File.stat(path).catch(() => null).then(info => {
return info ? info.lastModificationDate : 0;
});
});
return Promise.all(fileDatePromises).then(dates => {
return Math.max.apply(Math, dates);
});
});
return Promise.all(datePromises).then(dates => {
dates.push(0);
return new Date(Math.max.apply(Math, dates));
});
};
ChromeProfileMigrator.prototype.getSourceProfiles =
async function Chrome_getSourceProfiles() {
if ("__sourceProfiles" in this)
return this.__sourceProfiles;
if (!this._chromeUserDataFolder)
return [];
let profiles = [];
try {
let info_cache = ChromeMigrationUtils.getLocalState().profile.info_cache;
for (let profileFolderName in info_cache) {
let profileFolder = this._chromeUserDataFolder.clone();
profileFolder.append(profileFolderName);
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 defaultProfileFolder = this._chromeUserDataFolder.clone();
defaultProfileFolder.append("Default");
if (defaultProfileFolder.exists()) {
profiles = [{
id: "Default",
name: "Default",
}];
}
}
// Only list profiles from which any data can be imported
this.__sourceProfiles = profiles.filter(function(profile) {
let resources = this.getResources(profile);
return resources && resources.length > 0;
}, this);
return this.__sourceProfiles;
};
ChromeProfileMigrator.prototype.getSourceHomePageURL =
async function Chrome_getSourceHomePageURL() {
let prefsFile = this._chromeUserDataFolder.clone();
prefsFile.append("Preferences");
if (prefsFile.exists()) {
// XXX reading and parsing JSON is synchronous.
let fstream = Cc[FILE_INPUT_STREAM_CID].
createInstance(Ci.nsIFileInputStream);
fstream.init(prefsFile, -1, 0, 0);
try {
return JSON.parse(
NetUtil.readInputStreamToString(fstream, fstream.available(),
{ charset: "UTF-8" })
).homepage;
} catch (e) {
Cu.reportError("Error parsing Chrome's preferences file: " + e);
}
}
return "";
};
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;
},
});
function GetBookmarksResource(aProfileFolder) {
let bookmarksFile = aProfileFolder.clone();
bookmarksFile.append("Bookmarks");
if (!bookmarksFile.exists())
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(bookmarksFile.path, {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));
},
};
}
function GetHistoryResource(aProfileFolder) {
let historyFile = aProfileFolder.clone();
historyFile.append("History");
if (!historyFile.exists())
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 = 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(historyFile.path, "Chrome history", query);
let places = [];
for (let row of rows) {
try {
// if having typed_count, we changes transition type to typed.
let transType = PlacesUtils.history.TRANSITION_LINK;
if (row.getResultByName("typed_count") > 0)
transType = PlacesUtils.history.TRANSITION_TYPED;
places.push({
uri: NetUtil.newURI(row.getResultByName("url")),
title: row.getResultByName("title"),
visits: [{
transitionType: transType,
visitDate: chromeTimeToDate(
row.getResultByName(
"last_visit_time")) * 1000,
}],
});
} catch (e) {
Cu.reportError(e);
}
}
if (places.length > 0) {
await new Promise((resolve, reject) => {
MigrationUtils.insertVisitsWrapper(places, {
ignoreErrors: true,
ignoreResults: true,
handleCompletion(updatedCount) {
if (updatedCount > 0) {
resolve();
} else {
reject(new Error("Couldn't add visits"));
}
},
});
});
}
})().then(() => { aCallback(true); },
ex => {
Cu.reportError(ex);
aCallback(false);
});
},
};
}
function GetCookiesResource(aProfileFolder) {
let cookiesFile = aProfileFolder.clone();
cookiesFile.append("Cookies");
if (!cookiesFile.exists())
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(cookiesFile.path, "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 =
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),
{});
} catch (e) {
Cu.reportError(e);
}
}
aCallback(true);
},
};
}
function GetWindowsPasswordsResource(aProfileFolder) {
let loginFile = aProfileFolder.clone();
loginFile.append("Login Data");
if (!loginFile.exists())
return null;
return {
type: MigrationUtils.resourceTypes.PASSWORDS,
async migrate(aCallback) {
let rows = await MigrationUtils.getRowsFromDBWithoutLocks(loginFile.path, "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();
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),
hostname: origin_url.prePath,
formSubmitURL: null,
httpRealm: null,
usernameElement: row.getResultByName("username_element"),
passwordElement: row.getResultByName("password_element"),
timeCreated: 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 = NetUtil.newURI(row.getResultByName("action_url"));
if (!kValidSchemes.has(action_url.scheme)) {
continue; // This continues the outer for loop.
}
loginInfo.formSubmitURL = action_url.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.hostname.length + 1);
break;
default:
throw new Error("Login data scheme type not supported: " +
row.getResultByName("scheme"));
}
MigrationUtils.insertLoginWrapper(loginInfo);
} 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() {
let path = ChromeMigrationUtils.getDataPath("Chromium");
let chromiumUserDataFolder = new FileUtils.File(path);
this._chromeUserDataFolder = chromiumUserDataFolder.exists() ? chromiumUserDataFolder : null;
}
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 componentsArray = [ChromeProfileMigrator, ChromiumProfileMigrator];
/**
* Chrome Canary
* Not available on Linux
**/
function CanaryProfileMigrator() {
let path = ChromeMigrationUtils.getDataPath("Canary");
let chromeUserDataFolder = new FileUtils.File(path);
this._chromeUserDataFolder = chromeUserDataFolder.exists() ? chromeUserDataFolder : null;
}
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") {
componentsArray.push(CanaryProfileMigrator);
}
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray);