From 49ee0794b5d912db1f95dce6eb52d781dc210db5 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 03:35:06 -0500 Subject: Add Pale Moon --- .../migration/BrowserProfileMigrators.manifest | 14 + .../components/migration/ChromeProfileMigrator.js | 463 +++++++++++++++ .../components/migration/FirefoxProfileMigrator.js | 117 ++++ .../components/migration/IEProfileMigrator.js | 521 +++++++++++++++++ .../components/migration/MigrationUtils.jsm | 644 +++++++++++++++++++++ .../components/migration/ProfileMigrator.js | 21 + .../components/migration/SafariProfileMigrator.js | 416 +++++++++++++ .../components/migration/content/migration.js | 474 +++++++++++++++ .../components/migration/content/migration.xul | 96 +++ application/palemoon/components/migration/jar.mn | 7 + .../palemoon/components/migration/moz.build | 53 ++ .../migration/nsIBrowserProfileMigrator.idl | 63 ++ .../components/migration/nsIEHistoryEnumerator.cpp | 139 +++++ .../components/migration/nsIEHistoryEnumerator.h | 37 ++ 14 files changed, 3065 insertions(+) create mode 100644 application/palemoon/components/migration/BrowserProfileMigrators.manifest create mode 100644 application/palemoon/components/migration/ChromeProfileMigrator.js create mode 100644 application/palemoon/components/migration/FirefoxProfileMigrator.js create mode 100644 application/palemoon/components/migration/IEProfileMigrator.js create mode 100644 application/palemoon/components/migration/MigrationUtils.jsm create mode 100644 application/palemoon/components/migration/ProfileMigrator.js create mode 100644 application/palemoon/components/migration/SafariProfileMigrator.js create mode 100644 application/palemoon/components/migration/content/migration.js create mode 100644 application/palemoon/components/migration/content/migration.xul create mode 100644 application/palemoon/components/migration/jar.mn create mode 100644 application/palemoon/components/migration/moz.build create mode 100644 application/palemoon/components/migration/nsIBrowserProfileMigrator.idl create mode 100644 application/palemoon/components/migration/nsIEHistoryEnumerator.cpp create mode 100644 application/palemoon/components/migration/nsIEHistoryEnumerator.h (limited to 'application/palemoon/components/migration') diff --git a/application/palemoon/components/migration/BrowserProfileMigrators.manifest b/application/palemoon/components/migration/BrowserProfileMigrators.manifest new file mode 100644 index 000000000..d7fec75e3 --- /dev/null +++ b/application/palemoon/components/migration/BrowserProfileMigrators.manifest @@ -0,0 +1,14 @@ +component {6F8BB968-C14F-4D6F-9733-6C6737B35DCE} ProfileMigrator.js +contract @mozilla.org/toolkit/profile-migrator;1 {6F8BB968-C14F-4D6F-9733-6C6737B35DCE} +component {4cec1de4-1671-4fc3-a53e-6c539dc77a26} ChromeProfileMigrator.js +contract @mozilla.org/profile/migrator;1?app=browser&type=chrome {4cec1de4-1671-4fc3-a53e-6c539dc77a26} +component {91185366-ba97-4438-acba-48deaca63386} FirefoxProfileMigrator.js +contract @mozilla.org/profile/migrator;1?app=browser&type=firefox {91185366-ba97-4438-acba-48deaca63386} +#ifdef HAS_IE_MIGRATOR +component {3d2532e3-4932-4774-b7ba-968f5899d3a4} IEProfileMigrator.js +contract @mozilla.org/profile/migrator;1?app=browser&type=ie {3d2532e3-4932-4774-b7ba-968f5899d3a4} +#endif +#ifdef HAS_SAFARI_MIGRATOR +component {4b609ecf-60b2-4655-9df4-dc149e474da1} SafariProfileMigrator.js +contract @mozilla.org/profile/migrator;1?app=browser&type=safari {4b609ecf-60b2-4655-9df4-dc149e474da1} +#endif diff --git a/application/palemoon/components/migration/ChromeProfileMigrator.js b/application/palemoon/components/migration/ChromeProfileMigrator.js new file mode 100644 index 000000000..61955fbb4 --- /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]); diff --git a/application/palemoon/components/migration/FirefoxProfileMigrator.js b/application/palemoon/components/migration/FirefoxProfileMigrator.js new file mode 100644 index 000000000..ab4ae55fb --- /dev/null +++ b/application/palemoon/components/migration/FirefoxProfileMigrator.js @@ -0,0 +1,117 @@ +/* -*- 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"; + +/* + * Migrates from a Pale Moon profile in a lossy manner in order to clean up a + * user's profile. Data is only migrated where the benefits outweigh the + * potential problems caused by importing undesired/invalid configurations + * from the source profile. + */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource:///modules/MigrationUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", + "resource://gre/modules/PlacesBackups.jsm"); + +function FirefoxProfileMigrator() { } + +FirefoxProfileMigrator.prototype = Object.create(MigratorPrototype); + +FirefoxProfileMigrator.prototype._getAllProfiles = function () { + let allProfiles = new Map(); + let profiles = + Components.classes["@mozilla.org/toolkit/profile-service;1"] + .getService(Components.interfaces.nsIToolkitProfileService) + .profiles; + while (profiles.hasMoreElements()) { + let profile = profiles.getNext().QueryInterface(Components.interfaces.nsIToolkitProfile); + let rootDir = profile.rootDir; + + if (rootDir.exists() && rootDir.isReadable() && + !rootDir.equals(MigrationUtils.profileStartup.directory)) { + allProfiles.set(profile.name, rootDir); + } + } + return allProfiles; +}; + +function sorter(a, b) { + return a.id.toLocaleLowerCase().localeCompare(b.id.toLocaleLowerCase()); +} + +Object.defineProperty(FirefoxProfileMigrator.prototype, "sourceProfiles", { + get: function() { + return [{id: x, name: x} for (x of this._getAllProfiles().keys())].sort(sorter); + } +}); + +FirefoxProfileMigrator.prototype.getResources = function(aProfile) { + let sourceProfileDir = aProfile ? this._getAllProfiles().get(aProfile.id) : + Components.classes["@mozilla.org/toolkit/profile-service;1"] + .getService(Components.interfaces.nsIToolkitProfileService) + .selectedProfile.rootDir; + if (!sourceProfileDir || !sourceProfileDir.exists() || + !sourceProfileDir.isReadable()) + return null; + + // Being a startup-only migrator, we can rely on + // MigrationUtils.profileStartup being set. + let currentProfileDir = MigrationUtils.profileStartup.directory; + + // Surely data cannot be imported from the current profile. + if (sourceProfileDir.equals(currentProfileDir)) + return null; + + let getFileResource = function(aMigrationType, aFileNames) { + let files = []; + for (let fileName of aFileNames) { + let file = sourceProfileDir.clone(); + file.append(fileName); + + if (file.exists()) { + files.push(file); + } + } + if (!files.length) { + return null; + } + return { + type: aMigrationType, + migrate: function(aCallback) { + for (let file of files) { + file.copyTo(currentProfileDir, ""); + } + aCallback(true); + } + }; + }; + + let types = MigrationUtils.resourceTypes; + let places = getFileResource(types.HISTORY, ["places.sqlite", "places.sqlite-wal"]); + let cookies = getFileResource(types.COOKIES, ["cookies.sqlite", "cookies.sqlite-wal"]); + let passwords = getFileResource(types.PASSWORDS, + ["signons.sqlite", "logins.json", "key3.db"]); + let formData = getFileResource(types.FORMDATA, ["formhistory.sqlite"]); + let bookmarksBackups = getFileResource(types.OTHERDATA, + [PlacesBackups.profileRelativeFolderPath]); + let dictionary = getFileResource(types.OTHERDATA, ["persdict.dat"]); + + return [r for each (r in [places, cookies, passwords, formData, + dictionary, bookmarksBackups]) if (r)]; +} + +Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", { + get: function() true +}); + + +FirefoxProfileMigrator.prototype.classDescription = "Firefox Profile Migrator"; +FirefoxProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=firefox"; +FirefoxProfileMigrator.prototype.classID = Components.ID("{91185366-ba97-4438-acba-48deaca63386}"); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FirefoxProfileMigrator]); diff --git a/application/palemoon/components/migration/IEProfileMigrator.js b/application/palemoon/components/migration/IEProfileMigrator.js new file mode 100644 index 000000000..a46deae68 --- /dev/null +++ b/application/palemoon/components/migration/IEProfileMigrator.js @@ -0,0 +1,521 @@ +/* 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 kMainKey = "Software\\Microsoft\\Internet Explorer\\Main"; +const kRegMultiSz = 7; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource:///modules/MigrationUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ctypes", + "resource://gre/modules/ctypes.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", + "resource://gre/modules/WindowsRegistry.jsm"); + +Cu.importGlobalProperties(["File"]); + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers. + +let CtypesHelpers = { + _structs: {}, + _functions: {}, + _libs: {}, + + /** + * Must be invoked once before first use of any of the provided helpers. + */ + initialize: function CH_initialize() { + const WORD = ctypes.uint16_t; + const DWORD = ctypes.uint32_t; + const BOOL = ctypes.int; + + this._structs.SYSTEMTIME = new ctypes.StructType('SYSTEMTIME', [ + {wYear: WORD}, + {wMonth: WORD}, + {wDayOfWeek: WORD}, + {wDay: WORD}, + {wHour: WORD}, + {wMinute: WORD}, + {wSecond: WORD}, + {wMilliseconds: WORD} + ]); + + this._structs.FILETIME = new ctypes.StructType('FILETIME', [ + {dwLowDateTime: DWORD}, + {dwHighDateTime: DWORD} + ]); + + try { + this._libs.kernel32 = ctypes.open("Kernel32"); + this._functions.FileTimeToSystemTime = + this._libs.kernel32.declare("FileTimeToSystemTime", + ctypes.default_abi, + BOOL, + this._structs.FILETIME.ptr, + this._structs.SYSTEMTIME.ptr); + } catch (ex) { + this.finalize(); + } + }, + + /** + * Must be invoked once after last use of any of the provided helpers. + */ + finalize: function CH_finalize() { + this._structs = {}; + this._functions = {}; + for each (let lib in this._libs) { + try { + lib.close(); + } catch (ex) {} + } + this._libs = {}; + }, + + /** + * Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct. + * + * @param aTimeHi + * Least significant DWORD. + * @param aTimeLo + * Most significant DWORD. + * @return a Date object representing the converted datetime. + */ + fileTimeToDate: function CH_fileTimeToDate(aTimeHi, aTimeLo) { + let fileTime = this._structs.FILETIME(); + fileTime.dwLowDateTime = aTimeLo; + fileTime.dwHighDateTime = aTimeHi; + let systemTime = this._structs.SYSTEMTIME(); + let result = this._functions.FileTimeToSystemTime(fileTime.address(), + systemTime.address()); + if (result == 0) + throw new Error(ctypes.winLastError); + + return new Date(systemTime.wYear, + systemTime.wMonth - 1, + systemTime.wDay, + systemTime.wHour, + systemTime.wMinute, + systemTime.wSecond, + systemTime.wMilliseconds); + } +}; + +/** + * Checks whether an host is an IP (v4 or v6) address. + * + * @param aHost + * The host to check. + * @return whether aHost is an IP address. + */ +function hostIsIPAddress(aHost) { + try { + Services.eTLD.getBaseDomainFromHost(aHost); + } catch (e if e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS) { + return true; + } catch (e) {} + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +//// Resources + +function Bookmarks() { +} + +Bookmarks.prototype = { + type: MigrationUtils.resourceTypes.BOOKMARKS, + + get exists() !!this._favoritesFolder, + + __favoritesFolder: null, + get _favoritesFolder() { + if (!this.__favoritesFolder) { + let favoritesFolder = Services.dirsvc.get("Favs", Ci.nsIFile); + if (favoritesFolder.exists() && favoritesFolder.isReadable()) + this.__favoritesFolder = favoritesFolder; + } + return this.__favoritesFolder; + }, + + __toolbarFolderName: null, + get _toolbarFolderName() { + if (!this.__toolbarFolderName) { + // Retrieve the name of IE's favorites subfolder that holds the bookmarks + // in the toolbar. This was previously stored in the registry and changed + // in IE7 to always be called "Links". + let folderName = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "Software\\Microsoft\\Internet Explorer\\Toolbar", + "LinksFolderName"); + this.__toolbarFolderName = folderName || "Links"; + } + return this.__toolbarFolderName; + }, + + migrate: function B_migrate(aCallback) { + PlacesUtils.bookmarks.runInBatchMode({ + runBatched: (function migrateBatched() { + // Import to the bookmarks menu. + let destFolderId = PlacesUtils.bookmarksMenuFolderId; + if (!MigrationUtils.isStartupMigration) { + destFolderId = + MigrationUtils.createImportedBookmarksFolder("IE", destFolderId); + } + + this._migrateFolder(this._favoritesFolder, destFolderId); + + aCallback(true); + }).bind(this) + }, null); + }, + + _migrateFolder: function B__migrateFolder(aSourceFolder, aDestFolderId) { + // TODO (bug 741993): the favorites order is stored in the Registry, at + // HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites + // Until we support it, bookmarks are imported in alphabetical order. + let entries = aSourceFolder.directoryEntries; + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsIFile); + + // Make sure that entry.path == entry.target to not follow .lnk folder + // shortcuts which could lead to infinite cycles. + if (entry.isDirectory() && entry.path == entry.target) { + let destFolderId; + if (entry.leafName == this._toolbarFolderName && + entry.parent.equals(this._favoritesFolder)) { + // Import to the bookmarks toolbar. + destFolderId = PlacesUtils.toolbarFolderId; + if (!MigrationUtils.isStartupMigration) { + destFolderId = + MigrationUtils.createImportedBookmarksFolder("IE", destFolderId); + } + } + else { + // Import to a new folder. + destFolderId = + PlacesUtils.bookmarks.createFolder(aDestFolderId, entry.leafName, + PlacesUtils.bookmarks.DEFAULT_INDEX); + } + + if (entry.isReadable()) { + // Recursively import the folder. + this._migrateFolder(entry, destFolderId); + } + } + else { + // Strip the .url extension, to both check this is a valid link file, + // and get the associated title. + let matches = entry.leafName.match(/(.+)\.url$/i); + if (matches) { + let fileHandler = Cc["@mozilla.org/network/protocol;1?name=file"]. + getService(Ci.nsIFileProtocolHandler); + let uri = fileHandler.readURLFile(entry); + let title = matches[1]; + + PlacesUtils.bookmarks.insertBookmark(aDestFolderId, + uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + title); + } + } + } + } +}; + +function History() { +} + +History.prototype = { + type: MigrationUtils.resourceTypes.HISTORY, + + get exists() true, + + __typedURLs: null, + get _typedURLs() { + if (!this.__typedURLs) { + // The list of typed URLs is a sort of annotation stored in the registry. + // Currently, IE stores 25 entries and this value is not configurable, + // but we just keep reading up to the first non-existing entry to support + // possible future bumps of this limit. + this.__typedURLs = {}; + let registry = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + try { + registry.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "Software\\Microsoft\\Internet Explorer\\TypedURLs", + Ci.nsIWindowsRegKey.ACCESS_READ); + for (let entry = 1; registry.hasValue("url" + entry); entry++) { + let url = registry.readStringValue("url" + entry); + this.__typedURLs[url] = true; + } + } catch (ex) { + } finally { + registry.close(); + } + } + return this.__typedURLs; + }, + + migrate: function H_migrate(aCallback) { + let places = []; + let historyEnumerator = Cc["@mozilla.org/profile/migrator/iehistoryenumerator;1"]. + createInstance(Ci.nsISimpleEnumerator); + while (historyEnumerator.hasMoreElements()) { + let entry = historyEnumerator.getNext().QueryInterface(Ci.nsIPropertyBag2); + let uri = entry.get("uri").QueryInterface(Ci.nsIURI); + // MSIE stores some types of URLs in its history that we don't handle, + // like HTMLHelp and others. Since we don't properly map handling for + // all of them we just avoid importing them. + if (["http", "https", "ftp", "file"].indexOf(uri.scheme) == -1) { + continue; + } + + let title = entry.get("title"); + // Embed visits have no title and don't need to be imported. + if (title.length == 0) { + continue; + } + + // The typed urls are already fixed-up, so we can use them for comparison. + let transitionType = this._typedURLs[uri.spec] ? + Ci.nsINavHistoryService.TRANSITION_TYPED : + Ci.nsINavHistoryService.TRANSITION_LINK; + let lastVisitTime = entry.get("time"); + + places.push( + { uri: uri, + title: title, + visits: [{ transitionType: transitionType, + visitDate: lastVisitTime }] + } + ); + } + + // Check whether there is any history to import. + if (places.length == 0) { + aCallback(true); + return; + } + + PlacesUtils.asyncHistory.updatePlaces(places, { + _success: false, + handleResult: function() { + // Importing any entry is considered a successful import. + this._success = true; + }, + handleError: function() {}, + handleCompletion: function() { + aCallback(this._success); + } + }); + } +}; + +function Cookies() { +} + +Cookies.prototype = { + type: MigrationUtils.resourceTypes.COOKIES, + + get exists() !!this._cookiesFolder, + + __cookiesFolder: null, + get _cookiesFolder() { + // Cookies are stored in txt files, in a Cookies folder whose path varies + // across the different OS versions. CookD takes care of most of these + // cases, though, in Windows Vista/7, UAC makes a difference. + // If UAC is enabled, the most common destination is CookD/Low. Though, + // if the user runs the application in administrator mode or disables UAC, + // cookies are stored in the original CookD destination. Cause running the + // browser in administrator mode is unsafe and discouraged, we just care + // about the UAC state. + if (!this.__cookiesFolder) { + let cookiesFolder = Services.dirsvc.get("CookD", Ci.nsIFile); + if (cookiesFolder.exists() && cookiesFolder.isReadable()) { + // Check if UAC is enabled. + if (Services.appinfo.QueryInterface(Ci.nsIWinAppHelper).userCanElevate) { + cookiesFolder.append("Low"); + } + this.__cookiesFolder = cookiesFolder; + } + } + return this.__cookiesFolder; + }, + + migrate: function C_migrate(aCallback) { + CtypesHelpers.initialize(); + + let cookiesGenerator = (function genCookie() { + let success = false; + let entries = this._cookiesFolder.directoryEntries; + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsIFile); + // Skip eventual bogus entries. + if (!entry.isFile() || !/\.txt$/.test(entry.leafName)) + continue; + + this._readCookieFile(entry, function(aSuccess) { + // Importing even a single cookie file is considered a success. + if (aSuccess) + success = true; + try { + cookiesGenerator.next(); + } catch (ex) {} + }); + + yield; + } + + CtypesHelpers.finalize(); + + aCallback(success); + }).apply(this); + cookiesGenerator.next(); + }, + + _readCookieFile: function C__readCookieFile(aFile, aCallback) { + let fileReader = Cc["@mozilla.org/files/filereader;1"]. + createInstance(Ci.nsIDOMFileReader); + fileReader.addEventListener("loadend", (function onLoadEnd() { + fileReader.removeEventListener("loadend", onLoadEnd, false); + + if (fileReader.readyState != fileReader.DONE) { + Cu.reportError("Could not read cookie contents: " + fileReader.error); + aCallback(false); + return; + } + + let success = true; + try { + this._parseCookieBuffer(fileReader.result); + } catch (ex) { + Components.utils.reportError("Unable to migrate cookie: " + ex); + success = false; + } finally { + aCallback(success); + } + }).bind(this), false); + fileReader.readAsText(new File(aFile)); + }, + + /** + * Parses a cookie file buffer and returns an array of the contained cookies. + * + * The cookie file format is a newline-separated-values with a "*" used as + * delimeter between multiple records. + * Each cookie has the following fields: + * - name + * - value + * - host/path + * - flags + * - Expiration time most significant integer + * - Expiration time least significant integer + * - Creation time most significant integer + * - Creation time least significant integer + * - Record delimiter "*" + * + * @note All the times are in FILETIME format. + */ + _parseCookieBuffer: function C__parseCookieBuffer(aTextBuffer) { + // Note the last record is an empty string. + let records = [r for each (r in aTextBuffer.split("*\n")) if (r)]; + for (let record of records) { + let [name, value, hostpath, flags, + expireTimeLo, expireTimeHi] = record.split("\n"); + + // IE stores deleted cookies with a zero-length value, skip them. + if (value.length == 0) + continue; + + let hostLen = hostpath.indexOf("/"); + let host = hostpath.substr(0, hostLen); + let path = hostpath.substr(hostLen); + + // For a non-null domain, assume it's what Mozilla considers + // a domain cookie. See bug 222343. + if (host.length > 0) { + // Fist delete any possible extant matching host cookie. + Services.cookies.remove(host, name, path, false); + // Now make it a domain cookie. + if (host[0] != "." && !hostIsIPAddress(host)) + host = "." + host; + } + + let expireTime = CtypesHelpers.fileTimeToDate(Number(expireTimeHi), + Number(expireTimeLo)); + Services.cookies.add(host, + path, + name, + value, + Number(flags) & 0x1, // secure + false, // httpOnly + false, // session + expireTime); + } + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +//// Migrator + +function IEProfileMigrator() +{ +} + +IEProfileMigrator.prototype = Object.create(MigratorPrototype); + +IEProfileMigrator.prototype.getResources = function IE_getResources() { + let resources = [ + new Bookmarks() + , new History() + , new Cookies() + ]; + return [r for each (r in resources) if (r.exists)]; +}; + +Object.defineProperty(IEProfileMigrator.prototype, "sourceHomePageURL", { + get: function IE_get_sourceHomePageURL() { + let defaultStartPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + kMainKey, "Default_Page_URL"); + let startPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + kMainKey, "Start Page"); + // If the user didn't customize the Start Page, he is still on the default + // page, that may be considered the equivalent of our about:home. There's + // no reason to retain it, since it is heavily targeted to IE. + let homepage = startPage != defaultStartPage ? startPage : ""; + + // IE7+ supports secondary home pages located in a REG_MULTI_SZ key. These + // are in addition to the Start Page, and no empty entries are possible, + // thus a Start Page is always defined if any of these exists, though it + // may be the default one. + let secondaryPages = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + kMainKey, "Secondary Start Pages"); + if (secondaryPages) { + if (homepage) + secondaryPages.unshift(homepage); + homepage = secondaryPages.join("|"); + } + + return homepage; + } +}); + +IEProfileMigrator.prototype.classDescription = "IE Profile Migrator"; +IEProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=ie"; +IEProfileMigrator.prototype.classID = Components.ID("{3d2532e3-4932-4774-b7ba-968f5899d3a4}"); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([IEProfileMigrator]); diff --git a/application/palemoon/components/migration/MigrationUtils.jsm b/application/palemoon/components/migration/MigrationUtils.jsm new file mode 100644 index 000000000..fcd73a798 --- /dev/null +++ b/application/palemoon/components/migration/MigrationUtils.jsm @@ -0,0 +1,644 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = ["MigrationUtils", "MigratorPrototype"]; + +const Cu = Components.utils; +const Ci = Components.interfaces; +const Cc = Components.classes; + +const TOPIC_WILL_IMPORT_BOOKMARKS = "initial-migration-will-import-default-bookmarks"; +const TOPIC_DID_IMPORT_BOOKMARKS = "initial-migration-did-import-default-bookmarks"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Dict", + "resource://gre/modules/Dict.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils", + "resource://gre/modules/BookmarkHTMLUtils.jsm"); + +let gMigrators = null; +let gProfileStartup = null; +let gMigrationBundle = null; + +function getMigrationBundle() { + if (!gMigrationBundle) { + gMigrationBundle = Services.strings.createBundle( + "chrome://browser/locale/migration/migration.properties"); + } + return gMigrationBundle; +} + +/** + * Figure out what is the default browser, and if there is a migrator + * for it, return that migrator's internal name. + * For the time being, the "internal name" of a migraotr is its contract-id + * trailer (e.g. ie for @mozilla.org/profile/migrator;1?app=browser&type=ie), + * but it will soon be exposed properly. + */ +function getMigratorKeyForDefaultBrowser() { + const APP_DESC_TO_KEY = { + "Internet Explorer": "ie", + "Safari": "safari", + "Pale Moon web browser": "firefox", + "Google Chrome": "chrome", // Windows, Linux + "Chrome": "chrome", // OS X + }; + + let browserDesc = ""; + try { + let browserDesc = + Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService). + getApplicationDescription("http"); + return APP_DESC_TO_KEY[browserDesc] || ""; + } + catch(ex) { + Cu.reportError("Could not detect default browser: " + ex); + } + return ""; +} + +/** + * Shared prototype for migrators, implementing nsIBrowserProfileMigrator. + * + * To implement a migrator: + * 1. Import this module. + * 2. Create the prototype for the migrator, extending MigratorPrototype. + * Namely: MosaicMigrator.prototype = Object.create(MigratorPrototype); + * 3. Set classDescription, contractID and classID for your migrator, and set + * NSGetFactory appropriately. + * 4. If the migrator supports multiple profiles, override the sourceProfiles + * Here we default for single-profile migrator. + * 5. Implement getResources(aProfile) (see below). + * 6. If the migrator supports reading the home page of the source browser, + * override |sourceHomePageURL| getter. + * 7. For startup-only migrators, override |startupOnlyMigrator|. + */ +this.MigratorPrototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserProfileMigrator]), + + /** + * OVERRIDE IF AND ONLY IF the source supports multiple profiles. + * + * Returns array of profile objects from which data may be imported. The object + * should have the following keys: + * id - a unique string identifier for the profile + * name - a pretty name to display to the user in the UI + * + * Only profiles from which data can be imported should be listed. Otherwise + * the behavior of the migration wizard isn't well-defined. + * + * For a single-profile source (e.g. safari, ie), this returns null, + * and not an empty array. That is the default implementation. + */ + get sourceProfiles() null, + + /** + * MUST BE OVERRIDDEN. + * + * Returns an array of "migration resources" objects for the given profile, + * or for the "default" profile, if the migrator does not support multiple + * profiles. + * + * Each migration resource should provide: + * - a |type| getter, retunring any of the migration types (see + * nsIBrowserProfileMigrator). + * + * - a |migrate| method, taking a single argument, aCallback(bool success), + * for migrating the data for this resource. It may do its job + * synchronously or asynchronously. Either way, it must call + * aCallback(bool aSuccess) when it's done. In the case of an exception + * thrown from |migrate|, it's taken as if aCallback(false) is called. + * + * Note: In the case of a simple asynchronous implementation, you may find + * MigrationUtils.wrapMigrateFunction handy for handling aCallback easily. + * + * For each migration type listed in nsIBrowserProfileMigrator, multiple + * migration resources may be provided. This practice is useful when the + * data for a certain migration type is independently stored in few + * locations. For example, the mac version of Safari stores its "reading list" + * bookmarks in a separate property list. + * + * Note that the importation of a particular migration type is reported as + * successful if _any_ of its resources succeeded to import (that is, called, + * |aCallback(true)|). However, completion-status for a particular migration + * type is reported to the UI only once all of its migrators have called + * aCallback. + * + * @note The returned array should only include resources from which data + * can be imported. So, for example, before adding a resource for the + * BOOKMARKS migration type, you should check if you should check that the + * bookmarks file exists. + * + * @param aProfile + * The profile from which data may be imported, or an empty string + * in the case of a single-profile migrator. + * In the case of multiple-profiles migrator, it is guaranteed that + * aProfile is a value returned by the sourceProfiles getter (see + * above). + */ + getResources: function MP_getResources(aProfile) { + throw new Error("getResources must be overridden"); + }, + + /** + * OVERRIDE IF AND ONLY IF the migrator is a startup-only migrator (For now, + * that is just the Firefox migrator, see bug 737381). Default: false. + * + * Startup-only migrators are different in two ways: + * - they may only be used during startup. + * - the user-profile is half baked during migration. The folder exists, + * but it's only accessible through MigrationUtils.profileStartup. + * The migrator can call MigrationUtils.profileStartup.doStartup + * at any point in order to initialize the profile. + */ + get startupOnlyMigrator() false, + + /** + * OVERRIDE IF AND ONLY IF your migrator supports importing the homepage. + * @see nsIBrowserProfileMigrator + */ + get sourceHomePageURL() "", + + /** + * DO NOT OVERRIDE - After deCOMing migration, the UI will just call + * getResources. + * + * @see nsIBrowserProfileMigrator + */ + getMigrateData: function MP_getMigrateData(aProfile) { + let types = [r.type for each (r in this._getMaybeCachedResources(aProfile))]; + return types.reduce(function(a, b) a |= b, 0); + }, + + /** + * DO NOT OVERRIDE - After deCOMing migration, the UI will just call + * migrate for each resource. + * + * @see nsIBrowserProfileMigrator + */ + migrate: function MP_migrate(aItems, aStartup, aProfile) { + let resources = this._getMaybeCachedResources(aProfile); + if (resources.length == 0) + throw new Error("migrate called for a non-existent source"); + + if (aItems != Ci.nsIBrowserProfileMigrator.ALL) + resources = [r for each (r in resources) if (aItems & r.type)]; + + // Called either directly or through the bookmarks import callback. + function doMigrate() { + // TODO: use Map (for the items) and Set (for the resources) + // once they are iterable. + let resourcesGroupedByItems = new Dict(); + resources.forEach(function(resource) { + if (resourcesGroupedByItems.has(resource.type)) + resourcesGroupedByItems.get(resource.type).push(resource); + else + resourcesGroupedByItems.set(resource.type, [resource]); + }); + + if (resourcesGroupedByItems.count == 0) + throw new Error("No items to import"); + + let notify = function(aMsg, aItemType) { + Services.obs.notifyObservers(null, aMsg, aItemType); + } + + notify("Migration:Started"); + resourcesGroupedByItems.listkeys().forEach(function(migrationType) { + let migrationTypeA = migrationType; + let itemResources = resourcesGroupedByItems.get(migrationType); + notify("Migration:ItemBeforeMigrate", migrationType); + + let itemSuccess = false; + itemResources.forEach(function(resource) { + let resourceDone = function(aSuccess) { + let resourceIndex = itemResources.indexOf(resource); + if (resourceIndex != -1) { + itemResources.splice(resourceIndex, 1); + itemSuccess |= aSuccess; + if (itemResources.length == 0) { + resourcesGroupedByItems.del(migrationType); + notify(itemSuccess ? + "Migration:ItemAfterMigrate" : "Migration:ItemError", + migrationType); + if (resourcesGroupedByItems.count == 0) + notify("Migration:Ended"); + } + } + }; + + Services.tm.mainThread.dispatch(function() { + // If migrate throws, an error occurred, and the callback + // (itemMayBeDone) might haven't been called. + try { + resource.migrate(resourceDone); + } + catch(ex) { + Cu.reportError(ex); + resourceDone(false); + } + }, Ci.nsIThread.DISPATCH_NORMAL); + }); + }); + } + + if (MigrationUtils.isStartupMigration && !this.startupOnlyMigrator) { + MigrationUtils.profileStartup.doStartup(); + + // If we're about to migrate bookmarks, first import the default bookmarks. + // Note We do not need to do so for the Firefox migrator + // (=startupOnlyMigrator), as it just copies over the places database + // from another profile. + const BOOKMARKS = MigrationUtils.resourceTypes.BOOKMARKS; + let migratingBookmarks = resources.some(function(r) r.type == BOOKMARKS); + if (migratingBookmarks) { + let browserGlue = Cc["@mozilla.org/browser/browserglue;1"]. + getService(Ci.nsIObserver); + browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, ""); + + // Note doMigrate doesn't care about the success of the import. + let onImportComplete = function() { + browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, ""); + doMigrate(); + }; + BookmarkHTMLUtils.importFromURL( + "resource:///defaults/profile/bookmarks.html", true).then( + onImportComplete, onImportComplete); + return; + } + } + doMigrate(); + }, + + /** + * DO NOT OVERRIDE - After deCOMing migration, this code + * won't be part of the migrator itself. + * + * @see nsIBrowserProfileMigrator + */ + get sourceExists() { + if (this.startupOnlyMigrator && !MigrationUtils.isStartupMigration) + return false; + + // For a single-profile source, check if any data is available. + // For multiple-profiles source, make sure that at least one + // profile is available. + let exists = false; + try { + let profiles = this.sourceProfiles; + if (!profiles) { + let resources = this._getMaybeCachedResources(""); + if (resources && resources.length > 0) + exists = true; + } + else { + exists = profiles.length > 0; + } + } + catch(ex) { + Cu.reportError(ex); + } + return exists; + }, + + /*** PRIVATE STUFF - DO NOT OVERRIDE ***/ + _getMaybeCachedResources: function PMB__getMaybeCachedResources(aProfile) { + let profileKey = aProfile ? aProfile.id : ""; + if (this._resourcesByProfile) { + if (profileKey in this._resourcesByProfile) + return this._resourcesByProfile[profileKey]; + } + else { + this._resourcesByProfile = { }; + } + return this._resourcesByProfile[profileKey] = this.getResources(aProfile); + } +}; + +this.MigrationUtils = Object.freeze({ + resourceTypes: { + SETTINGS: Ci.nsIBrowserProfileMigrator.SETTINGS, + COOKIES: Ci.nsIBrowserProfileMigrator.COOKIES, + HISTORY: Ci.nsIBrowserProfileMigrator.HISTORY, + FORMDATA: Ci.nsIBrowserProfileMigrator.FORMDATA, + PASSWORDS: Ci.nsIBrowserProfileMigrator.PASSWORDS, + BOOKMARKS: Ci.nsIBrowserProfileMigrator.BOOKMARKS, + OTHERDATA: Ci.nsIBrowserProfileMigrator.OTHERDATA + }, + + /** + * Helper for implementing simple asynchronous cases of migration resources' + * |migrate(aCallback)| (see MigratorPrototype). If your |migrate| method + * just waits for some file to be read, for example, and then migrates + * everything right away, you can wrap the async-function with this helper + * and not worry about notifying the callback. + * + * For example, instead of writing: + * setTimeout(function() { + * try { + * .... + * aCallback(true); + * } + * catch() { + * aCallback(false); + * } + * }, 0); + * + * You may write: + * setTimeout(MigrationUtils.wrapMigrateFunction(function() { + * if (importingFromMosaic) + * throw Cr.NS_ERROR_UNEXPECTED; + * }, aCallback), 0); + * + * ... and aCallback will be called with aSuccess=false when importing + * from Mosaic, or with aSuccess=true otherwise. + * + * @param aFunction + * the function that will be called sometime later. If aFunction + * throws when it's called, aCallback(false) is called, otherwise + * aCallback(true) is called. + * @param aCallback + * the callback function passed to |migrate|. + * @return the wrapped function. + */ + wrapMigrateFunction: function MU_wrapMigrateFunction(aFunction, aCallback) { + return function() { + let success = false; + try { + aFunction.apply(null, arguments); + success = true; + } + catch(ex) { + Cu.reportError(ex); + } + // Do not change this to call aCallback directly in try try & catch + // blocks, because if aCallback throws, we may end up calling aCallback + // twice. + aCallback(success); + } + }, + + /** + * Gets a string from the migration bundle. Shorthand for + * nsIStringBundle.GetStringFromName, if aReplacements isn't passed, or for + * nsIStringBundle.formatStringFromName if it is. + * + * This method also takes care of "bumped" keys (See bug 737381 comment 8 for + * details). + * + * @param aKey + * The key of the string to retrieve. + * @param aReplacemts + * [optioanl] Array of replacements to run on the retrieved string. + * @return the retrieved string. + * + * @see nsIStringBundle + */ + getLocalizedString: function MU_getLocalizedString(aKey, aReplacements) { + const OVERRIDES = { + "4_firefox": "4_firefox_history_and_bookmarks", + "64_firefox": "64_firefox_other" + }; + aKey = OVERRIDES[aKey] || aKey; + + if (aReplacements === undefined) + return getMigrationBundle().GetStringFromName(aKey); + return getMigrationBundle().formatStringFromName( + aKey, aReplacements, aReplacements.length); + }, + + /** + * Helper for creating a folder for imported bookmarks from a particular + * migration source. The folder is created at the end of the given folder. + * + * @param aSourceNameStr + * the source name (first letter capitalized). This is used + * for reading the localized source name from the migration + * bundle (e.g. if aSourceNameStr is Mosaic, this will try to read + * sourceNameMosaic from the migration bundle). + * @param aParentId + * the item-id of the folder in which the new folder should be + * created. + * @return the item-id of the new folder. + */ + createImportedBookmarksFolder: + function MU_createImportedBookmarksFolder(aSourceNameStr, aParentId) { + let source = this.getLocalizedString("sourceName" + aSourceNameStr); + let label = this.getLocalizedString("importedBookmarksFolder", [source]); + return PlacesUtils.bookmarks.createFolder( + aParentId, label, PlacesUtils.bookmarks.DEFAULT_INDEX); + }, + + get _migrators() gMigrators ? gMigrators : gMigrators = new Dict(), + + /* + * Returns the migrator for the given source, if any data is available + * for this source, or null otherwise. + * + * @param aKey internal name of the migration source. + * Supported values: ie (windows), + * safari (mac/windows), + * chrome (mac/windows/linux), + * firefox. + * + * If null is returned, either no data can be imported + * for the given migrator, or aMigratorKey is invalid (e.g. ie on mac, + * or mosaic everywhere). This method should be used rather than direct + * getService for future compatibility (see bug 718280). + * + * @return profile migrator implementing nsIBrowserProfileMigrator, if it can + * import any data, null otherwise. + */ + getMigrator: function MU_getMigrator(aKey) { + let migrator = null; + if (this._migrators.has(aKey)) { + migrator = this._migrators.get(aKey); + } + else { + try { + migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=" + + aKey].createInstance(Ci.nsIBrowserProfileMigrator); + } + catch(ex) { } + this._migrators.set(aKey, migrator); + } + + return migrator && migrator.sourceExists ? migrator : null; + }, + + // Iterates the available migrators, in the most suitable + // order for the running platform. + get migrators() { + let migratorKeysOrdered = [ +#ifdef XP_WIN + "firefox", "ie", "chrome", "safari" +#elifdef XP_MACOSX + "firefox", "safari", "chrome" +#elifdef XP_UNIX + "firefox", "chrome" +#endif + ]; + + // If a supported default browser is found check it first + // so that the wizard defaults to import from that browser. + let defaultBrowserKey = getMigratorKeyForDefaultBrowser(); + if (defaultBrowserKey) + migratorKeysOrdered.sort(function (a, b) b == defaultBrowserKey ? 1 : 0); + + for (let migratorKey of migratorKeysOrdered) { + let migrator = this.getMigrator(migratorKey); + if (migrator) + yield migrator; + } + }, + + // Whether or not we're in the process of startup migration + get isStartupMigration() gProfileStartup != null, + + /** + * In the case of startup migration, this is set to the nsIProfileStartup + * instance passed to ProfileMigrator's migrate. + * + * @see showMigrationWizard + */ + get profileStartup() gProfileStartup, + + /** + * Show the migration wizard. On mac, this may just focus the wizard if it's + * already running, in which case aOpener and aParams are ignored. + * + * @param [optional] aOpener + * the window that asks to open the wizard. + * @param [optional] aParams + * arguments for the migration wizard, in the form of an nsIArray. + * This is passed as-is for the params argument of + * nsIWindowWatcher.openWindow. + */ + showMigrationWizard: + function MU_showMigrationWizard(aOpener, aParams) { + let features = "chrome,dialog,modal,centerscreen,titlebar,resizable=no"; +#ifdef XP_MACOSX + if (!this.isStartupMigration) { + let win = Services.wm.getMostRecentWindow("Browser:MigrationWizard"); + if (win) { + win.focus(); + return; + } + // On mac, the migration wiazrd should only be modal in the case of + // startup-migration. + features = "centerscreen,chrome,resizable=no"; + } +#endif + + Services.ww.openWindow(aOpener, + "chrome://browser/content/migration/migration.xul", + "_blank", + features, + aParams); + }, + + /** + * Show the migration wizard for startup-migration. This should only be + * called by ProfileMigrator (see ProfileMigrator.js), which implements + * nsIProfileMigrator. + * + * @param aProfileStartup + * the nsIProfileStartup instance provided to ProfileMigrator.migrate. + * @param [optional] aMigratorKey + * If set, the migration wizard will import from the corresponding + * migrator, bypassing the source-selection page. Otherwise, the + * source-selection page will be displayed, either with the default + * browser selected, if it could be detected and if there is a + * migrator for it, or with the first option selected as a fallback + * (The first option is hardcoded to be the most common browser for + * the OS we run on. See migration.xul). + * @param [optional] aProfileToMigrate + * If set, the migration wizard will import from the profile indicated. + * + * @throws if aMigratorKey is invalid or if it points to a non-existent + * source. + */ + startupMigration: + function MU_startupMigrator(aProfileStartup, aMigratorKey, aProfileToMigrate) { + if (!aProfileStartup) { + throw new Error("a profile-startup instance is required for startup-migration"); + } + gProfileStartup = aProfileStartup; + + let skipSourcePage = false, migrator = null, migratorKey = ""; + if (aMigratorKey) { + migrator = this.getMigrator(aMigratorKey); + if (!migrator) { + // aMigratorKey must point to a valid source, so, if it doesn't + // cleanup and throw. + this.finishMigration(); + throw new Error("startMigration was asked to open auto-migrate from " + + "a non-existent source: " + aMigratorKey); + } + migratorKey = aMigratorKey; + skipSourcePage = true; + } + else { + let defaultBrowserKey = getMigratorKeyForDefaultBrowser(); + if (defaultBrowserKey) { + migrator = this.getMigrator(defaultBrowserKey); + if (migrator) + migratorKey = defaultBrowserKey; + } + } + + if (!migrator) { + // If there's no migrator set so far, ensure that there is at least one + // migrator available before opening the wizard. + try { + this.migrators.next(); + } + catch(ex) { + this.finishMigration(); + if (!(ex instanceof StopIteration)) + throw ex; + return; + } + } + + let params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + let keyCSTR = Cc["@mozilla.org/supports-cstring;1"]. + createInstance(Ci.nsISupportsCString); + keyCSTR.data = migratorKey; + let skipImportSourcePageBool = Cc["@mozilla.org/supports-PRBool;1"]. + createInstance(Ci.nsISupportsPRBool); + skipImportSourcePageBool.data = skipSourcePage; + let profileToMigrate = null; + if (aProfileToMigrate) { + profileToMigrate = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + profileToMigrate.data = aProfileToMigrate; + } + params.appendElement(keyCSTR, false); + params.appendElement(migrator, false); + params.appendElement(aProfileStartup, false); + params.appendElement(skipImportSourcePageBool, false); + params.appendElement(profileToMigrate, false); + + this.showMigrationWizard(null, params); + }, + + /** + * Cleans up references to migrators and nsIProfileInstance instances. + */ + finishMigration: function MU_finishMigration() { + gMigrators = null; + gProfileStartup = null; + gMigrationBundle = null; + } +}); diff --git a/application/palemoon/components/migration/ProfileMigrator.js b/application/palemoon/components/migration/ProfileMigrator.js new file mode 100644 index 000000000..f67823bae --- /dev/null +++ b/application/palemoon/components/migration/ProfileMigrator.js @@ -0,0 +1,21 @@ +/* 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"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource:///modules/MigrationUtils.jsm"); + +function ProfileMigrator() { +} + +ProfileMigrator.prototype = { + migrate: MigrationUtils.startupMigration.bind(MigrationUtils), + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProfileMigrator]), + classDescription: "Profile Migrator", + contractID: "@mozilla.org/toolkit/profile-migrator;1", + classID: Components.ID("6F8BB968-C14F-4D6F-9733-6C6737B35DCE") +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ProfileMigrator]); diff --git a/application/palemoon/components/migration/SafariProfileMigrator.js b/application/palemoon/components/migration/SafariProfileMigrator.js new file mode 100644 index 000000000..70804793d --- /dev/null +++ b/application/palemoon/components/migration/SafariProfileMigrator.js @@ -0,0 +1,416 @@ +/* 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"; + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/MigrationUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PropertyListUtils", + "resource://gre/modules/PropertyListUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); + +function Bookmarks(aBookmarksFile) { + this._file = aBookmarksFile; +} +Bookmarks.prototype = { + type: MigrationUtils.resourceTypes.BOOKMARKS, + + migrate: function B_migrate(aCallback) { + PropertyListUtils.read(this._file, + MigrationUtils.wrapMigrateFunction(function migrateBookmarks(aDict) { + if (!aDict) + throw new Error("Could not read Bookmarks.plist"); + + let children = aDict.get("Children");; + if (!children) + throw new Error("Invalid Bookmarks.plist format"); + + PlacesUtils.bookmarks.runInBatchMode({ + runBatched: function() { + let collection = aDict.get("Title") == "com.apple.ReadingList" ? + this.READING_LIST_COLLECTION : this.ROOT_COLLECTION; + this._migrateCollection(children, collection); + }.bind(this) + }, null); + }.bind(this), aCallback)); + }, + + // Bookmarks collections in Safari. Constants for migrateCollection. + ROOT_COLLECTION: 0, + MENU_COLLECTION: 1, + TOOLBAR_COLLECTION: 2, + READING_LIST_COLLECTION: 3, + + /** + * Recursively migrate a Safari collection of bookmarks. + * + * @param aEntries + * the collection's children + * @param aCollection + * one of the values above. + */ + _migrateCollection: function B__migrateCollection(aEntries, aCollection) { + // A collection of bookmarks in Safari resembles places roots. In the + // property list files (Bookmarks.plist, ReadingList.plist) they are + // stored as regular bookmarks folders, and thus can only be distinguished + // from by their names and places in the hierarchy. + + let entriesFiltered = []; + if (aCollection == this.ROOT_COLLECTION) { + for (let entry of aEntries) { + let type = entry.get("WebBookmarkType"); + if (type == "WebBookmarkTypeList" && entry.has("Children")) { + let title = entry.get("Title"); + let children = entry.get("Children"); + if (title == "BookmarksBar") + this._migrateCollection(children, this.TOOLBAR_COLLECTION); + else if (title == "BookmarksMenu") + this._migrateCollection(children, this.MENU_COLLECTION); + else if (title == "com.apple.ReadingList") + this._migrateCollection(children, this.READING_LIST_COLLECTION); + else if (entry.get("ShouldOmitFromUI") !== true) + entriesFiltered.push(entry); + } + else if (type == "WebBookmarkTypeLeaf") { + entriesFiltered.push(entry); + } + } + } + else { + entriesFiltered = aEntries; + } + + if (entriesFiltered.length == 0) + return; + + let folder = -1; + switch (aCollection) { + case this.ROOT_COLLECTION: { + // In Safari, it is possible (though quite cumbersome) to move + // bookmarks to the bookmarks root, which is the parent folder of + // all bookmarks "collections". That is somewhat in parallel with + // both the places root and the unfiled-bookmarks root. + // Because the former is only an implementation detail in our UI, + // the unfiled root seems to be the best choice. + folder = PlacesUtils.unfiledBookmarksFolderId; + break; + } + case this.MENU_COLLECTION: { + folder = PlacesUtils.bookmarksMenuFolderId; + if (!MigrationUtils.isStartupMigration) { + folder = MigrationUtils.createImportedBookmarksFolder("Safari", + folder); + } + break; + } + case this.TOOLBAR_COLLECTION: { + folder = PlacesUtils.toolbarFolderId; + if (!MigrationUtils.isStartupMigration) { + folder = MigrationUtils.createImportedBookmarksFolder("Safari", + folder); + } + break; + } + case this.READING_LIST_COLLECTION: { + // Reading list items are imported as regular bookmarks. + // They are imported under their own folder, created either under the + // bookmarks menu (in the case of startup migration). + folder = PlacesUtils.bookmarks.createFolder( + PlacesUtils.bookmarksMenuFolderId, + MigrationUtils.getLocalizedString("importedSafariReadingList"), + PlacesUtils.bookmarks.DEFAULT_INDEX); + break; + } + default: + throw new Error("Unexpected value for aCollection!"); + } + + this._migrateEntries(entriesFiltered, folder); + }, + + // migrate the given array of safari bookmarks to the given places + // folder. + _migrateEntries: function B__migrateEntries(aEntries, aFolderId) { + for (let entry of aEntries) { + let type = entry.get("WebBookmarkType"); + if (type == "WebBookmarkTypeList" && entry.has("Children")) { + let title = entry.get("Title"); + let folderId = PlacesUtils.bookmarks.createFolder( + aFolderId, title, PlacesUtils.bookmarks.DEFAULT_INDEX); + + // Empty folders may not have a children array. + if (entry.has("Children")) + this._migrateEntries(entry.get("Children"), folderId, false); + } + else if (type == "WebBookmarkTypeLeaf" && entry.has("URLString")) { + let title, uri; + if (entry.has("URIDictionary")) + title = entry.get("URIDictionary").get("title"); + + try { + uri = NetUtil.newURI(entry.get("URLString")); + } + catch(ex) { + Cu.reportError("Invalid uri set for Safari bookmark: " + entry.get("URLString")); + } + if (uri) { + PlacesUtils.bookmarks.insertBookmark(aFolderId, uri, + PlacesUtils.bookmarks.DEFAULT_INDEX, title); + } + } + } + } +}; + +function History(aHistoryFile) { + this._file = aHistoryFile; +} +History.prototype = { + type: MigrationUtils.resourceTypes.HISTORY, + + // Helper method for converting the visit date property to a PRTime value. + // The visit date is stored as a string, so it's not read as a Date + // object by PropertyListUtils. + _parseCocoaDate: function H___parseCocoaDate(aCocoaDateStr) { + let asDouble = parseFloat(aCocoaDateStr); + if (!isNaN(asDouble)) { + // reference date of NSDate. + let date = new Date("1 January 2001, GMT"); + date.setMilliseconds(asDouble * 1000); + return date * 1000; + } + return 0; + }, + + migrate: function H_migrate(aCallback) { + PropertyListUtils.read(this._file, function migrateHistory(aDict) { + try { + if (!aDict) + throw new Error("Could not read history property list"); + if (!aDict.has("WebHistoryDates")) + throw new Error("Unexpected history-property list format"); + + // Safari's History file contains only top-level urls. It does not + // distinguish between typed urls and linked urls. + let transType = PlacesUtils.history.TRANSITION_LINK; + + let places = []; + let entries = aDict.get("WebHistoryDates"); + for (let entry of entries) { + if (entry.has("lastVisitedDate")) { + let visitDate = this._parseCocoaDate(entry.get("lastVisitedDate")); + places.push({ uri: NetUtil.newURI(entry.get("")), + title: entry.get("title"), + visits: [{ transitionType: transType, + visitDate: visitDate }] }); + } + } + if (places.length > 0) { + PlacesUtils.asyncHistory.updatePlaces(places, { + _success: false, + handleResult: function() { + // Importing any entry is considered a successful import. + this._success = true; + }, + handleError: function() {}, + handleCompletion: function() { + aCallback(this._success); + } + }); + } + else { + aCallback(false); + } + } + catch(ex) { + Cu.reportError(ex); + aCallback(false); + } + }.bind(this)); + } +}; + +/** + * Safari's preferences property list is independently used for three purposes: + * (a) importation of preferences + * (b) importation of search strings + * (c) retrieving the home page. + * + * So, rather than reading it three times, it's cached and managed here. + */ +function MainPreferencesPropertyList(aPreferencesFile) { + this._file = aPreferencesFile; + this._callbacks = []; +} +MainPreferencesPropertyList.prototype = { + /** + * @see PropertyListUtils.read + */ + read: function MPPL_read(aCallback) { + if ("_dict" in this) { + aCallback(this._dict); + return; + } + + let alreadyReading = this._callbacks.length > 0; + this._callbacks.push(aCallback); + if (!alreadyReading) { + PropertyListUtils.read(this._file, function readPrefs(aDict) { + this._dict = aDict; + for (let callback of this._callbacks) { + try { + callback(aDict); + } + catch(ex) { + Cu.reportError(ex); + } + } + this._callbacks.splice(0); + }.bind(this)); + } + }, + + // Workaround for nsIBrowserProfileMigrator.sourceHomePageURL until + // it's replaced with an async method. + _readSync: function MPPL__readSync() { + if ("_dict" in this) + return this._dict; + + let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + inputStream.init(this._file, -1, -1, 0); + let binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + binaryStream.setInputStream(inputStream); + let bytes = binaryStream.readByteArray(inputStream.available()); + this._dict = PropertyListUtils._readFromArrayBufferSync( + Uint8Array(bytes).buffer); + return this._dict; + } +}; + +function SearchStrings(aMainPreferencesPropertyListInstance) { + this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance; +} +SearchStrings.prototype = { + type: MigrationUtils.resourceTypes.OTHERDATA, + + migrate: function SS_migrate(aCallback) { + this._mainPreferencesPropertyList.read(MigrationUtils.wrapMigrateFunction( + function migrateSearchStrings(aDict) { + if (!aDict) + throw new Error("Could not get preferences dictionary"); + + if (aDict.has("RecentSearchStrings")) { + let recentSearchStrings = aDict.get("RecentSearchStrings"); + if (recentSearchStrings && recentSearchStrings.length > 0) { + let changes = [{op: "add", + fieldname: "searchbar-history", + value: searchString} + for (searchString of recentSearchStrings)]; + FormHistory.update(changes); + } + } + }.bind(this), aCallback)); + } +}; + +function SafariProfileMigrator() { +} + +SafariProfileMigrator.prototype = Object.create(MigratorPrototype); + +SafariProfileMigrator.prototype.getResources = function SM_getResources() { + let profileDir = +#ifdef XP_MACOSX + FileUtils.getDir("ULibDir", ["Safari"], false); +#else + FileUtils.getDir("AppData", ["Apple Computer", "Safari"], false); +#endif + if (!profileDir.exists()) + return null; + + let resources = []; + let pushProfileFileResource = function(aFileName, aConstructor) { + let file = profileDir.clone(); + file.append(aFileName); + if (file.exists()) + resources.push(new aConstructor(file)); + }; + + pushProfileFileResource("History.plist", History); + pushProfileFileResource("Bookmarks.plist", Bookmarks); + + // The Reading List feature was introduced at the same time in Windows and + // Mac versions of Safari. Not surprisingly, they are stored in the same + // format in both versions. Surpsingly, only on Windows there is a + // separate property list for it. This isn't #ifdefed out on mac, because + // Apple may fix this at some point. + pushProfileFileResource("ReadingList.plist", Bookmarks); + + let prefsDir = +#ifdef XP_MACOSX + FileUtils.getDir("UsrPrfs", [], false); +#else + FileUtils.getDir("AppData", ["Apple Computer", "Preferences"], false); +#endif + + let prefs = this.mainPreferencesPropertyList; + if (prefs) { + resources.push(new SearchStrings(prefs)); + } + + return resources; +}; + +Object.defineProperty(SafariProfileMigrator.prototype, "mainPreferencesPropertyList", { + get: function get_mainPreferencesPropertyList() { + if (this._mainPreferencesPropertyList === undefined) { + let file = +#ifdef XP_MACOSX + FileUtils.getDir("UsrPrfs", [], false); +#else + FileUtils.getDir("AppData", ["Apple Computer", "Preferences"], false); +#endif + if (file.exists()) { + file.append("com.apple.Safari.plist"); + if (file.exists()) { + return this._mainPreferencesPropertyList = + new MainPreferencesPropertyList(file); + } + } + return this._mainPreferencesPropertyList = null; + } + return this._mainPreferencesPropertyList; + } +}); + +Object.defineProperty(SafariProfileMigrator.prototype, "sourceHomePageURL", { + get: function get_sourceHomePageURL() { + if (this.mainPreferencesPropertyList) { + let dict = this.mainPreferencesPropertyList._readSync(); + if (dict.has("HomePage")) + return dict.get("HomePage"); + } + return ""; + } +}); + +SafariProfileMigrator.prototype.classDescription = "Safari Profile Migrator"; +SafariProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=safari"; +SafariProfileMigrator.prototype.classID = Components.ID("{4b609ecf-60b2-4655-9df4-dc149e474da1}"); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SafariProfileMigrator]); diff --git a/application/palemoon/components/migration/content/migration.js b/application/palemoon/components/migration/content/migration.js new file mode 100644 index 000000000..512197b8b --- /dev/null +++ b/application/palemoon/components/migration/content/migration.js @@ -0,0 +1,474 @@ +/* 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/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const kIMig = Ci.nsIBrowserProfileMigrator; +const kIPStartup = Ci.nsIProfileStartup; + +Cu.import("resource:///modules/MigrationUtils.jsm"); + +var MigrationWizard = { + _source: "", // Source Profile Migrator ContractID suffix + _itemsFlags: kIMig.ALL, // Selected Import Data Sources (16-bit bitfield) + _selectedProfile: null, // Selected Profile name to import from + _wiz: null, + _migrator: null, + _autoMigrate: null, + + init: function () + { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(this, "Migration:Started", false); + os.addObserver(this, "Migration:ItemBeforeMigrate", false); + os.addObserver(this, "Migration:ItemAfterMigrate", false); + os.addObserver(this, "Migration:ItemError", false); + os.addObserver(this, "Migration:Ended", false); + + this._wiz = document.documentElement; + + if ("arguments" in window && window.arguments.length > 1) { + this._source = window.arguments[0]; + this._migrator = window.arguments[1] instanceof kIMig ? + window.arguments[1] : null; + this._autoMigrate = window.arguments[2].QueryInterface(kIPStartup); + this._skipImportSourcePage = window.arguments[3]; + if (this._migrator && window.arguments[4]) { + let sourceProfiles = this._migrator.sourceProfiles; + this._selectedProfile = sourceProfiles.find(profile => profile.id == window.arguments[4]); + } + + if (this._autoMigrate) { + // Show the "nothing" option in the automigrate case to provide an + // easily identifiable way to avoid migration and create a new profile. + var nothing = document.getElementById("nothing"); + nothing.hidden = false; + } + } + + this.onImportSourcePageShow(); + }, + + uninit: function () + { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.removeObserver(this, "Migration:Started"); + os.removeObserver(this, "Migration:ItemBeforeMigrate"); + os.removeObserver(this, "Migration:ItemAfterMigrate"); + os.removeObserver(this, "Migration:ItemError"); + os.removeObserver(this, "Migration:Ended"); + MigrationUtils.finishMigration(); + }, + + // 1 - Import Source + onImportSourcePageShow: function () + { + this._wiz.canRewind = false; + + var selectedMigrator = null; + + // Figure out what source apps are are available to import from: + var group = document.getElementById("importSourceGroup"); + for (var i = 0; i < group.childNodes.length; ++i) { + var migratorKey = group.childNodes[i].id; + if (migratorKey != "nothing") { + var migrator = MigrationUtils.getMigrator(migratorKey); + if (migrator) { + // Save this as the first selectable item, if we don't already have + // one, or if it is the migrator that was passed to us. + if (!selectedMigrator || this._source == migratorKey) + selectedMigrator = group.childNodes[i]; + } else { + // Hide this option + group.childNodes[i].hidden = true; + } + } + } + + if (selectedMigrator) + group.selectedItem = selectedMigrator; + else { + // We didn't find a migrator, notify the user + document.getElementById("noSources").hidden = false; + + this._wiz.canAdvance = false; + + document.getElementById("importBookmarks").hidden = true; + document.getElementById("importAll").hidden = true; + } + + // Advance to the next page if the caller told us to. + if (this._migrator && this._skipImportSourcePage) { + this._wiz.advance(); + this._wiz.canRewind = false; + } + }, + + onImportSourcePageAdvanced: function () + { + var newSource = document.getElementById("importSourceGroup").selectedItem.id; + + if (newSource == "nothing") { + document.documentElement.cancel(); + return false; + } + + if (!this._migrator || (newSource != this._source)) { + // Create the migrator for the selected source. + this._migrator = MigrationUtils.getMigrator(newSource); + + this._itemsFlags = kIMig.ALL; + this._selectedProfile = null; + } + this._source = newSource; + + // check for more than one source profile + var sourceProfiles = this._migrator.sourceProfiles; + if (this._skipImportSourcePage) { + this._wiz.currentPage.next = "homePageImport"; + } + else if (sourceProfiles && sourceProfiles.length > 1) { + this._wiz.currentPage.next = "selectProfile"; + } + else { + if (this._autoMigrate) + this._wiz.currentPage.next = "homePageImport"; + else + this._wiz.currentPage.next = "importItems"; + + if (sourceProfiles && sourceProfiles.length == 1) + this._selectedProfile = sourceProfiles[0]; + else + this._selectedProfile = null; + } + }, + + // 2 - [Profile Selection] + onSelectProfilePageShow: function () + { + // Disabling this for now, since we ask about import sources in automigration + // too and don't want to disable the back button + // if (this._autoMigrate) + // document.documentElement.getButton("back").disabled = true; + + var profiles = document.getElementById("profiles"); + while (profiles.hasChildNodes()) + profiles.removeChild(profiles.firstChild); + + // Note that this block is still reached even if the user chose 'From File' + // and we canceled the dialog. When that happens, _migrator will be null. + if (this._migrator) { + var sourceProfiles = this._migrator.sourceProfiles; + + for (let profile of sourceProfiles) { + var item = document.createElement("radio"); + item.id = profile.id; + item.setAttribute("label", profile.name); + profiles.appendChild(item); + } + } + + profiles.selectedItem = this._selectedProfile ? document.getElementById(this._selectedProfile.id) : profiles.firstChild; + }, + + onSelectProfilePageRewound: function () + { + var profiles = document.getElementById("profiles"); + this._selectedProfile = this._migrator.sourceProfiles.find( + profile => profile.id == profiles.selectedItem.id + ) || null; + }, + + onSelectProfilePageAdvanced: function () + { + var profiles = document.getElementById("profiles"); + this._selectedProfile = this._migrator.sourceProfiles.find( + profile => profile.id == profiles.selectedItem.id + ) || null; + + // If we're automigrating or just doing bookmarks don't show the item selection page + if (this._autoMigrate) + this._wiz.currentPage.next = "homePageImport"; + }, + + // 3 - ImportItems + onImportItemsPageShow: function () + { + var dataSources = document.getElementById("dataSources"); + while (dataSources.hasChildNodes()) + dataSources.removeChild(dataSources.firstChild); + + var items = this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate); + for (var i = 0; i < 16; ++i) { + var itemID = (items >> i) & 0x1 ? Math.pow(2, i) : 0; + if (itemID > 0) { + var checkbox = document.createElement("checkbox"); + checkbox.id = itemID; + checkbox.setAttribute("label", + MigrationUtils.getLocalizedString(itemID + "_" + this._source)); + dataSources.appendChild(checkbox); + if (!this._itemsFlags || this._itemsFlags & itemID) + checkbox.checked = true; + } + } + }, + + onImportItemsPageRewound: function () + { + this._wiz.canAdvance = true; + this.onImportItemsPageAdvanced(); + }, + + onImportItemsPageAdvanced: function () + { + var dataSources = document.getElementById("dataSources"); + this._itemsFlags = 0; + for (var i = 0; i < dataSources.childNodes.length; ++i) { + var checkbox = dataSources.childNodes[i]; + if (checkbox.localName == "checkbox" && checkbox.checked) + this._itemsFlags |= parseInt(checkbox.id); + } + }, + + onImportItemCommand: function (aEvent) + { + var items = document.getElementById("dataSources"); + var checkboxes = items.getElementsByTagName("checkbox"); + + var oneChecked = false; + for (var i = 0; i < checkboxes.length; ++i) { + if (checkboxes[i].checked) { + oneChecked = true; + break; + } + } + + this._wiz.canAdvance = oneChecked; + }, + + // 4 - Home Page Selection + onHomePageMigrationPageShow: function () + { + // only want this on the first run + if (!this._autoMigrate) { + this._wiz.advance(); + return; + } + + var brandBundle = document.getElementById("brandBundle"); + // These strings don't exist when not using official branding. If that's + // the case, just skip this page. + try { + var pageTitle = brandBundle.getString("homePageMigrationPageTitle"); + var pageDesc = brandBundle.getString("homePageMigrationDescription"); + var mainStr = brandBundle.getString("homePageSingleStartMain"); + } + catch (e) { + this._wiz.advance(); + return; + } + + document.getElementById("homePageImport").setAttribute("label", pageTitle); + document.getElementById("homePageImportDesc").setAttribute("value", pageDesc); + + this._wiz._adjustWizardHeader(); + + var singleStart = document.getElementById("homePageSingleStart"); + singleStart.setAttribute("label", mainStr); + singleStart.setAttribute("value", "DEFAULT"); + + var source = null; + switch (this._source) { + case "ie": + source = "sourceNameIE"; + break; + case "safari": + source = "sourceNameSafari"; + break; + case "chrome": + source = "sourceNameChrome"; + break; + case "firefox": + source = "sourceNameFirefox"; + break; + } + + // semi-wallpaper for crash when multiple profiles exist, since we haven't initialized mSourceProfile in places + this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate); + + var oldHomePageURL = this._migrator.sourceHomePageURL; + + if (oldHomePageURL && source) { + var appName = MigrationUtils.getLocalizedString(source); + var oldHomePageLabel = + brandBundle.getFormattedString("homePageImport", [appName]); + var oldHomePage = document.getElementById("oldHomePage"); + oldHomePage.setAttribute("label", oldHomePageLabel); + oldHomePage.setAttribute("value", oldHomePageURL); + oldHomePage.removeAttribute("hidden"); + } + else { + // if we don't have at least two options, just advance + this._wiz.advance(); + } + }, + + onHomePageMigrationPageAdvanced: function () + { + // we might not have a selectedItem if we're in fallback mode + try { + var radioGroup = document.getElementById("homePageRadiogroup"); + + this._newHomePage = radioGroup.selectedItem.value; + } catch(ex) {} + }, + + // 5 - Migrating + onMigratingPageShow: function () + { + this._wiz.getButton("cancel").disabled = true; + this._wiz.canRewind = false; + this._wiz.canAdvance = false; + + // When automigrating, show all of the data that can be received from this source. + if (this._autoMigrate) + this._itemsFlags = this._migrator.getMigrateData(this._selectedProfile, this._autoMigrate); + + this._listItems("migratingItems"); + setTimeout(this.onMigratingMigrate, 0, this); + }, + + onMigratingMigrate: function (aOuter) + { + aOuter._migrator.migrate(aOuter._itemsFlags, aOuter._autoMigrate, aOuter._selectedProfile); + }, + + _listItems: function (aID) + { + var items = document.getElementById(aID); + while (items.hasChildNodes()) + items.removeChild(items.firstChild); + + var brandBundle = document.getElementById("brandBundle"); + var itemID; + for (var i = 0; i < 16; ++i) { + var itemID = (this._itemsFlags >> i) & 0x1 ? Math.pow(2, i) : 0; + if (itemID > 0) { + var label = document.createElement("label"); + label.id = itemID + "_migrated"; + try { + label.setAttribute("value", + MigrationUtils.getLocalizedString(itemID + "_" + this._source)); + items.appendChild(label); + } + catch (e) { + // if the block above throws, we've enumerated all the import data types we + // currently support and are now just wasting time, break. + break; + } + } + } + }, + + observe: function (aSubject, aTopic, aData) + { + switch (aTopic) { + case "Migration:Started": + break; + case "Migration:ItemBeforeMigrate": + var label = document.getElementById(aData + "_migrated"); + if (label) + label.setAttribute("style", "font-weight: bold"); + break; + case "Migration:ItemAfterMigrate": + var label = document.getElementById(aData + "_migrated"); + if (label) + label.removeAttribute("style"); + break; + case "Migration:Ended": + if (this._autoMigrate) { + if (this._newHomePage) { + try { + // set homepage properly + var prefSvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + var prefBranch = prefSvc.getBranch(null); + + if (this._newHomePage == "DEFAULT") { + prefBranch.clearUserPref("browser.startup.homepage"); + } + else { + var str = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + str.data = this._newHomePage; + prefBranch.setComplexValue("browser.startup.homepage", + Components.interfaces.nsISupportsString, + str); + } + + var dirSvc = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + var prefFile = dirSvc.get("ProfDS", Components.interfaces.nsIFile); + prefFile.append("prefs.js"); + prefSvc.savePrefFile(prefFile); + } catch(ex) { + dump(ex); + } + } + + // We're done now. + this._wiz.canAdvance = true; + this._wiz.advance(); + + setTimeout(close, 5000); + } + else { + this._wiz.canAdvance = true; + var nextButton = this._wiz.getButton("next"); + nextButton.click(); + } + break; + case "Migration:ItemError": + var type = "undefined"; + switch (parseInt(aData)) { + case Ci.nsIBrowserProfileMigrator.SETTINGS: + type = "settings"; + break; + case Ci.nsIBrowserProfileMigrator.COOKIES: + type = "cookies"; + break; + case Ci.nsIBrowserProfileMigrator.HISTORY: + type = "history"; + break; + case Ci.nsIBrowserProfileMigrator.FORMDATA: + type = "form data"; + break; + case Ci.nsIBrowserProfileMigrator.PASSWORDS: + type = "passwords"; + break; + case Ci.nsIBrowserProfileMigrator.BOOKMARKS: + type = "bookmarks"; + break; + case Ci.nsIBrowserProfileMigrator.OTHERDATA: + type = "misc. data"; + break; + } + Cc["@mozilla.org/consoleservice;1"] + .getService(Ci.nsIConsoleService) + .logStringMessage("some " + type + " did not successfully migrate."); + break; + } + }, + + onDonePageShow: function () + { + this._wiz.getButton("cancel").disabled = true; + this._wiz.canRewind = false; + this._listItems("doneItems"); + } +}; diff --git a/application/palemoon/components/migration/content/migration.xul b/application/palemoon/components/migration/content/migration.xul new file mode 100644 index 000000000..2c8df7b3e --- /dev/null +++ b/application/palemoon/components/migration/content/migration.xul @@ -0,0 +1,96 @@ + +# 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/. + + + + + + + +