tenfourfox/browser/components/migration/ChromeProfileMigrator.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

534 lines
19 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/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Task.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);
}
/**
* Insert bookmark items into specific folder.
*
* @param parentGuid
* GUID of the folder where items will be inserted
* @param items
* bookmark items to be inserted
* @param errorAccumulator
* function that gets called with any errors thrown so we don't drop them on the floor.
*/
function* insertBookmarkItems(parentGuid, items, errorAccumulator) {
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;
}
yield PlacesUtils.bookmarks.insert({
parentGuid, url: item.url, title: item.name
});
} else if (item.type == "folder") {
let newFolderGuid = (yield PlacesUtils.bookmarks.insert({
parentGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title: item.name
})).guid;
yield insertBookmarkItems(newFolderGuid, item.children, errorAccumulator);
}
} catch (e) {
Cu.reportError(e);
errorAccumulator(e);
}
}
}
function ChromeProfileMigrator() {
let chromeUserDataFolder = FileUtils.getDir(
#ifdef XP_WIN
"LocalAppData", ["Google", "Chrome", "User Data"]
#elifdef XP_MACOSX
"ULibDir", ["Application Support", "Google", "Chrome"]
#else
"Home", [".config", "google-chrome"]
#endif
, false);
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),
#ifdef XP_WIN
GetWindowsPasswordsResource(profileFolder)
#endif
];
return possibleResources.filter(r => r != null);
}
}
return [];
};
Object.defineProperty(ChromeProfileMigrator.prototype, "sourceProfiles", {
get: function Chrome_sourceProfiles() {
if ("__sourceProfiles" in this)
return this.__sourceProfiles;
if (!this._chromeUserDataFolder)
return [];
let profiles = [];
try {
// Local State is a JSON file that contains profile info.
let localState = this._chromeUserDataFolder.clone();
localState.append("Local State");
if (!localState.exists())
throw new Error("Chrome's 'Local State' file does not exist.");
if (!localState.isReadable())
throw new Error("Chrome's 'Local State' file could not be read.");
let fstream = Cc[FILE_INPUT_STREAM_CID].createInstance(Ci.nsIFileInputStream);
fstream.init(localState, -1, 0, 0);
let inputStream = NetUtil.readInputStreamToString(fstream, fstream.available(),
{ charset: "UTF-8" });
let info_cache = JSON.parse(inputStream).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
return this.__sourceProfiles = profiles.filter(function(profile) {
let resources = this.getResources(profile);
return resources && resources.length > 0;
}, this);
}
});
Object.defineProperty(ChromeProfileMigrator.prototype, "sourceHomePageURL", {
get: function Chrome_sourceHomePageURL() {
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(file, -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: function(aCallback) {
return Task.spawn(function* () {
let gotErrors = false;
let errorGatherer = () => gotErrors = true;
let jsonStream = yield new Promise(resolve =>
NetUtil.asyncFetch({ uri: NetUtil.newURI(bookmarksFile),
loadUsingSystemPrincipal: true
},
(inputStream, resultCode) => {
if (Components.isSuccessCode(resultCode)) {
resolve(inputStream);
} else {
reject(new Error("Could not read Bookmarks file"));
}
}
)
);
// Parse Chrome bookmark file that is JSON format
let bookmarkJSON = NetUtil.readInputStreamToString(
jsonStream, jsonStream.available(), { charset : "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;
if (!MigrationUtils.isStartupMigration) {
parentGuid =
yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
}
yield insertBookmarkItems(parentGuid, roots.bookmark_bar.children, errorGatherer);
}
// Importing bookmark menu items
if (roots.other.children &&
roots.other.children.length > 0) {
// Bookmark menu
let parentGuid = PlacesUtils.bookmarks.menuGuid;
if (!MigrationUtils.isStartupMigration) {
parentGuid =
yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
}
yield insertBookmarkItems(parentGuid, roots.other.children, errorGatherer);
}
if (gotErrors) {
throw "The migration included errors.";
}
}.bind(this)).then(() => aCallback(true),
e => aCallback(false));
}
};
}
function GetHistoryResource(aProfileFolder) {
let historyFile = aProfileFolder.clone();
historyFile.append("History");
if (!historyFile.exists())
return null;
return {
type: MigrationUtils.resourceTypes.HISTORY,
migrate: function(aCallback) {
let dbConn = Services.storage.openUnsharedDatabase(historyFile);
let stmt = dbConn.createAsyncStatement(
"SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0");
stmt.executeAsync({
handleResult : function(aResults) {
let places = [];
for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
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);
}
}
try {
PlacesUtils.asyncHistory.updatePlaces(places);
} catch (e) {
Cu.reportError(e);
}
},
handleError : function(aError) {
Cu.reportError("Async statement execution returned with '" +
aError.result + "', '" + aError.message + "'");
},
handleCompletion : function(aReason) {
dbConn.asyncClose();
aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
}
});
stmt.finalize();
}
};
}
function GetCookiesResource(aProfileFolder) {
let cookiesFile = aProfileFolder.clone();
cookiesFile.append("Cookies");
if (!cookiesFile.exists())
return null;
return {
type: MigrationUtils.resourceTypes.COOKIES,
migrate: function(aCallback) {
let dbConn = Services.storage.openUnsharedDatabase(cookiesFile);
// We don't support decrypting cookies yet so only import plaintext ones.
let stmt = dbConn.createAsyncStatement(`
SELECT host_key, name, value, path, expires_utc, secure, httponly, encrypted_value
FROM cookies
WHERE length(encrypted_value) = 0`);
stmt.executeAsync({
handleResult : function(aResults) {
for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
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);
}
}
},
handleError : function(aError) {
Cu.reportError("Async statement execution returned with '" +
aError.result + "', '" + aError.message + "'");
},
handleCompletion : function(aReason) {
dbConn.asyncClose();
aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
},
});
stmt.finalize();
}
}
}
function GetWindowsPasswordsResource(aProfileFolder) {
let loginFile = aProfileFolder.clone();
loginFile.append("Login Data");
if (!loginFile.exists())
return null;
return {
type: MigrationUtils.resourceTypes.PASSWORDS,
migrate(aCallback) {
let dbConn = Services.storage.openUnsharedDatabase(loginFile);
let stmt = dbConn.createAsyncStatement(`
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`);
let crypto = new OSCrypto();
stmt.executeAsync({
_rowToLoginInfo(row) {
let loginInfo = {
username: row.getResultByName("username_value"),
password: crypto.
decryptData(crypto.arrayToString(row.getResultByName("password_value")),
null),
hostName: NetUtil.newURI(row.getResultByName("origin_url")).prePath,
submitURL: 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:
loginInfo.submitURL = NetUtil.newURI(row.getResultByName("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"));
}
return loginInfo;
},
handleResult(aResults) {
for (let row = aResults.getNextRow(); row; row = aResults.getNextRow()) {
try {
let loginInfo = this._rowToLoginInfo(row);
let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
login.init(loginInfo.hostName, loginInfo.submitURL, loginInfo.httpRealm,
loginInfo.username, loginInfo.password, loginInfo.usernameElement,
loginInfo.passwordElement);
login.QueryInterface(Ci.nsILoginMetaInfo);
login.timeCreated = loginInfo.timeCreated;
login.timeLastUsed = loginInfo.timeCreated;
login.timePasswordChanged = loginInfo.timeCreated;
login.timesUsed = loginInfo.timesUsed;
// Add the login only if there's not an existing entry
let logins = Services.logins.findLogins({}, login.hostname,
login.formSubmitURL,
login.httpRealm);
// Bug 1187190: Password changes should be propagated depending on timestamps.
if (!logins.some(l => login.matches(l, true))) {
Services.logins.addLogin(login);
}
} catch (e) {
Cu.reportError(e);
}
}
},
handleError(aError) {
Cu.reportError("Async statement execution returned with '" +
aError.result + "', '" + aError.message + "'");
},
handleCompletion(aReason) {
dbConn.asyncClose();
aCallback(aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED);
crypto.finalize();
},
});
stmt.finalize();
}
};
}
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 chromiumUserDataFolder = FileUtils.getDir(
#ifdef XP_WIN
"LocalAppData", ["Chromium", "User Data"]
#elifdef XP_MACOSX
"ULibDir", ["Application Support", "Chromium"]
#else
"Home", [".config", "chromium"]
#endif
, false);
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];
#if defined(XP_WIN) || defined(XP_MACOSX)
/**
* Chrome Canary
* Not available on Linux
**/
function CanaryProfileMigrator() {
let chromeUserDataFolder = FileUtils.getDir(
#ifdef XP_WIN
"LocalAppData", ["Google", "Chrome SxS", "User Data"]
#elifdef XP_MACOSX
"ULibDir", ["Application Support", "Google", "Chrome Canary"]
#endif
, false);
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}");
componentsArray.push(CanaryProfileMigrator);
#endif
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray);