summaryrefslogtreecommitdiff
path: root/application/palemoon/components/migration/ChromeProfileMigrator.js
diff options
context:
space:
mode:
Diffstat (limited to 'application/palemoon/components/migration/ChromeProfileMigrator.js')
-rw-r--r--application/palemoon/components/migration/ChromeProfileMigrator.js463
1 files changed, 463 insertions, 0 deletions
diff --git a/application/palemoon/components/migration/ChromeProfileMigrator.js b/application/palemoon/components/migration/ChromeProfileMigrator.js
new file mode 100644
index 0000000000..61955fbb49
--- /dev/null
+++ b/application/palemoon/components/migration/ChromeProfileMigrator.js
@@ -0,0 +1,463 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+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:///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 aFolderId
+ * id of folder where items will be inserted
+ * @param aItems
+ * bookmark items to be inserted
+ */
+function insertBookmarkItems(aFolderId, aItems)
+{
+ for (let i = 0; i < aItems.length; i++) {
+ let item = aItems[i];
+
+ try {
+ if (item.type == "url") {
+ PlacesUtils.bookmarks.insertBookmark(aFolderId,
+ NetUtil.newURI(item.url),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ item.name);
+ } else if (item.type == "folder") {
+ let newFolderId =
+ PlacesUtils.bookmarks.createFolder(aFolderId,
+ item.name,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ insertBookmarkItems(newFolderId, item.children);
+ }
+ } catch (e) {
+ Cu.reportError(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 [r for each (r in possibleResources) if (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 "";
+ }
+});
+
+function GetBookmarksResource(aProfileFolder) {
+ let bookmarksFile = aProfileFolder.clone();
+ bookmarksFile.append("Bookmarks");
+ if (!bookmarksFile.exists())
+ return null;
+
+ return {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+ migrate: function(aCallback) {
+ NetUtil.asyncFetch(bookmarksFile, MigrationUtils.wrapMigrateFunction(
+ function(aInputStream, aResultCode) {
+ if (!Components.isSuccessCode(aResultCode))
+ throw new Error("Could not read Bookmarks file");
+
+ // Parse Chrome bookmark file that is JSON format
+ let bookmarkJSON = NetUtil.readInputStreamToString(
+ aInputStream, aInputStream.available(), { charset : "UTF-8" });
+ let roots = JSON.parse(bookmarkJSON).roots;
+ PlacesUtils.bookmarks.runInBatchMode({
+ runBatched: function() {
+ // Importing bookmark bar items
+ if (roots.bookmark_bar.children &&
+ roots.bookmark_bar.children.length > 0) {
+ // Toolbar
+ let parentId = PlacesUtils.toolbarFolderId;
+ if (!MigrationUtils.isStartupMigration) {
+ parentId = MigrationUtils.createImportedBookmarksFolder(
+ "Chrome", parentId);
+ }
+ insertBookmarkItems(parentId, roots.bookmark_bar.children);
+ }
+
+ // Importing bookmark menu items
+ if (roots.other.children &&
+ roots.other.children.length > 0) {
+ // Bookmark menu
+ let parentId = PlacesUtils.bookmarksMenuFolderId;
+ if (!MigrationUtils.isStartupMigration) {
+ parentId = MigrationUtils.createImportedBookmarksFolder(
+ "Chrome", parentId);
+ }
+ insertBookmarkItems(parentId, roots.other.children);
+ }
+ }
+ }, null);
+ }, aCallback));
+ }
+ };
+}
+
+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);
+ let stmt = dbConn.createAsyncStatement(
+ "SELECT host_key, path, name, value, secure, httponly, expires_utc FROM cookies");
+
+ 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();
+ let utf8Converter = Cc["@mozilla.org/intl/utf8converterservice;1"].getService(Ci.nsIUTF8ConverterService);
+
+ stmt.executeAsync({
+ _rowToLoginInfo(row) {
+ let loginInfo = {
+ username: utf8Converter.convertURISpecToUTF8(row.getResultByName("username_value"), "UTF-8"),
+ password: utf8Converter.convertURISpecToUTF8(
+ crypto.decryptData(crypto.arrayToString(row.getResultByName("password_value")), null),
+ "UTF-8"),
+ 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}");
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeProfileMigrator]);