summaryrefslogtreecommitdiffstats
path: root/browser/components/migration
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/migration')
-rw-r--r--browser/components/migration/.eslintrc.js82
-rw-r--r--browser/components/migration/360seProfileMigrator.js328
-rw-r--r--browser/components/migration/AutoMigrate.jsm670
-rw-r--r--browser/components/migration/BrowserProfileMigrators.manifest33
-rw-r--r--browser/components/migration/ChromeProfileMigrator.js557
-rw-r--r--browser/components/migration/ESEDBReader.jsm590
-rw-r--r--browser/components/migration/EdgeProfileMigrator.js450
-rw-r--r--browser/components/migration/FirefoxProfileMigrator.js255
-rw-r--r--browser/components/migration/IEProfileMigrator.js542
-rw-r--r--browser/components/migration/MSMigrationUtils.jsm889
-rw-r--r--browser/components/migration/MigrationUtils.jsm1117
-rw-r--r--browser/components/migration/ProfileMigrator.js21
-rw-r--r--browser/components/migration/SafariProfileMigrator.js650
-rw-r--r--browser/components/migration/content/aboutWelcomeBack.xhtml82
-rw-r--r--browser/components/migration/content/extra-migration-strings.properties14
-rw-r--r--browser/components/migration/content/migration.js549
-rw-r--r--browser/components/migration/content/migration.xul109
-rw-r--r--browser/components/migration/jar.mn9
-rw-r--r--browser/components/migration/moz.build62
-rw-r--r--browser/components/migration/nsIBrowserProfileMigrator.idl77
-rw-r--r--browser/components/migration/nsIEHistoryEnumerator.cpp120
-rw-r--r--browser/components/migration/nsIEHistoryEnumerator.h37
-rw-r--r--browser/components/migration/nsWindowsMigrationUtils.h36
-rw-r--r--browser/components/migration/tests/browser/.eslintrc.js9
-rw-r--r--browser/components/migration/tests/browser/browser.ini3
-rw-r--r--browser/components/migration/tests/browser/browser_undo_notification.js67
-rw-r--r--browser/components/migration/tests/browser/browser_undo_notification_multiple_dismissal.js122
-rw-r--r--browser/components/migration/tests/browser/browser_undo_notification_wording.js67
-rw-r--r--browser/components/migration/tests/marionette/manifest.ini5
-rw-r--r--browser/components/migration/tests/marionette/test_refresh_firefox.py416
-rw-r--r--browser/components/migration/tests/unit/.eslintrc.js7
-rw-r--r--browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Databin0 -> 22528 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookiesbin0 -> 10240 bytes
-rw-r--r--browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State22
-rw-r--r--browser/components/migration/tests/unit/Library/Safari/Bookmarks.plistbin0 -> 1860 bytes
-rw-r--r--browser/components/migration/tests/unit/head_migration.js69
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_cookies.js51
-rw-r--r--browser/components/migration/tests/unit/test_Chrome_passwords.js219
-rw-r--r--browser/components/migration/tests/unit/test_Edge_availability.js20
-rw-r--r--browser/components/migration/tests/unit/test_Edge_db_migration.js471
-rw-r--r--browser/components/migration/tests/unit/test_IE7_passwords.js397
-rw-r--r--browser/components/migration/tests/unit/test_IE_bookmarks.js44
-rw-r--r--browser/components/migration/tests/unit/test_IE_cookies.js111
-rw-r--r--browser/components/migration/tests/unit/test_Safari_bookmarks.js46
-rw-r--r--browser/components/migration/tests/unit/test_automigration.js695
-rw-r--r--browser/components/migration/tests/unit/test_fx_telemetry.js288
-rw-r--r--browser/components/migration/tests/unit/xpcshell.ini26
47 files changed, 10434 insertions, 0 deletions
diff --git a/browser/components/migration/.eslintrc.js b/browser/components/migration/.eslintrc.js
new file mode 100644
index 000000000..6693f83d0
--- /dev/null
+++ b/browser/components/migration/.eslintrc.js
@@ -0,0 +1,82 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../.eslintrc.js"
+ ],
+
+ "globals": {
+ "Components": true,
+ "dump": true,
+ "Iterator": true
+ },
+
+ "env": { "browser": true },
+
+ "rules": {
+ "block-scoped-var": "error",
+ // "brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
+ "comma-dangle": "off",
+ "comma-spacing": ["warn", {"before": false, "after": true}],
+ "comma-style": ["warn", "last"],
+ // "complexity": "warn",
+ "consistent-return": "error",
+ //"curly": "error",
+ "dot-notation": "error",
+ "eol-last": "error",
+ "indent": ["warn", 2, {"SwitchCase": 1}],
+ // "key-spacing": ["warn", {"beforeColon": false, "afterColon": true}],
+ "keyword-spacing": "warn",
+ "max-nested-callbacks": ["error", 3],
+ "new-parens": "error",
+ "no-array-constructor": "error",
+ "no-cond-assign": "error",
+ "no-control-regex": "error",
+ "no-debugger": "error",
+ "no-delete-var": "error",
+ "no-dupe-args": "error",
+ "no-dupe-keys": "error",
+ "no-duplicate-case": "error",
+ "no-else-return": "error",
+ "no-eval": "error",
+ "no-extend-native": "error",
+ // "no-extra-bind": "error",
+ "no-extra-boolean-cast": "error",
+ "no-extra-semi": "warn",
+ "no-fallthrough": ["error", { "commentPattern": ".*[Ii]ntentional(?:ly)?\\s+fall(?:ing)?[\\s-]*through.*" }],
+ "no-lonely-if": "error",
+ "no-mixed-spaces-and-tabs": "error",
+ "no-multi-spaces": "warn",
+ "no-multi-str": "warn",
+ "no-native-reassign": "error",
+ "no-nested-ternary": "error",
+ "no-redeclare": "error",
+ "no-return-assign": "error",
+ "no-self-compare": "error",
+ "no-sequences": "error",
+ "no-shadow": "warn",
+ "no-shadow-restricted-names": "error",
+ // "no-spaced-func": "warn",
+ "no-throw-literal": "error",
+ "no-trailing-spaces": "error",
+ "no-undef": "error",
+ "no-unneeded-ternary": "error",
+ "no-unreachable": "error",
+ "no-unused-vars": ["error", { "varsIgnorePattern": "^C[ciur]$" }],
+ "no-with": "error",
+ "padded-blocks": ["warn", "never"],
+ "quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
+ "semi": ["error", "always", {"omitLastInOneLineBlock": true }],
+ "semi-spacing": ["warn", {"before": false, "after": true}],
+ "space-before-blocks": ["warn", "always"],
+ // "space-before-function-paren": ["warn", "never"],
+ "space-in-parens": ["warn", "never"],
+ "space-infix-ops": ["warn", {"int32Hint": true}],
+ // "space-unary-ops": ["warn", { "words": true, "nonwords": false }],
+ "strict": ["error", "global"],
+ "use-isnan": "error",
+ "valid-typeof": "error",
+ "yoda": "error"
+ }
+};
+
diff --git a/browser/components/migration/360seProfileMigrator.js b/browser/components/migration/360seProfileMigrator.js
new file mode 100644
index 000000000..42347d542
--- /dev/null
+++ b/browser/components/migration/360seProfileMigrator.js
@@ -0,0 +1,328 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
+Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+
+const kBookmarksFileName = "360sefav.db";
+
+function copyToTempUTF8File(file, charset) {
+ let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ inputStream.init(file, -1, -1, 0);
+ let inputStr = NetUtil.readInputStreamToString(
+ inputStream, inputStream.available(), { charset });
+
+ // Use random to reduce the likelihood of a name collision in createUnique.
+ let rand = Math.floor(Math.random() * Math.pow(2, 15));
+ let leafName = "mozilla-temp-" + rand;
+ let tempUTF8File = FileUtils.getFile(
+ "TmpD", ["mozilla-temp-files", leafName], true);
+ tempUTF8File.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+
+ let out = FileUtils.openAtomicFileOutputStream(tempUTF8File);
+ try {
+ let bufferedOut = Cc["@mozilla.org/network/buffered-output-stream;1"]
+ .createInstance(Ci.nsIBufferedOutputStream);
+ bufferedOut.init(out, 4096);
+ try {
+ let converterOut = Cc["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Ci.nsIConverterOutputStream);
+ converterOut.init(bufferedOut, "utf-8", 0, 0x0000);
+ try {
+ converterOut.writeString(inputStr || "");
+ bufferedOut.QueryInterface(Ci.nsISafeOutputStream).finish();
+ } finally {
+ converterOut.close();
+ }
+ } finally {
+ bufferedOut.close();
+ }
+ } finally {
+ out.close();
+ }
+
+ return tempUTF8File;
+}
+
+function parseINIStrings(file) {
+ let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
+ getService(Ci.nsIINIParserFactory);
+ let parser = factory.createINIParser(file);
+ let obj = {};
+ let sections = parser.getSections();
+ while (sections.hasMore()) {
+ let section = sections.getNext();
+ obj[section] = {};
+
+ let keys = parser.getKeys(section);
+ while (keys.hasMore()) {
+ let key = keys.getNext();
+ obj[section][key] = parser.getString(section, key);
+ }
+ }
+ return obj;
+}
+
+function getHash(aStr) {
+ // return the two-digit hexadecimal code for a byte
+ let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2);
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(Ci.nsICryptoHash.MD5);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stringStream.data = aStr;
+ hasher.updateFromStream(stringStream, -1);
+
+ // convert the binary hash data to a hex string.
+ let binary = hasher.finish(false);
+ return Array.from(binary, (c, i) => toHexString(binary.charCodeAt(i))).join("").toLowerCase();
+}
+
+function Bookmarks(aProfileFolder) {
+ let file = aProfileFolder.clone();
+ file.append(kBookmarksFileName);
+
+ this._file = file;
+}
+Bookmarks.prototype = {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+ get exists() {
+ return this._file.exists() && this._file.isReadable();
+ },
+
+ migrate(aCallback) {
+ return Task.spawn(function* () {
+ let idToGuid = new Map();
+ let folderGuid = PlacesUtils.bookmarks.toolbarGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ folderGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("360se", folderGuid);
+ }
+ idToGuid.set(0, folderGuid);
+
+ let connection = yield Sqlite.openConnection({
+ path: this._file.path
+ });
+
+ try {
+ let rows = yield connection.execute(
+ `WITH RECURSIVE
+ bookmark(id, parent_id, is_folder, title, url, pos) AS (
+ VALUES(0, -1, 1, '', '', 0)
+ UNION
+ SELECT f.id, f.parent_id, f.is_folder, f.title, f.url, f.pos
+ FROM tb_fav AS f
+ JOIN bookmark AS b ON f.parent_id = b.id
+ ORDER BY f.pos ASC
+ )
+ SELECT id, parent_id, is_folder, title, url FROM bookmark WHERE id`);
+
+ for (let row of rows) {
+ let id = parseInt(row.getResultByName("id"), 10);
+ let parent_id = parseInt(row.getResultByName("parent_id"), 10);
+ let is_folder = parseInt(row.getResultByName("is_folder"), 10);
+ let title = row.getResultByName("title");
+ let url = row.getResultByName("url");
+
+ let parentGuid = idToGuid.get(parent_id) || idToGuid.get("fallback");
+ if (!parentGuid) {
+ parentGuid = PlacesUtils.bookmarks.unfiledGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ parentGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("360se", parentGuid);
+ }
+ idToGuid.set("fallback", parentGuid);
+ }
+
+ try {
+ if (is_folder == 1) {
+ let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title
+ })).guid;
+
+ idToGuid.set(id, newFolderGuid);
+ } else {
+ yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid,
+ url,
+ title
+ });
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ } finally {
+ yield connection.close();
+ }
+ }.bind(this)).then(() => aCallback(true),
+ e => { Cu.reportError(e); aCallback(false) });
+ }
+};
+
+function Qihoo360seProfileMigrator() {
+ let paths = [
+ // for v6 and above
+ {
+ users: ["360se6", "apps", "data", "users"],
+ defaultUser: "default"
+ },
+ // for earlier versions
+ {
+ users: ["360se"],
+ defaultUser: "data"
+ }
+ ];
+ this._usersDir = null;
+ this._defaultUserPath = null;
+ for (let path of paths) {
+ let usersDir = FileUtils.getDir("AppData", path.users, false);
+ if (usersDir.exists()) {
+ this._usersDir = usersDir;
+ this._defaultUserPath = path.defaultUser;
+ break;
+ }
+ }
+}
+
+Qihoo360seProfileMigrator.prototype = Object.create(MigratorPrototype);
+
+Object.defineProperty(Qihoo360seProfileMigrator.prototype, "sourceProfiles", {
+ get: function() {
+ if ("__sourceProfiles" in this)
+ return this.__sourceProfiles;
+
+ if (!this._usersDir) {
+ this.__sourceProfiles = [];
+ return this.__sourceProfiles;
+ }
+
+ let profiles = [];
+ let noLoggedInUser = true;
+ try {
+ let loginIni = this._usersDir.clone();
+ loginIni.append("login.ini");
+ if (!loginIni.exists()) {
+ throw new Error("360 Secure Browser's 'login.ini' does not exist.");
+ }
+ if (!loginIni.isReadable()) {
+ throw new Error("360 Secure Browser's 'login.ini' file could not be read.");
+ }
+
+ let loginIniInUtf8 = copyToTempUTF8File(loginIni, "gbk");
+ let loginIniObj = parseINIStrings(loginIniInUtf8);
+ try {
+ loginIniInUtf8.remove(false);
+ } catch (ex) {}
+
+ let nowLoginEmail = loginIniObj.NowLogin && loginIniObj.NowLogin.email;
+
+ /*
+ * NowLogin section may:
+ * 1. be missing or without email, before any user logs in.
+ * 2. represents the current logged in user
+ * 3. represents the most recent logged in user
+ *
+ * In the second case, user represented by NowLogin should be the first
+ * profile; otherwise the default user should be selected by default.
+ */
+ if (nowLoginEmail) {
+ if (loginIniObj.NowLogin.IsLogined === "1") {
+ noLoggedInUser = false;
+ }
+
+ profiles.push({
+ id: this._getIdFromConfig(loginIniObj.NowLogin),
+ name: nowLoginEmail,
+ });
+ }
+
+ for (let section in loginIniObj) {
+ if (!loginIniObj[section].email ||
+ (nowLoginEmail && loginIniObj[section].email == nowLoginEmail)) {
+ continue;
+ }
+
+ profiles.push({
+ id: this._getIdFromConfig(loginIniObj[section]),
+ name: loginIniObj[section].email,
+ });
+ }
+ } catch (e) {
+ Cu.reportError("Error detecting 360 Secure Browser profiles: " + e);
+ } finally {
+ profiles[noLoggedInUser ? "unshift" : "push"]({
+ id: this._defaultUserPath,
+ name: "Default",
+ });
+ }
+
+ this.__sourceProfiles = profiles.filter(profile => {
+ let resources = this.getResources(profile);
+ return resources && resources.length > 0;
+ });
+ return this.__sourceProfiles;
+ }
+});
+
+Qihoo360seProfileMigrator.prototype._getIdFromConfig = function(aConfig) {
+ return aConfig.UserMd5 || getHash(aConfig.email);
+};
+
+Qihoo360seProfileMigrator.prototype.getResources = function(aProfile) {
+ let profileFolder = this._usersDir.clone();
+ profileFolder.append(aProfile.id);
+
+ if (!profileFolder.exists()) {
+ return [];
+ }
+
+ let resources = [
+ new Bookmarks(profileFolder)
+ ];
+ return resources.filter(r => r.exists);
+};
+
+Qihoo360seProfileMigrator.prototype.getLastUsedDate = function() {
+ let bookmarksPaths = this.sourceProfiles.map(({id}) => {
+ return OS.Path.join(this._usersDir.path, id, kBookmarksFileName);
+ });
+ if (!bookmarksPaths.length) {
+ return Promise.resolve(new Date(0));
+ }
+ let datePromises = bookmarksPaths.map(path => {
+ return OS.File.stat(path).catch(() => null).then(info => {
+ return info ? info.lastModificationDate : 0;
+ });
+ });
+ return Promise.all(datePromises).then(dates => {
+ return new Date(Math.max.apply(Math, dates));
+ });
+};
+
+Qihoo360seProfileMigrator.prototype.classDescription = "360 Secure Browser Profile Migrator";
+Qihoo360seProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=360se";
+Qihoo360seProfileMigrator.prototype.classID = Components.ID("{d0037b95-296a-4a4e-94b2-c3d075d20ab1}");
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Qihoo360seProfileMigrator]);
diff --git a/browser/components/migration/AutoMigrate.jsm b/browser/components/migration/AutoMigrate.jsm
new file mode 100644
index 000000000..b38747825
--- /dev/null
+++ b/browser/components/migration/AutoMigrate.jsm
@@ -0,0 +1,670 @@
+/* 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 = ["AutoMigrate"];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+const kAutoMigrateEnabledPref = "browser.migrate.automigrate.enabled";
+const kUndoUIEnabledPref = "browser.migrate.automigrate.ui.enabled";
+
+const kAutoMigrateBrowserPref = "browser.migrate.automigrate.browser";
+const kAutoMigrateImportedItemIds = "browser.migrate.automigrate.imported-items";
+
+const kAutoMigrateLastUndoPromptDateMsPref = "browser.migrate.automigrate.lastUndoPromptDateMs";
+const kAutoMigrateDaysToOfferUndoPref = "browser.migrate.automigrate.daysToOfferUndo";
+
+const kAutoMigrateUndoSurveyPref = "browser.migrate.automigrate.undo-survey";
+const kAutoMigrateUndoSurveyLocalePref = "browser.migrate.automigrate.undo-survey-locales";
+
+const kNotificationId = "automigration-undo";
+
+Cu.import("resource:///modules/MigrationUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
+ "resource://gre/modules/NewTabUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
+ const kBrandBundle = "chrome://branding/locale/brand.properties";
+ return Services.strings.createBundle(kBrandBundle);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gHardcodedStringBundle", function() {
+ const kBundleURI = "chrome://browser/content/migration/extra-migration-strings.properties";
+ return Services.strings.createBundle(kBundleURI);
+});
+
+Cu.importGlobalProperties(["URL"]);
+
+/* globals kUndoStateFullPath */
+XPCOMUtils.defineLazyGetter(this, "kUndoStateFullPath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "initialMigrationMetadata.jsonlz4");
+});
+
+const AutoMigrate = {
+ get resourceTypesToUse() {
+ let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
+ return BOOKMARKS | HISTORY | PASSWORDS;
+ },
+
+ _checkIfEnabled() {
+ let pref = Preferences.get(kAutoMigrateEnabledPref, false);
+ // User-set values should take precedence:
+ if (Services.prefs.prefHasUserValue(kAutoMigrateEnabledPref)) {
+ return pref;
+ }
+ // If we're using the default value, make sure the distribution.ini
+ // value is taken into account even early on startup.
+ try {
+ let distributionFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile);
+ distributionFile.append("distribution.ini");
+ let parser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
+ getService(Ci.nsIINIParserFactory).
+ createINIParser(distributionFile);
+ return JSON.parse(parser.getString("Preferences", kAutoMigrateEnabledPref));
+ } catch (ex) { /* ignore exceptions (file doesn't exist, invalid value, etc.) */ }
+
+ return pref;
+ },
+
+ init() {
+ this.enabled = this._checkIfEnabled();
+ },
+
+ /**
+ * Automatically pick a migrator and resources to migrate,
+ * then migrate those and start up.
+ *
+ * @throws if automatically deciding on migrators/data
+ * failed for some reason.
+ */
+ migrate(profileStartup, migratorKey, profileToMigrate) {
+ let histogram = Services.telemetry.getHistogramById(
+ "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_PROCESS_SUCCESS");
+ histogram.add(0);
+ let {migrator, pickedKey} = this.pickMigrator(migratorKey);
+ histogram.add(5);
+
+ profileToMigrate = this.pickProfile(migrator, profileToMigrate);
+ histogram.add(10);
+
+ let resourceTypes = migrator.getMigrateData(profileToMigrate, profileStartup);
+ if (!(resourceTypes & this.resourceTypesToUse)) {
+ throw new Error("No usable resources were found for the selected browser!");
+ }
+ histogram.add(15);
+
+ let sawErrors = false;
+ let migrationObserver = (subject, topic) => {
+ if (topic == "Migration:ItemError") {
+ sawErrors = true;
+ } else if (topic == "Migration:Ended") {
+ histogram.add(25);
+ if (sawErrors) {
+ histogram.add(26);
+ }
+ Services.obs.removeObserver(migrationObserver, "Migration:Ended");
+ Services.obs.removeObserver(migrationObserver, "Migration:ItemError");
+ Services.prefs.setCharPref(kAutoMigrateBrowserPref, pickedKey);
+ // Save the undo history and block shutdown on that save completing.
+ AsyncShutdown.profileBeforeChange.addBlocker(
+ "AutoMigrate Undo saving", this.saveUndoState(), () => {
+ return {state: this._saveUndoStateTrackerForShutdown};
+ });
+ }
+ };
+
+ MigrationUtils.initializeUndoData();
+ Services.obs.addObserver(migrationObserver, "Migration:Ended", false);
+ Services.obs.addObserver(migrationObserver, "Migration:ItemError", false);
+ migrator.migrate(this.resourceTypesToUse, profileStartup, profileToMigrate);
+ histogram.add(20);
+ },
+
+ /**
+ * Pick and return a migrator to use for automatically migrating.
+ *
+ * @param {String} migratorKey optional, a migrator key to prefer/pick.
+ * @returns {Object} an object with the migrator to use for migrating, as
+ * well as the key we eventually ended up using to obtain it.
+ */
+ pickMigrator(migratorKey) {
+ if (!migratorKey) {
+ let defaultKey = MigrationUtils.getMigratorKeyForDefaultBrowser();
+ if (!defaultKey) {
+ throw new Error("Could not determine default browser key to migrate from");
+ }
+ migratorKey = defaultKey;
+ }
+ if (migratorKey == "firefox") {
+ throw new Error("Can't automatically migrate from Firefox.");
+ }
+
+ let migrator = MigrationUtils.getMigrator(migratorKey);
+ if (!migrator) {
+ throw new Error("Migrator specified or a default was found, but the migrator object is not available (or has no data).");
+ }
+ return {migrator, pickedKey: migratorKey};
+ },
+
+ /**
+ * Pick a source profile (from the original browser) to use.
+ *
+ * @param {Migrator} migrator the migrator object to use
+ * @param {String} suggestedId the id of the profile to migrate, if pre-specified, or null
+ * @returns the profile to migrate, or null if migrating
+ * from the default profile.
+ */
+ pickProfile(migrator, suggestedId) {
+ let profiles = migrator.sourceProfiles;
+ if (profiles && !profiles.length) {
+ throw new Error("No profile data found to migrate.");
+ }
+ if (suggestedId) {
+ if (!profiles) {
+ throw new Error("Profile specified but only a default profile found.");
+ }
+ let suggestedProfile = profiles.find(profile => profile.id == suggestedId);
+ if (!suggestedProfile) {
+ throw new Error("Profile specified was not found.");
+ }
+ return suggestedProfile;
+ }
+ if (profiles && profiles.length > 1) {
+ throw new Error("Don't know how to pick a profile when more than 1 profile is present.");
+ }
+ return profiles ? profiles[0] : null;
+ },
+
+ _pendingUndoTasks: false,
+ canUndo: Task.async(function* () {
+ if (this._savingPromise) {
+ yield this._savingPromise;
+ }
+ if (this._pendingUndoTasks) {
+ return false;
+ }
+ let fileExists = false;
+ try {
+ fileExists = yield OS.File.exists(kUndoStateFullPath);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ return fileExists;
+ }),
+
+ undo: Task.async(function* () {
+ let browserId = Preferences.get(kAutoMigrateBrowserPref, "unknown");
+ TelemetryStopwatch.startKeyed("FX_STARTUP_MIGRATION_UNDO_TOTAL_MS", browserId);
+ let histogram = Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_UNDO");
+ histogram.add(0);
+ if (!(yield this.canUndo())) {
+ histogram.add(5);
+ throw new Error("Can't undo!");
+ }
+
+ this._pendingUndoTasks = true;
+ this._removeNotificationBars();
+ histogram.add(10);
+
+ let readPromise = OS.File.read(kUndoStateFullPath, {
+ encoding: "utf-8",
+ compression: "lz4",
+ });
+ let stateData = this._dejsonifyUndoState(yield readPromise);
+ histogram.add(12);
+
+ this._errorMap = {bookmarks: 0, visits: 0, logins: 0};
+ let reportErrorTelemetry = (type) => {
+ let histogramId = `FX_STARTUP_MIGRATION_UNDO_${type.toUpperCase()}_ERRORCOUNT`;
+ Services.telemetry.getKeyedHistogramById(histogramId).add(browserId, this._errorMap[type]);
+ };
+
+ let startTelemetryStopwatch = resourceType => {
+ let histogramId = `FX_STARTUP_MIGRATION_UNDO_${resourceType.toUpperCase()}_MS`;
+ TelemetryStopwatch.startKeyed(histogramId, browserId);
+ };
+ let stopTelemetryStopwatch = resourceType => {
+ let histogramId = `FX_STARTUP_MIGRATION_UNDO_${resourceType.toUpperCase()}_MS`;
+ TelemetryStopwatch.finishKeyed(histogramId, browserId);
+ };
+ startTelemetryStopwatch("bookmarks");
+ yield this._removeUnchangedBookmarks(stateData.get("bookmarks")).catch(ex => {
+ Cu.reportError("Uncaught exception when removing unchanged bookmarks!");
+ Cu.reportError(ex);
+ });
+ stopTelemetryStopwatch("bookmarks");
+ reportErrorTelemetry("bookmarks");
+ histogram.add(15);
+
+ startTelemetryStopwatch("visits");
+ yield this._removeSomeVisits(stateData.get("visits")).catch(ex => {
+ Cu.reportError("Uncaught exception when removing history visits!");
+ Cu.reportError(ex);
+ });
+ stopTelemetryStopwatch("visits");
+ reportErrorTelemetry("visits");
+ histogram.add(20);
+
+ startTelemetryStopwatch("logins");
+ yield this._removeUnchangedLogins(stateData.get("logins")).catch(ex => {
+ Cu.reportError("Uncaught exception when removing unchanged logins!");
+ Cu.reportError(ex);
+ });
+ stopTelemetryStopwatch("logins");
+ reportErrorTelemetry("logins");
+ histogram.add(25);
+
+ // This is async, but no need to wait for it.
+ NewTabUtils.links.populateCache(() => {
+ NewTabUtils.allPages.update();
+ }, true);
+
+ this._purgeUndoState(this.UNDO_REMOVED_REASON_UNDO_USED);
+ histogram.add(30);
+ TelemetryStopwatch.finishKeyed("FX_STARTUP_MIGRATION_UNDO_TOTAL_MS", browserId);
+ }),
+
+ _removeNotificationBars() {
+ let browserWindows = Services.wm.getEnumerator("navigator:browser");
+ while (browserWindows.hasMoreElements()) {
+ let win = browserWindows.getNext();
+ if (!win.closed) {
+ for (let browser of win.gBrowser.browsers) {
+ let nb = win.gBrowser.getNotificationBox(browser);
+ let notification = nb.getNotificationWithValue(kNotificationId);
+ if (notification) {
+ nb.removeNotification(notification);
+ }
+ }
+ }
+ }
+ },
+
+ _purgeUndoState(reason) {
+ // We don't wait for the off-main-thread removal to complete. OS.File will
+ // ensure it happens before shutdown.
+ OS.File.remove(kUndoStateFullPath, {ignoreAbsent: true}).then(() => {
+ this._pendingUndoTasks = false;
+ });
+
+ let migrationBrowser = Preferences.get(kAutoMigrateBrowserPref, "unknown");
+ Services.prefs.clearUserPref(kAutoMigrateBrowserPref);
+
+ let histogram =
+ Services.telemetry.getKeyedHistogramById("FX_STARTUP_MIGRATION_UNDO_REASON");
+ histogram.add(migrationBrowser, reason);
+ },
+
+ getBrowserUsedForMigration() {
+ let browserId = Services.prefs.getCharPref(kAutoMigrateBrowserPref);
+ if (browserId) {
+ return MigrationUtils.getBrowserName(browserId);
+ }
+ return null;
+ },
+
+ /**
+ * Show the user a notification bar indicating we automatically imported
+ * their data and offering them the possibility of removing it.
+ * @param target (xul:browser)
+ * The browser in which we should show the notification.
+ */
+ maybeShowUndoNotification: Task.async(function* (target) {
+ if (!(yield this.canUndo())) {
+ return;
+ }
+
+ // The tab might have navigated since we requested the undo state:
+ let canUndoFromThisPage = ["about:home", "about:newtab"].includes(target.currentURI.spec);
+ if (!canUndoFromThisPage ||
+ !Preferences.get(kUndoUIEnabledPref, false)) {
+ return;
+ }
+
+ let win = target.ownerGlobal;
+ let notificationBox = win.gBrowser.getNotificationBox(target);
+ if (!notificationBox || notificationBox.getNotificationWithValue(kNotificationId)) {
+ return;
+ }
+
+ // At this stage we're committed to show the prompt - unless we shouldn't,
+ // in which case we remove the undo prefs (which will cause canUndo() to
+ // return false from now on.):
+ if (!this.shouldStillShowUndoPrompt()) {
+ this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_EXPIRED);
+ this._removeNotificationBars();
+ return;
+ }
+
+ let browserName = this.getBrowserUsedForMigration();
+ if (!browserName) {
+ browserName = gHardcodedStringBundle.GetStringFromName("automigration.undo.unknownbrowser");
+ }
+ const kMessageId = "automigration.undo.message." +
+ Preferences.get(kAutoMigrateImportedItemIds, "all");
+ const kBrandShortName = gBrandBundle.GetStringFromName("brandShortName");
+ let message = gHardcodedStringBundle.formatStringFromName(kMessageId,
+ [browserName, kBrandShortName], 2);
+
+ let buttons = [
+ {
+ label: gHardcodedStringBundle.GetStringFromName("automigration.undo.keep2.label"),
+ accessKey: gHardcodedStringBundle.GetStringFromName("automigration.undo.keep2.accesskey"),
+ callback: () => {
+ this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_REJECTED);
+ this._removeNotificationBars();
+ },
+ },
+ {
+ label: gHardcodedStringBundle.GetStringFromName("automigration.undo.dontkeep2.label"),
+ accessKey: gHardcodedStringBundle.GetStringFromName("automigration.undo.dontkeep2.accesskey"),
+ callback: () => {
+ this._maybeOpenUndoSurveyTab(win);
+ this.undo();
+ },
+ },
+ ];
+ notificationBox.appendNotification(
+ message, kNotificationId, null, notificationBox.PRIORITY_INFO_HIGH, buttons
+ );
+ let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 0);
+ Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_UNDO_OFFERED").add(4 - remainingDays);
+ }),
+
+ shouldStillShowUndoPrompt() {
+ let today = new Date();
+ // Round down to midnight:
+ today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
+ // We store the unix timestamp corresponding to midnight on the last day
+ // on which we prompted. Fetch that and compare it to today's date.
+ // (NB: stored as a string because int prefs are too small for unix
+ // timestamps.)
+ let previousPromptDateMsStr = Preferences.get(kAutoMigrateLastUndoPromptDateMsPref, "0");
+ let previousPromptDate = new Date(parseInt(previousPromptDateMsStr, 10));
+ if (previousPromptDate < today) {
+ let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 4) - 1;
+ Preferences.set(kAutoMigrateDaysToOfferUndoPref, remainingDays);
+ Preferences.set(kAutoMigrateLastUndoPromptDateMsPref, today.valueOf().toString());
+ if (remainingDays <= 0) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ UNDO_REMOVED_REASON_UNDO_USED: 0,
+ UNDO_REMOVED_REASON_SYNC_SIGNIN: 1,
+ UNDO_REMOVED_REASON_PASSWORD_CHANGE: 2,
+ UNDO_REMOVED_REASON_BOOKMARK_CHANGE: 3,
+ UNDO_REMOVED_REASON_OFFER_EXPIRED: 4,
+ UNDO_REMOVED_REASON_OFFER_REJECTED: 5,
+
+ _jsonifyUndoState(state) {
+ if (!state) {
+ return "null";
+ }
+ // Deal with date serialization.
+ let bookmarks = state.get("bookmarks");
+ for (let bm of bookmarks) {
+ bm.lastModified = bm.lastModified.getTime();
+ }
+ let serializableState = {
+ bookmarks,
+ logins: state.get("logins"),
+ visits: state.get("visits"),
+ };
+ return JSON.stringify(serializableState);
+ },
+
+ _dejsonifyUndoState(state) {
+ state = JSON.parse(state);
+ if (!state) {
+ return new Map();
+ }
+ for (let bm of state.bookmarks) {
+ bm.lastModified = new Date(bm.lastModified);
+ }
+ return new Map([
+ ["bookmarks", state.bookmarks],
+ ["logins", state.logins],
+ ["visits", state.visits],
+ ]);
+ },
+
+ /**
+ * Store the items we've saved into a pref. We use this to be able to show
+ * a detailed message to the user indicating what we've imported.
+ * @param state (Map)
+ * The 'undo' state for the import, which contains info about
+ * how many items of each kind we've (tried to) import.
+ */
+ _setImportedItemPrefFromState(state) {
+ let itemsWithData = [];
+ if (state) {
+ for (let itemType of state.keys()) {
+ if (state.get(itemType).length) {
+ itemsWithData.push(itemType);
+ }
+ }
+ }
+ if (itemsWithData.length == 3) {
+ itemsWithData = "all";
+ } else {
+ itemsWithData = itemsWithData.sort().join(".");
+ }
+ if (itemsWithData) {
+ Preferences.set(kAutoMigrateImportedItemIds, itemsWithData);
+ }
+ },
+
+ /**
+ * Used for the shutdown blocker's information field.
+ */
+ _saveUndoStateTrackerForShutdown: "not running",
+ /**
+ * Store the information required for using 'undo' of the automatic
+ * migration in the user's profile.
+ */
+ saveUndoState: Task.async(function* () {
+ let resolveSavingPromise;
+ this._saveUndoStateTrackerForShutdown = "processing undo history";
+ this._savingPromise = new Promise(resolve => { resolveSavingPromise = resolve });
+ let state = yield MigrationUtils.stopAndRetrieveUndoData();
+
+ if (!state || ![...state.values()].some(ary => ary.length > 0)) {
+ // If we didn't import anything, abort now.
+ resolveSavingPromise();
+ return Promise.resolve();
+ }
+
+ this._saveUndoStateTrackerForShutdown = "saving imported item list";
+ this._setImportedItemPrefFromState(state);
+
+ this._saveUndoStateTrackerForShutdown = "writing undo history";
+ this._undoSavePromise = OS.File.writeAtomic(
+ kUndoStateFullPath, this._jsonifyUndoState(state), {
+ encoding: "utf-8",
+ compression: "lz4",
+ tmpPath: kUndoStateFullPath + ".tmp",
+ });
+ this._undoSavePromise.then(
+ rv => {
+ resolveSavingPromise(rv);
+ delete this._savingPromise;
+ },
+ e => {
+ Cu.reportError("Could not write undo state for automatic migration.");
+ throw e;
+ });
+ return this._undoSavePromise;
+ }),
+
+ _removeUnchangedBookmarks: Task.async(function* (bookmarks) {
+ if (!bookmarks.length) {
+ return;
+ }
+
+ let guidToLMMap = new Map(bookmarks.map(b => [b.guid, b.lastModified]));
+ let bookmarksFromDB = [];
+ let bmPromises = Array.from(guidToLMMap.keys()).map(guid => {
+ // Ignore bookmarks where the promise doesn't resolve (ie that are missing)
+ // Also check that the bookmark fetch returns isn't null before adding it.
+ try {
+ return PlacesUtils.bookmarks.fetch(guid).then(bm => bm && bookmarksFromDB.push(bm), () => {});
+ } catch (ex) {
+ // Ignore immediate exceptions, too.
+ }
+ return Promise.resolve();
+ });
+ // We can't use the result of Promise.all because that would include nulls
+ // for bookmarks that no longer exist (which we're catching above).
+ yield Promise.all(bmPromises);
+ let unchangedBookmarks = bookmarksFromDB.filter(bm => {
+ return bm.lastModified.getTime() == guidToLMMap.get(bm.guid).getTime();
+ });
+
+ // We need to remove items without children first, followed by their
+ // parents, etc. In order to do this, find out how many ancestors each item
+ // has that also appear in our list of things to remove, and sort the items
+ // by those numbers. This ensures that children are always removed before
+ // their parents.
+ function determineAncestorCount(bm) {
+ if (bm._ancestorCount) {
+ return bm._ancestorCount;
+ }
+ let myCount = 0;
+ let parentBM = unchangedBookmarks.find(item => item.guid == bm.parentGuid);
+ if (parentBM) {
+ myCount = determineAncestorCount(parentBM) + 1;
+ }
+ bm._ancestorCount = myCount;
+ return myCount;
+ }
+ unchangedBookmarks.forEach(determineAncestorCount);
+ unchangedBookmarks.sort((a, b) => b._ancestorCount - a._ancestorCount);
+ for (let {guid} of unchangedBookmarks) {
+ // Can't just use a .catch() because Bookmarks.remove() can throw (rather
+ // than returning rejected promises).
+ try {
+ yield PlacesUtils.bookmarks.remove(guid, {preventRemovalOfNonEmptyFolders: true});
+ } catch (err) {
+ if (err && err.message != "Cannot remove a non-empty folder.") {
+ this._errorMap.bookmarks++;
+ Cu.reportError(err);
+ }
+ }
+ }
+ }),
+
+ _removeUnchangedLogins: Task.async(function* (logins) {
+ for (let login of logins) {
+ let foundLogins = LoginHelper.searchLoginsWithObject({guid: login.guid});
+ if (foundLogins.length) {
+ let foundLogin = foundLogins[0];
+ foundLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ if (foundLogin.timePasswordChanged == login.timePasswordChanged) {
+ try {
+ Services.logins.removeLogin(foundLogin);
+ } catch (ex) {
+ Cu.reportError("Failed to remove a login for " + foundLogins.hostname);
+ Cu.reportError(ex);
+ this._errorMap.logins++;
+ }
+ }
+ }
+ }
+ }),
+
+ _removeSomeVisits: Task.async(function* (visits) {
+ for (let urlVisits of visits) {
+ let urlObj;
+ try {
+ urlObj = new URL(urlVisits.url);
+ } catch (ex) {
+ continue;
+ }
+ let visitData = {
+ url: urlObj,
+ beginDate: PlacesUtils.toDate(urlVisits.first),
+ endDate: PlacesUtils.toDate(urlVisits.last),
+ limit: urlVisits.visitCount,
+ };
+ try {
+ yield PlacesUtils.history.removeVisitsByFilter(visitData);
+ } catch (ex) {
+ this._errorMap.visits++;
+ try {
+ visitData.url = visitData.url.href;
+ } catch (ignoredEx) {}
+ Cu.reportError("Failed to remove a visit: " + JSON.stringify(visitData));
+ Cu.reportError(ex);
+ }
+ }
+ }),
+
+ /**
+ * Maybe open a new tab with a survey. The tab will only be opened if all of
+ * the following are true:
+ * - the 'browser.migrate.automigrate.undo-survey' pref is not empty.
+ * It should contain the URL of the survey to open.
+ * - the 'browser.migrate.automigrate.undo-survey-locales' pref, a
+ * comma-separated list of language codes, contains the language code
+ * that is currently in use for the 'global' chrome pacakge (ie the
+ * locale in which the user is currently using Firefox).
+ * The URL will be passed through nsIURLFormatter to allow including
+ * build ids etc. The special additional formatting variable
+ * "%IMPORTEDBROWSER" is also replaced with the name of the browser
+ * from which we imported data.
+ *
+ * @param {Window} chromeWindow A reference to the window in which to open a link.
+ */
+ _maybeOpenUndoSurveyTab(chromeWindow) {
+ let canDoSurveyInLocale = false;
+ try {
+ let surveyLocales = Preferences.get(kAutoMigrateUndoSurveyLocalePref, "");
+ surveyLocales = surveyLocales.split(",").map(str => str.trim());
+ // Strip out any empty elements, so an empty pref doesn't
+ // lead to a an array with 1 empty string in it.
+ surveyLocales = new Set(surveyLocales.filter(str => !!str));
+ let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry);
+ canDoSurveyInLocale =
+ surveyLocales.has(chromeRegistry.getSelectedLocale("global"));
+ } catch (ex) {
+ /* ignore exceptions and just don't do the survey. */
+ }
+
+ let migrationBrowser = this.getBrowserUsedForMigration();
+ let rawURL = Preferences.get(kAutoMigrateUndoSurveyPref, "");
+ if (!canDoSurveyInLocale || !migrationBrowser || !rawURL) {
+ return;
+ }
+
+ let url = Services.urlFormatter.formatURL(rawURL);
+ url = url.replace("%IMPORTEDBROWSER%", encodeURIComponent(migrationBrowser));
+ chromeWindow.openUILinkIn(url, "tab");
+ },
+
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIObserver, Ci.nsINavBookmarkObserver, Ci.nsISupportsWeakReference]
+ ),
+};
+
+AutoMigrate.init();
diff --git a/browser/components/migration/BrowserProfileMigrators.manifest b/browser/components/migration/BrowserProfileMigrators.manifest
new file mode 100644
index 000000000..e16fba13a
--- /dev/null
+++ b/browser/components/migration/BrowserProfileMigrators.manifest
@@ -0,0 +1,33 @@
+component {6F8BB968-C14F-4D6F-9733-6C6737B35DCE} ProfileMigrator.js
+contract @mozilla.org/toolkit/profile-migrator;1 {6F8BB968-C14F-4D6F-9733-6C6737B35DCE}
+
+#if defined(XP_WIN) || defined(XP_MACOSX)
+component {4bf85aa5-4e21-46ca-825f-f9c51a5e8c76} ChromeProfileMigrator.js
+contract @mozilla.org/profile/migrator;1?app=browser&type=canary {4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}
+#endif
+component {4cec1de4-1671-4fc3-a53e-6c539dc77a26} ChromeProfileMigrator.js
+contract @mozilla.org/profile/migrator;1?app=browser&type=chrome {4cec1de4-1671-4fc3-a53e-6c539dc77a26}
+component {8cece922-9720-42de-b7db-7cef88cb07ca} ChromeProfileMigrator.js
+contract @mozilla.org/profile/migrator;1?app=browser&type=chromium {8cece922-9720-42de-b7db-7cef88cb07ca}
+
+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_EDGE_MIGRATOR
+component {62e8834b-2d17-49f5-96ff-56344903a2ae} EdgeProfileMigrator.js
+contract @mozilla.org/profile/migrator;1?app=browser&type=edge {62e8834b-2d17-49f5-96ff-56344903a2ae}
+#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
+#ifdef HAS_360SE_MIGRATOR
+component {d0037b95-296a-4a4e-94b2-c3d075d20ab1} 360seProfileMigrator.js
+contract @mozilla.org/profile/migrator;1?app=browser&type=360se {d0037b95-296a-4a4e-94b2-c3d075d20ab1}
+#endif
diff --git a/browser/components/migration/ChromeProfileMigrator.js b/browser/components/migration/ChromeProfileMigrator.js
new file mode 100644
index 000000000..ec0c8d444
--- /dev/null
+++ b/browser/components/migration/ChromeProfileMigrator.js
@@ -0,0 +1,557 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1";
+
+const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000;
+const S100NS_PER_MS = 10;
+
+const AUTH_TYPE = {
+ SCHEME_HTML: 0,
+ SCHEME_BASIC: 1,
+ SCHEME_DIGEST: 2
+};
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
+ "resource://gre/modules/OSCrypto.jsm");
+/**
+ * Get an nsIFile instance representing the expected location of user data
+ * for this copy of Chrome/Chromium/Canary on different OSes.
+ * @param subfoldersWin {Array} an array of subfolders to use for Windows
+ * @param subfoldersOSX {Array} an array of subfolders to use for OS X
+ * @param subfoldersUnix {Array} an array of subfolders to use for *nix systems
+ * @returns {nsIFile} the place we expect data to live. Might not actually exist!
+ */
+function getDataFolder(subfoldersWin, subfoldersOSX, subfoldersUnix) {
+ let dirServiceID, subfolders;
+ if (AppConstants.platform == "win") {
+ dirServiceID = "LocalAppData";
+ subfolders = subfoldersWin.concat(["User Data"]);
+ } else if (AppConstants.platform == "macosx") {
+ dirServiceID = "ULibDir";
+ subfolders = ["Application Support"].concat(subfoldersOSX);
+ } else {
+ dirServiceID = "Home";
+ subfolders = [".config"].concat(subfoldersUnix);
+ }
+ return FileUtils.getDir(dirServiceID, subfolders, false);
+}
+
+/**
+ * Convert Chrome time format to Date object
+ *
+ * @param aTime
+ * Chrome time
+ * @return converted Date object
+ * @note Google Chrome uses FILETIME / 10 as time.
+ * FILETIME is based on same structure of Windows.
+ */
+function chromeTimeToDate(aTime)
+{
+ return new Date((aTime * S100NS_PER_MS - S100NS_FROM1601TO1970) / 10000);
+}
+
+/**
+ * Convert Date object to Chrome time format
+ *
+ * @param aDate
+ * Date object or integer equivalent
+ * @return Chrome time
+ * @note For details on Chrome time, see chromeTimeToDate.
+ */
+function dateToChromeTime(aDate) {
+ return (aDate * 10000 + S100NS_FROM1601TO1970) / S100NS_PER_MS;
+}
+
+/**
+ * Insert bookmark items into specific folder.
+ *
+ * @param parentGuid
+ * GUID of the folder where items will be inserted
+ * @param items
+ * bookmark items to be inserted
+ * @param errorAccumulator
+ * function that gets called with any errors thrown so we don't drop them on the floor.
+ */
+function* insertBookmarkItems(parentGuid, items, errorAccumulator) {
+ for (let item of items) {
+ try {
+ if (item.type == "url") {
+ if (item.url.trim().startsWith("chrome:")) {
+ // Skip invalid chrome URIs. Creating an actual URI always reports
+ // messages to the console, so we avoid doing that.
+ continue;
+ }
+ yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid, url: item.url, title: item.name
+ });
+ } else if (item.type == "folder") {
+ let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title: item.name
+ })).guid;
+
+ yield insertBookmarkItems(newFolderGuid, item.children, errorAccumulator);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ errorAccumulator(e);
+ }
+ }
+}
+
+function ChromeProfileMigrator() {
+ let chromeUserDataFolder =
+ getDataFolder(["Google", "Chrome"], ["Google", "Chrome"], ["google-chrome"]);
+ this._chromeUserDataFolder = chromeUserDataFolder.exists() ?
+ chromeUserDataFolder : null;
+}
+
+ChromeProfileMigrator.prototype = Object.create(MigratorPrototype);
+
+ChromeProfileMigrator.prototype.getResources =
+ function Chrome_getResources(aProfile) {
+ if (this._chromeUserDataFolder) {
+ let profileFolder = this._chromeUserDataFolder.clone();
+ profileFolder.append(aProfile.id);
+ if (profileFolder.exists()) {
+ let possibleResources = [
+ GetBookmarksResource(profileFolder),
+ GetHistoryResource(profileFolder),
+ GetCookiesResource(profileFolder),
+ ];
+ if (AppConstants.platform == "win") {
+ possibleResources.push(GetWindowsPasswordsResource(profileFolder));
+ }
+ return possibleResources.filter(r => r != null);
+ }
+ }
+ return [];
+ };
+
+ChromeProfileMigrator.prototype.getLastUsedDate =
+ function Chrome_getLastUsedDate() {
+ let datePromises = this.sourceProfiles.map(profile => {
+ let basePath = OS.Path.join(this._chromeUserDataFolder.path, profile.id);
+ let fileDatePromises = ["Bookmarks", "History", "Cookies"].map(leafName => {
+ let path = OS.Path.join(basePath, leafName);
+ return OS.File.stat(path).catch(() => null).then(info => {
+ return info ? info.lastModificationDate : 0;
+ });
+ });
+ return Promise.all(fileDatePromises).then(dates => {
+ return Math.max.apply(Math, dates);
+ });
+ });
+ return Promise.all(datePromises).then(dates => {
+ dates.push(0);
+ return new Date(Math.max.apply(Math, dates));
+ });
+ };
+
+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
+ this.__sourceProfiles = profiles.filter(function(profile) {
+ let resources = this.getResources(profile);
+ return resources && resources.length > 0;
+ }, this);
+ return this.__sourceProfiles;
+ }
+});
+
+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(prefsFile, -1, 0, 0);
+ try {
+ return JSON.parse(
+ NetUtil.readInputStreamToString(fstream, fstream.available(),
+ { charset: "UTF-8" })
+ ).homepage;
+ }
+ catch (e) {
+ Cu.reportError("Error parsing Chrome's preferences file: " + e);
+ }
+ }
+ return "";
+ }
+});
+
+Object.defineProperty(ChromeProfileMigrator.prototype, "sourceLocked", {
+ get: function Chrome_sourceLocked() {
+ // There is an exclusive lock on some SQLite databases. Assume they are locked for now.
+ return true;
+ },
+});
+
+function GetBookmarksResource(aProfileFolder) {
+ let bookmarksFile = aProfileFolder.clone();
+ bookmarksFile.append("Bookmarks");
+ if (!bookmarksFile.exists())
+ return null;
+
+ return {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+ migrate: function(aCallback) {
+ return Task.spawn(function* () {
+ let gotErrors = false;
+ let errorGatherer = function() { gotErrors = true };
+ let jsonStream = yield new Promise((resolve, reject) => {
+ let options = {
+ uri: NetUtil.newURI(bookmarksFile),
+ loadUsingSystemPrincipal: true
+ };
+ NetUtil.asyncFetch(options, (inputStream, resultCode) => {
+ if (Components.isSuccessCode(resultCode)) {
+ resolve(inputStream);
+ } else {
+ reject(new Error("Could not read Bookmarks file"));
+ }
+ });
+ });
+
+ // Parse Chrome bookmark file that is JSON format
+ let bookmarkJSON = NetUtil.readInputStreamToString(
+ jsonStream, jsonStream.available(), { charset : "UTF-8" });
+ let roots = JSON.parse(bookmarkJSON).roots;
+
+ // Importing bookmark bar items
+ if (roots.bookmark_bar.children &&
+ roots.bookmark_bar.children.length > 0) {
+ // Toolbar
+ let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ parentGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
+ }
+ yield insertBookmarkItems(parentGuid, roots.bookmark_bar.children, errorGatherer);
+ }
+
+ // Importing bookmark menu items
+ if (roots.other.children &&
+ roots.other.children.length > 0) {
+ // Bookmark menu
+ let parentGuid = PlacesUtils.bookmarks.menuGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ parentGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid);
+ }
+ yield insertBookmarkItems(parentGuid, roots.other.children, errorGatherer);
+ }
+ if (gotErrors) {
+ throw new Error("The migration included errors.");
+ }
+ }.bind(this)).then(() => aCallback(true),
+ () => aCallback(false));
+ }
+ };
+}
+
+function GetHistoryResource(aProfileFolder) {
+ let historyFile = aProfileFolder.clone();
+ historyFile.append("History");
+ if (!historyFile.exists())
+ return null;
+
+ return {
+ type: MigrationUtils.resourceTypes.HISTORY,
+
+ migrate(aCallback) {
+ Task.spawn(function* () {
+ const MAX_AGE_IN_DAYS = Services.prefs.getIntPref("browser.migrate.chrome.history.maxAgeInDays");
+ const LIMIT = Services.prefs.getIntPref("browser.migrate.chrome.history.limit");
+
+ let query = "SELECT url, title, last_visit_time, typed_count FROM urls WHERE hidden = 0";
+ if (MAX_AGE_IN_DAYS) {
+ let maxAge = dateToChromeTime(Date.now() - MAX_AGE_IN_DAYS * 24 * 60 * 60 * 1000);
+ query += " AND last_visit_time > " + maxAge;
+ }
+ if (LIMIT) {
+ query += " ORDER BY last_visit_time DESC LIMIT " + LIMIT;
+ }
+
+ let rows =
+ yield MigrationUtils.getRowsFromDBWithoutLocks(historyFile.path, "Chrome history", query);
+ let places = [];
+ for (let row of rows) {
+ try {
+ // if having typed_count, we changes transition type to typed.
+ let transType = PlacesUtils.history.TRANSITION_LINK;
+ if (row.getResultByName("typed_count") > 0)
+ transType = PlacesUtils.history.TRANSITION_TYPED;
+
+ places.push({
+ uri: NetUtil.newURI(row.getResultByName("url")),
+ title: row.getResultByName("title"),
+ visits: [{
+ transitionType: transType,
+ visitDate: chromeTimeToDate(
+ row.getResultByName(
+ "last_visit_time")) * 1000,
+ }],
+ });
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+
+ if (places.length > 0) {
+ yield new Promise((resolve, reject) => {
+ MigrationUtils.insertVisitsWrapper(places, {
+ _success: false,
+ handleResult: function() {
+ // Importing any entry is considered a successful import.
+ this._success = true;
+ },
+ handleError: function() {},
+ handleCompletion: function() {
+ if (this._success) {
+ resolve();
+ } else {
+ reject(new Error("Couldn't add visits"));
+ }
+ }
+ });
+ });
+ }
+ }).then(() => { aCallback(true) },
+ ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ });
+ }
+ };
+}
+
+function GetCookiesResource(aProfileFolder) {
+ let cookiesFile = aProfileFolder.clone();
+ cookiesFile.append("Cookies");
+ if (!cookiesFile.exists())
+ return null;
+
+ return {
+ type: MigrationUtils.resourceTypes.COOKIES,
+
+ migrate: Task.async(function* (aCallback) {
+ // We don't support decrypting cookies yet so only import plaintext ones.
+ let rows = yield MigrationUtils.getRowsFromDBWithoutLocks(cookiesFile.path, "Chrome cookies",
+ `SELECT host_key, name, value, path, expires_utc, secure, httponly, encrypted_value
+ FROM cookies
+ WHERE length(encrypted_value) = 0`).catch(ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ });
+ // If the promise was rejected we will have already called aCallback,
+ // so we can just return here.
+ if (!rows) {
+ return;
+ }
+
+ for (let row of rows) {
+ let host_key = row.getResultByName("host_key");
+ if (host_key.match(/^\./)) {
+ // 1st character of host_key may be ".", so we have to remove it
+ host_key = host_key.substr(1);
+ }
+
+ try {
+ let expiresUtc =
+ chromeTimeToDate(row.getResultByName("expires_utc")) / 1000;
+ Services.cookies.add(host_key,
+ row.getResultByName("path"),
+ row.getResultByName("name"),
+ row.getResultByName("value"),
+ row.getResultByName("secure"),
+ row.getResultByName("httponly"),
+ false,
+ parseInt(expiresUtc),
+ {});
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ aCallback(true);
+ }),
+ };
+}
+
+function GetWindowsPasswordsResource(aProfileFolder) {
+ let loginFile = aProfileFolder.clone();
+ loginFile.append("Login Data");
+ if (!loginFile.exists())
+ return null;
+
+ return {
+ type: MigrationUtils.resourceTypes.PASSWORDS,
+
+ migrate: Task.async(function* (aCallback) {
+ let rows = yield MigrationUtils.getRowsFromDBWithoutLocks(loginFile.path, "Chrome passwords",
+ `SELECT origin_url, action_url, username_element, username_value,
+ password_element, password_value, signon_realm, scheme, date_created,
+ times_used FROM logins WHERE blacklisted_by_user = 0`).catch(ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ });
+ // If the promise was rejected we will have already called aCallback,
+ // so we can just return here.
+ if (!rows) {
+ return;
+ }
+ let crypto = new OSCrypto();
+
+ for (let row of rows) {
+ try {
+ let origin_url = NetUtil.newURI(row.getResultByName("origin_url"));
+ // Ignore entries for non-http(s)/ftp URLs because we likely can't
+ // use them anyway.
+ const kValidSchemes = new Set(["https", "http", "ftp"]);
+ if (!kValidSchemes.has(origin_url.scheme)) {
+ continue;
+ }
+ let loginInfo = {
+ username: row.getResultByName("username_value"),
+ password: crypto.
+ decryptData(crypto.arrayToString(row.getResultByName("password_value")),
+ null),
+ hostname: origin_url.prePath,
+ formSubmitURL: null,
+ httpRealm: null,
+ usernameElement: row.getResultByName("username_element"),
+ passwordElement: row.getResultByName("password_element"),
+ timeCreated: chromeTimeToDate(row.getResultByName("date_created") + 0).getTime(),
+ timesUsed: row.getResultByName("times_used") + 0,
+ };
+
+ switch (row.getResultByName("scheme")) {
+ case AUTH_TYPE.SCHEME_HTML:
+ let action_url = NetUtil.newURI(row.getResultByName("action_url"));
+ if (!kValidSchemes.has(action_url.scheme)) {
+ continue; // This continues the outer for loop.
+ }
+ loginInfo.formSubmitURL = action_url.prePath;
+ break;
+ case AUTH_TYPE.SCHEME_BASIC:
+ case AUTH_TYPE.SCHEME_DIGEST:
+ // signon_realm format is URIrealm, so we need remove URI
+ loginInfo.httpRealm = row.getResultByName("signon_realm")
+ .substring(loginInfo.hostname.length + 1);
+ break;
+ default:
+ throw new Error("Login data scheme type not supported: " +
+ row.getResultByName("scheme"));
+ }
+ MigrationUtils.insertLoginWrapper(loginInfo);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ crypto.finalize();
+ aCallback(true);
+ }),
+ };
+}
+
+ChromeProfileMigrator.prototype.classDescription = "Chrome Profile Migrator";
+ChromeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chrome";
+ChromeProfileMigrator.prototype.classID = Components.ID("{4cec1de4-1671-4fc3-a53e-6c539dc77a26}");
+
+
+/**
+ * Chromium migration
+ **/
+function ChromiumProfileMigrator() {
+ let chromiumUserDataFolder = getDataFolder(["Chromium"], ["Chromium"], ["chromium"]);
+ this._chromeUserDataFolder = chromiumUserDataFolder.exists() ? chromiumUserDataFolder : null;
+}
+
+ChromiumProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
+ChromiumProfileMigrator.prototype.classDescription = "Chromium Profile Migrator";
+ChromiumProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=chromium";
+ChromiumProfileMigrator.prototype.classID = Components.ID("{8cece922-9720-42de-b7db-7cef88cb07ca}");
+
+var componentsArray = [ChromeProfileMigrator, ChromiumProfileMigrator];
+
+/**
+ * Chrome Canary
+ * Not available on Linux
+ **/
+function CanaryProfileMigrator() {
+ let chromeUserDataFolder = getDataFolder(["Google", "Chrome SxS"], ["Google", "Chrome Canary"]);
+ this._chromeUserDataFolder = chromeUserDataFolder.exists() ? chromeUserDataFolder : null;
+}
+CanaryProfileMigrator.prototype = Object.create(ChromeProfileMigrator.prototype);
+CanaryProfileMigrator.prototype.classDescription = "Chrome Canary Profile Migrator";
+CanaryProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=canary";
+CanaryProfileMigrator.prototype.classID = Components.ID("{4bf85aa5-4e21-46ca-825f-f9c51a5e8c76}");
+
+if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
+ componentsArray.push(CanaryProfileMigrator);
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray);
diff --git a/browser/components/migration/ESEDBReader.jsm b/browser/components/migration/ESEDBReader.jsm
new file mode 100644
index 000000000..0768c65aa
--- /dev/null
+++ b/browser/components/migration/ESEDBReader.jsm
@@ -0,0 +1,590 @@
+/* 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 = ["ESEDBReader"]; /* exported ESEDBReader */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/ctypes.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
+ let consoleOptions = {
+ maxLogLevelPref: "browser.esedbreader.loglevel",
+ prefix: "ESEDBReader",
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+// We have a globally unique identifier for ESE instances. A new one
+// is used for each different database opened.
+let gESEInstanceCounter = 0;
+
+// We limit the length of strings that we read from databases.
+const MAX_STR_LENGTH = 64 * 1024;
+
+// Kernel-related types:
+const KERNEL = {};
+KERNEL.FILETIME = new ctypes.StructType("FILETIME", [
+ {dwLowDateTime: ctypes.uint32_t},
+ {dwHighDateTime: ctypes.uint32_t}
+]);
+KERNEL.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
+ {wYear: ctypes.uint16_t},
+ {wMonth: ctypes.uint16_t},
+ {wDayOfWeek: ctypes.uint16_t},
+ {wDay: ctypes.uint16_t},
+ {wHour: ctypes.uint16_t},
+ {wMinute: ctypes.uint16_t},
+ {wSecond: ctypes.uint16_t},
+ {wMilliseconds: ctypes.uint16_t}
+]);
+
+// DB column types, cribbed from the ESE header
+var COLUMN_TYPES = {
+ JET_coltypBit: 1, /* True, False, or NULL */
+ JET_coltypUnsignedByte: 2, /* 1-byte integer, unsigned */
+ JET_coltypShort: 3, /* 2-byte integer, signed */
+ JET_coltypLong: 4, /* 4-byte integer, signed */
+ JET_coltypCurrency: 5, /* 8 byte integer, signed */
+ JET_coltypIEEESingle: 6, /* 4-byte IEEE single precision */
+ JET_coltypIEEEDouble: 7, /* 8-byte IEEE double precision */
+ JET_coltypDateTime: 8, /* Integral date, fractional time */
+ JET_coltypBinary: 9, /* Binary data, < 255 bytes */
+ JET_coltypText: 10, /* ANSI text, case insensitive, < 255 bytes */
+ JET_coltypLongBinary: 11, /* Binary data, long value */
+ JET_coltypLongText: 12, /* ANSI text, long value */
+
+ JET_coltypUnsignedLong: 14, /* 4-byte unsigned integer */
+ JET_coltypLongLong: 15, /* 8-byte signed integer */
+ JET_coltypGUID: 16, /* 16-byte globally unique identifier */
+};
+
+// Not very efficient, but only used for error messages
+function getColTypeName(numericValue) {
+ return Object.keys(COLUMN_TYPES).find(t => COLUMN_TYPES[t] == numericValue) || "unknown";
+}
+
+// All type constants and method wrappers go on this object:
+const ESE = {};
+ESE.JET_ERR = ctypes.long;
+ESE.JET_PCWSTR = ctypes.char16_t.ptr;
+// The ESE header calls this JET_API_PTR, but because it isn't ever used as a
+// pointer and because OS.File code implies that the name you give a type
+// matters, I opted for a different name.
+// Note that this is defined differently on 32 vs. 64-bit in the header.
+ESE.JET_API_ITEM = ctypes.voidptr_t.size == 4 ? ctypes.unsigned_long : ctypes.uint64_t;
+ESE.JET_INSTANCE = ESE.JET_API_ITEM;
+ESE.JET_SESID = ESE.JET_API_ITEM;
+ESE.JET_TABLEID = ESE.JET_API_ITEM;
+ESE.JET_COLUMNID = ctypes.unsigned_long;
+ESE.JET_GRBIT = ctypes.unsigned_long;
+ESE.JET_COLTYP = ctypes.unsigned_long;
+ESE.JET_DBID = ctypes.unsigned_long;
+
+ESE.JET_COLUMNDEF = new ctypes.StructType("JET_COLUMNDEF", [
+ {"cbStruct": ctypes.unsigned_long},
+ {"columnid": ESE.JET_COLUMNID },
+ {"coltyp": ESE.JET_COLTYP },
+ {"wCountry": ctypes.unsigned_short }, // sepcifies the country/region for the column definition
+ {"langid": ctypes.unsigned_short },
+ {"cp": ctypes.unsigned_short },
+ {"wCollate": ctypes.unsigned_short }, /* Must be 0 */
+ {"cbMax": ctypes.unsigned_long },
+ {"grbit": ESE.JET_GRBIT }
+]);
+
+// Track open databases
+let gOpenDBs = new Map();
+
+// Track open libraries
+let gLibs = {};
+this.ESE = ESE; // Required for tests.
+this.KERNEL = KERNEL; // ditto
+this.gLibs = gLibs; // ditto
+
+function convertESEError(errorCode) {
+ switch (errorCode) {
+ case -1213 /* JET_errPageSizeMismatch */:
+ case -1002 /* JET_errInvalidName*/:
+ case -1507 /* JET_errColumnNotFound */:
+ // The DB format has changed and we haven't updated this migration code:
+ return "The database format has changed, error code: " + errorCode;
+ case -1207 /* JET_errDatabaseLocked */:
+ case -1302 /* JET_errTableLocked */:
+ return "The database or table is locked, error code: " + errorCode;
+ case -1809 /* JET_errPermissionDenied*/:
+ case -1907 /* JET_errAccessDenied */:
+ return "Access or permission denied, error code: " + errorCode;
+ case -1044 /* JET_errInvalidFilename */:
+ return "Invalid file name";
+ case -1811 /* JET_errFileNotFound */:
+ return "File not found";
+ case -550 /* JET_errDatabaseDirtyShutdown */:
+ return "Database in dirty shutdown state (without the requisite logs?)";
+ case -514 /* JET_errBadLogVersion */:
+ return "Database log version does not match the version of ESE in use.";
+ default:
+ return "Unknown error: " + errorCode;
+ }
+}
+
+function handleESEError(method, methodName, shouldThrow = true, errorLog = true) {
+ return function () {
+ let rv;
+ try {
+ rv = method.apply(null, arguments);
+ } catch (ex) {
+ log.error("Error calling into ctypes method", methodName, ex);
+ throw ex;
+ }
+ let resultCode = parseInt(rv.toString(10), 10);
+ if (resultCode < 0) {
+ if (errorLog) {
+ log.error("Got error " + resultCode + " calling " + methodName);
+ }
+ if (shouldThrow) {
+ throw new Error(convertESEError(rv));
+ }
+ } else if (resultCode > 0 && errorLog) {
+ log.warn("Got warning " + resultCode + " calling " + methodName);
+ }
+ return resultCode;
+ };
+}
+
+
+function declareESEFunction(methodName, ...args) {
+ let declaration = ["Jet" + methodName, ctypes.winapi_abi, ESE.JET_ERR].concat(args);
+ let ctypeMethod = gLibs.ese.declare.apply(gLibs.ese, declaration);
+ ESE[methodName] = handleESEError(ctypeMethod, methodName);
+ ESE["FailSafe" + methodName] = handleESEError(ctypeMethod, methodName, false);
+ ESE["Manual" + methodName] = handleESEError(ctypeMethod, methodName, false, false);
+}
+
+function declareESEFunctions() {
+ declareESEFunction("GetDatabaseFileInfoW", ESE.JET_PCWSTR, ctypes.voidptr_t,
+ ctypes.unsigned_long, ctypes.unsigned_long);
+
+ declareESEFunction("GetSystemParameterW", ESE.JET_INSTANCE, ESE.JET_SESID,
+ ctypes.unsigned_long, ESE.JET_API_ITEM.ptr,
+ ESE.JET_PCWSTR, ctypes.unsigned_long);
+ declareESEFunction("SetSystemParameterW", ESE.JET_INSTANCE.ptr,
+ ESE.JET_SESID, ctypes.unsigned_long, ESE.JET_API_ITEM,
+ ESE.JET_PCWSTR);
+ declareESEFunction("CreateInstanceW", ESE.JET_INSTANCE.ptr, ESE.JET_PCWSTR);
+ declareESEFunction("Init", ESE.JET_INSTANCE.ptr);
+
+ declareESEFunction("BeginSessionW", ESE.JET_INSTANCE, ESE.JET_SESID.ptr,
+ ESE.JET_PCWSTR, ESE.JET_PCWSTR);
+ declareESEFunction("AttachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
+ ESE.JET_GRBIT);
+ declareESEFunction("DetachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR);
+ declareESEFunction("OpenDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
+ ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT);
+ declareESEFunction("OpenTableW", ESE.JET_SESID, ESE.JET_DBID, ESE.JET_PCWSTR,
+ ctypes.voidptr_t, ctypes.unsigned_long, ESE.JET_GRBIT,
+ ESE.JET_TABLEID.ptr);
+
+ declareESEFunction("GetColumnInfoW", ESE.JET_SESID, ESE.JET_DBID,
+ ESE.JET_PCWSTR, ESE.JET_PCWSTR, ctypes.voidptr_t,
+ ctypes.unsigned_long, ctypes.unsigned_long);
+
+ declareESEFunction("Move", ESE.JET_SESID, ESE.JET_TABLEID, ctypes.long,
+ ESE.JET_GRBIT);
+
+ declareESEFunction("RetrieveColumn", ESE.JET_SESID, ESE.JET_TABLEID,
+ ESE.JET_COLUMNID, ctypes.voidptr_t, ctypes.unsigned_long,
+ ctypes.unsigned_long.ptr, ESE.JET_GRBIT, ctypes.voidptr_t);
+
+ declareESEFunction("CloseTable", ESE.JET_SESID, ESE.JET_TABLEID);
+ declareESEFunction("CloseDatabase", ESE.JET_SESID, ESE.JET_DBID,
+ ESE.JET_GRBIT);
+
+ declareESEFunction("EndSession", ESE.JET_SESID, ESE.JET_GRBIT);
+
+ declareESEFunction("Term", ESE.JET_INSTANCE);
+}
+
+function unloadLibraries() {
+ log.debug("Unloading");
+ if (gOpenDBs.size) {
+ log.error("Shouldn't unload libraries before DBs are closed!");
+ for (let db of gOpenDBs.values()) {
+ db._close();
+ }
+ }
+ for (let k of Object.keys(ESE)) {
+ delete ESE[k];
+ }
+ gLibs.ese.close();
+ gLibs.kernel.close();
+ delete gLibs.ese;
+ delete gLibs.kernel;
+}
+
+function loadLibraries() {
+ Services.obs.addObserver(unloadLibraries, "xpcom-shutdown", false);
+ gLibs.ese = ctypes.open("esent.dll");
+ gLibs.kernel = ctypes.open("kernel32.dll");
+ KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime",
+ ctypes.default_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
+
+ declareESEFunctions();
+}
+
+function ESEDB(rootPath, dbPath, logPath) {
+ log.info("Created db");
+ this.rootPath = rootPath;
+ this.dbPath = dbPath;
+ this.logPath = logPath;
+ this._references = 0;
+ this._init();
+}
+
+ESEDB.prototype = {
+ rootPath: null,
+ dbPath: null,
+ logPath: null,
+ _opened: false,
+ _attached: false,
+ _sessionCreated: false,
+ _instanceCreated: false,
+ _dbId: null,
+ _sessionId: null,
+ _instanceId: null,
+
+ _init() {
+ if (!gLibs.ese) {
+ loadLibraries();
+ }
+ this.incrementReferenceCounter();
+ this._internalOpen();
+ },
+
+ _internalOpen() {
+ try {
+ let dbinfo = new ctypes.unsigned_long();
+ ESE.GetDatabaseFileInfoW(this.dbPath, dbinfo.address(),
+ ctypes.unsigned_long.size, 17);
+
+ let pageSize = ctypes.UInt64.lo(dbinfo.value);
+ ESE.SetSystemParameterW(null, 0, 64 /* JET_paramDatabasePageSize*/,
+ pageSize, null);
+
+ this._instanceId = new ESE.JET_INSTANCE();
+ ESE.CreateInstanceW(this._instanceId.address(),
+ "firefox-dbreader-" + (gESEInstanceCounter++));
+ this._instanceCreated = true;
+
+ ESE.SetSystemParameterW(this._instanceId.address(), 0,
+ 0 /* JET_paramSystemPath*/, 0, this.rootPath);
+ ESE.SetSystemParameterW(this._instanceId.address(), 0,
+ 1 /* JET_paramTempPath */, 0, this.rootPath);
+ ESE.SetSystemParameterW(this._instanceId.address(), 0,
+ 2 /* JET_paramLogFilePath*/, 0, this.logPath);
+
+ // Shouldn't try to call JetTerm if the following call fails.
+ this._instanceCreated = false;
+ ESE.Init(this._instanceId.address());
+ this._instanceCreated = true;
+ this._sessionId = new ESE.JET_SESID();
+ ESE.BeginSessionW(this._instanceId, this._sessionId.address(), null,
+ null);
+ this._sessionCreated = true;
+
+ const JET_bitDbReadOnly = 1;
+ ESE.AttachDatabaseW(this._sessionId, this.dbPath, JET_bitDbReadOnly);
+ this._attached = true;
+ this._dbId = new ESE.JET_DBID();
+ ESE.OpenDatabaseW(this._sessionId, this.dbPath, null,
+ this._dbId.address(), JET_bitDbReadOnly);
+ this._opened = true;
+ } catch (ex) {
+ try {
+ this._close();
+ } catch (innerException) {
+ Cu.reportError(innerException);
+ }
+ // Make sure caller knows we failed.
+ throw ex;
+ }
+ gOpenDBs.set(this.dbPath, this);
+ },
+
+ checkForColumn(tableName, columnName) {
+ if (!this._opened) {
+ throw new Error("The database was closed!");
+ }
+
+ let columnInfo;
+ try {
+ columnInfo = this._getColumnInfo(tableName, [{name: columnName}]);
+ } catch (ex) {
+ return null;
+ }
+ return columnInfo[0];
+ },
+
+ tableExists(tableName) {
+ if (!this._opened) {
+ throw new Error("The database was closed!");
+ }
+
+ let tableId = new ESE.JET_TABLEID();
+ let rv = ESE.ManualOpenTableW(this._sessionId, this._dbId, tableName, null,
+ 0, 4 /* JET_bitTableReadOnly */,
+ tableId.address());
+ if (rv == -1305 /* JET_errObjectNotFound */) {
+ return false;
+ }
+ if (rv < 0) {
+ log.error("Got error " + rv + " calling OpenTableW");
+ throw new Error(convertESEError(rv));
+ }
+
+ if (rv > 0) {
+ log.error("Got warning " + rv + " calling OpenTableW");
+ }
+ ESE.FailSafeCloseTable(this._sessionId, tableId);
+ return true;
+ },
+
+ tableItems: function*(tableName, columns) {
+ if (!this._opened) {
+ throw new Error("The database was closed!");
+ }
+
+ let tableOpened = false;
+ let tableId;
+ try {
+ tableId = this._openTable(tableName);
+ tableOpened = true;
+
+ let columnInfo = this._getColumnInfo(tableName, columns);
+
+ let rv = ESE.ManualMove(this._sessionId, tableId,
+ -2147483648 /* JET_MoveFirst */, 0);
+ if (rv == -1603 /* JET_errNoCurrentRecord */) {
+ // There are no rows in the table.
+ this._closeTable(tableId);
+ return;
+ }
+ if (rv != 0) {
+ throw new Error(convertESEError(rv));
+ }
+
+ do {
+ let rowContents = {};
+ for (let column of columnInfo) {
+ let [buffer, bufferSize] = this._getBufferForColumn(column);
+ // We handle errors manually so we accurately deal with NULL values.
+ let err = ESE.ManualRetrieveColumn(this._sessionId, tableId,
+ column.id, buffer.address(),
+ bufferSize, null, 0, null);
+ rowContents[column.name] = this._convertResult(column, buffer, err);
+ }
+ yield rowContents;
+ } while (ESE.ManualMove(this._sessionId, tableId, 1 /* JET_MoveNext */, 0) === 0);
+ } catch (ex) {
+ if (tableOpened) {
+ this._closeTable(tableId);
+ }
+ throw ex;
+ }
+ this._closeTable(tableId);
+ },
+
+ _openTable(tableName) {
+ let tableId = new ESE.JET_TABLEID();
+ ESE.OpenTableW(this._sessionId, this._dbId, tableName, null,
+ 0, 4 /* JET_bitTableReadOnly */, tableId.address());
+ return tableId;
+ },
+
+ _getBufferForColumn(column) {
+ let buffer;
+ if (column.type == "string") {
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ // size on the column is in bytes, 2 bytes to a wchar, so:
+ let charCount = column.dbSize >> 1;
+ buffer = new wchar_tArray(charCount);
+ } else if (column.type == "boolean") {
+ buffer = new ctypes.uint8_t();
+ } else if (column.type == "date") {
+ buffer = new KERNEL.FILETIME();
+ } else if (column.type == "guid") {
+ let byteArray = ctypes.ArrayType(ctypes.uint8_t);
+ buffer = new byteArray(column.dbSize);
+ } else {
+ throw new Error("Unknown type " + column.type);
+ }
+ return [buffer, buffer.constructor.size];
+ },
+
+ _convertResult(column, buffer, err) {
+ if (err != 0) {
+ if (err == 1004) {
+ // Deal with null values:
+ buffer = null;
+ } else {
+ Cu.reportError("Unexpected JET error: " + err + ";" + " retrieving value for column " + column.name);
+ throw new Error(convertESEError(err));
+ }
+ }
+ if (column.type == "string") {
+ return buffer ? buffer.readString() : "";
+ }
+ if (column.type == "boolean") {
+ return buffer ? (buffer.value == 255) : false;
+ }
+ if (column.type == "guid") {
+ if (buffer.length != 16) {
+ Cu.reportError("Buffer size for guid field " + column.id + " should have been 16!");
+ return "";
+ }
+ let rv = "{";
+ for (let i = 0; i < 16; i++) {
+ if (i == 4 || i == 6 || i == 8 || i == 10) {
+ rv += "-";
+ }
+ let byteValue = buffer.addressOfElement(i).contents;
+ // Ensure there's a leading 0
+ rv += ("0" + byteValue.toString(16)).substr(-2);
+ }
+ return rv + "}";
+ }
+ if (column.type == "date") {
+ if (!buffer) {
+ return null;
+ }
+ let systemTime = new KERNEL.SYSTEMTIME();
+ let result = KERNEL.FileTimeToSystemTime(buffer.address(), systemTime.address());
+ if (result == 0) {
+ throw new Error(ctypes.winLastError);
+ }
+
+ // System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
+ // then divide by 1000 to get seconds, and round down:
+ return new Date(Date.UTC(systemTime.wYear,
+ systemTime.wMonth - 1,
+ systemTime.wDay,
+ systemTime.wHour,
+ systemTime.wMinute,
+ systemTime.wSecond,
+ systemTime.wMilliseconds));
+ }
+ return undefined;
+ },
+
+ _getColumnInfo(tableName, columns) {
+ let rv = [];
+ for (let column of columns) {
+ let columnInfoFromDB = new ESE.JET_COLUMNDEF();
+ ESE.GetColumnInfoW(this._sessionId, this._dbId, tableName, column.name,
+ columnInfoFromDB.address(), ESE.JET_COLUMNDEF.size, 0 /* JET_ColInfo */);
+ let dbType = parseInt(columnInfoFromDB.coltyp.toString(10), 10);
+ let dbSize = parseInt(columnInfoFromDB.cbMax.toString(10), 10);
+ if (column.type == "string") {
+ if (dbType != COLUMN_TYPES.JET_coltypLongText &&
+ dbType != COLUMN_TYPES.JET_coltypText) {
+ throw new Error("Invalid column type for column " + column.name +
+ "; expected text type, got type " + getColTypeName(dbType));
+ }
+ if (dbSize > MAX_STR_LENGTH) {
+ throw new Error("Column " + column.name + " has more than 64k data in it. This API is not designed to handle data that large.");
+ }
+ } else if (column.type == "boolean") {
+ if (dbType != COLUMN_TYPES.JET_coltypBit) {
+ throw new Error("Invalid column type for column " + column.name +
+ "; expected bit type, got type " + getColTypeName(dbType));
+ }
+ } else if (column.type == "date") {
+ if (dbType != COLUMN_TYPES.JET_coltypLongLong) {
+ throw new Error("Invalid column type for column " + column.name +
+ "; expected long long type, got type " + getColTypeName(dbType));
+ }
+ } else if (column.type == "guid") {
+ if (dbType != COLUMN_TYPES.JET_coltypGUID) {
+ throw new Error("Invalid column type for column " + column.name +
+ "; expected guid type, got type " + getColTypeName(dbType));
+ }
+ } else if (column.type) {
+ throw new Error("Unknown column type " + column.type + " requested for column " +
+ column.name + ", don't know what to do.");
+ }
+
+ rv.push({name: column.name, id: columnInfoFromDB.columnid, type: column.type, dbSize, dbType});
+ }
+ return rv;
+ },
+
+ _closeTable(tableId) {
+ ESE.FailSafeCloseTable(this._sessionId, tableId);
+ },
+
+ _close() {
+ this._internalClose();
+ gOpenDBs.delete(this.dbPath);
+ },
+
+ _internalClose() {
+ if (this._opened) {
+ log.debug("close db");
+ ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
+ log.debug("finished close db");
+ this._opened = false;
+ }
+ if (this._attached) {
+ log.debug("detach db");
+ ESE.FailSafeDetachDatabaseW(this._sessionId, this.dbPath);
+ this._attached = false;
+ }
+ if (this._sessionCreated) {
+ log.debug("end session");
+ ESE.FailSafeEndSession(this._sessionId, 0);
+ this._sessionCreated = false;
+ }
+ if (this._instanceCreated) {
+ log.debug("term");
+ ESE.FailSafeTerm(this._instanceId);
+ this._instanceCreated = false;
+ }
+ },
+
+ incrementReferenceCounter() {
+ this._references++;
+ },
+
+ decrementReferenceCounter() {
+ this._references--;
+ if (this._references <= 0) {
+ this._close();
+ }
+ },
+};
+
+let ESEDBReader = {
+ openDB(rootDir, dbFile, logDir) {
+ let dbFilePath = dbFile.path;
+ if (gOpenDBs.has(dbFilePath)) {
+ let db = gOpenDBs.get(dbFilePath);
+ db.incrementReferenceCounter();
+ return db;
+ }
+ // ESE is really picky about the trailing slashes according to the docs,
+ // so we do as we're told and ensure those are there:
+ return new ESEDB(rootDir.path + "\\", dbFilePath, logDir.path + "\\");
+ },
+
+ closeDB(db) {
+ db.decrementReferenceCounter();
+ },
+
+ COLUMN_TYPES,
+};
+
diff --git a/browser/components/migration/EdgeProfileMigrator.js b/browser/components/migration/EdgeProfileMigrator.js
new file mode 100644
index 000000000..afdcc2773
--- /dev/null
+++ b/browser/components/migration/EdgeProfileMigrator.js
@@ -0,0 +1,450 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
+Cu.import("resource:///modules/MSMigrationUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ESEDBReader",
+ "resource:///modules/ESEDBReader.jsm");
+
+const kEdgeRegistryRoot = "SOFTWARE\\Classes\\Local Settings\\Software\\" +
+ "Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\" +
+ "microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge";
+const kEdgeDatabasePath = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
+
+XPCOMUtils.defineLazyGetter(this, "gEdgeDatabase", function() {
+ let edgeDir = MSMigrationUtils.getEdgeLocalDataFolder();
+ if (!edgeDir) {
+ return null;
+ }
+ edgeDir.appendRelativePath(kEdgeDatabasePath);
+ if (!edgeDir.exists() || !edgeDir.isReadable() || !edgeDir.isDirectory()) {
+ return null;
+ }
+ let expectedLocation = edgeDir.clone();
+ expectedLocation.appendRelativePath("nouser1\\120712-0049\\DBStore\\spartan.edb");
+ if (expectedLocation.exists() && expectedLocation.isReadable() && expectedLocation.isFile()) {
+ return expectedLocation;
+ }
+ // We used to recurse into arbitrary subdirectories here, but that code
+ // went unused, so it likely isn't necessary, even if we don't understand
+ // where the magic folders above come from, they seem to be the same for
+ // everyone. Just return null if they're not there:
+ return null;
+});
+
+/**
+ * Get rows from a table in the Edge DB as an array of JS objects.
+ *
+ * @param {String} tableName the name of the table to read.
+ * @param {String[]|function} columns a list of column specifiers
+ * (see ESEDBReader.jsm) or a function that
+ * generates them based on the database
+ * reference once opened.
+ * @param {function} filterFn a function that is called for each row.
+ * Only rows for which it returns a truthy
+ * value are included in the result.
+ * @param {nsIFile} dbFile the database file to use. Defaults to
+ * the main Edge database.
+ * @returns {Array} An array of row objects.
+ */
+function readTableFromEdgeDB(tableName, columns, filterFn, dbFile = gEdgeDatabase) {
+ let database;
+ let rows = [];
+ try {
+ let logFile = dbFile.parent;
+ logFile.append("LogFiles");
+ database = ESEDBReader.openDB(dbFile.parent, dbFile, logFile);
+
+ if (typeof columns == "function") {
+ columns = columns(database);
+ }
+
+ let tableReader = database.tableItems(tableName, columns);
+ for (let row of tableReader) {
+ if (filterFn(row)) {
+ rows.push(row);
+ }
+ }
+ } catch (ex) {
+ Cu.reportError("Failed to extract items from table " + tableName + " in Edge database at " +
+ dbFile.path + " due to the following error: " + ex);
+ // Deliberately make this fail so we expose failure in the UI:
+ throw ex;
+ } finally {
+ if (database) {
+ ESEDBReader.closeDB(database);
+ }
+ }
+ return rows;
+}
+
+function EdgeTypedURLMigrator() {
+}
+
+EdgeTypedURLMigrator.prototype = {
+ type: MigrationUtils.resourceTypes.HISTORY,
+
+ get _typedURLs() {
+ if (!this.__typedURLs) {
+ this.__typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot);
+ }
+ return this.__typedURLs;
+ },
+
+ get exists() {
+ return this._typedURLs.size > 0;
+ },
+
+ migrate: function(aCallback) {
+ let typedURLs = this._typedURLs;
+ let places = [];
+ for (let [urlString, time] of typedURLs) {
+ let uri;
+ try {
+ uri = Services.io.newURI(urlString, null, null);
+ if (["http", "https", "ftp"].indexOf(uri.scheme) == -1) {
+ continue;
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ continue;
+ }
+
+ // Note that the time will be in microseconds (PRTime),
+ // and Date.now() returns milliseconds. Places expects PRTime,
+ // so we multiply the Date.now return value to make up the difference.
+ let visitDate = time || (Date.now() * 1000);
+ places.push({
+ uri,
+ visits: [{ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ visitDate}]
+ });
+ }
+
+ if (places.length == 0) {
+ aCallback(typedURLs.size == 0);
+ return;
+ }
+
+ MigrationUtils.insertVisitsWrapper(places, {
+ _success: false,
+ handleResult: function() {
+ // Importing any entry is considered a successful import.
+ this._success = true;
+ },
+ handleError: function() {},
+ handleCompletion: function() {
+ aCallback(this._success);
+ }
+ });
+ },
+};
+
+function EdgeReadingListMigrator() {
+}
+
+EdgeReadingListMigrator.prototype = {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+ get exists() {
+ return !!gEdgeDatabase;
+ },
+
+ migrate(callback) {
+ this._migrateReadingList(PlacesUtils.bookmarks.menuGuid).then(
+ () => callback(true),
+ ex => {
+ Cu.reportError(ex);
+ callback(false);
+ }
+ );
+ },
+
+ _migrateReadingList: Task.async(function*(parentGuid) {
+ let columnFn = db => {
+ let columns = [
+ {name: "URL", type: "string"},
+ {name: "Title", type: "string"},
+ {name: "AddedDate", type: "date"}
+ ];
+
+ // Later versions have an IsDeleted column:
+ let isDeletedColumn = db.checkForColumn("ReadingList", "IsDeleted");
+ if (isDeletedColumn && isDeletedColumn.dbType == ESEDBReader.COLUMN_TYPES.JET_coltypBit) {
+ columns.push({name: "IsDeleted", type: "boolean"});
+ }
+ return columns;
+ };
+
+ let filterFn = row => {
+ return !row.IsDeleted;
+ };
+
+ let readingListItems = readTableFromEdgeDB("ReadingList", columnFn, filterFn);
+ if (!readingListItems.length) {
+ return;
+ }
+
+ let destFolderGuid = yield this._ensureReadingListFolder(parentGuid);
+ let exceptionThrown;
+ for (let item of readingListItems) {
+ let dateAdded = item.AddedDate || new Date();
+ yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid: destFolderGuid, url: item.URL, title: item.Title, dateAdded
+ }).catch(ex => {
+ if (!exceptionThrown) {
+ exceptionThrown = ex;
+ }
+ Cu.reportError(ex);
+ });
+ }
+ if (exceptionThrown) {
+ throw exceptionThrown;
+ }
+ }),
+
+ _ensureReadingListFolder: Task.async(function*(parentGuid) {
+ if (!this.__readingListFolderGuid) {
+ let folderTitle = MigrationUtils.getLocalizedString("importedEdgeReadingList");
+ let folderSpec = {type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title: folderTitle};
+ this.__readingListFolderGuid = (yield MigrationUtils.insertBookmarkWrapper(folderSpec)).guid;
+ }
+ return this.__readingListFolderGuid;
+ }),
+};
+
+function EdgeBookmarksMigrator(dbOverride) {
+ this.dbOverride = dbOverride;
+}
+
+EdgeBookmarksMigrator.prototype = {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+ get db() { return this.dbOverride || gEdgeDatabase },
+
+ get TABLE_NAME() { return "Favorites" },
+
+ get exists() {
+ if (!("_exists" in this)) {
+ this._exists = !!this.db;
+ }
+ return this._exists;
+ },
+
+ migrate(callback) {
+ this._migrateBookmarks(PlacesUtils.bookmarks.menuGuid).then(
+ () => callback(true),
+ ex => {
+ Cu.reportError(ex);
+ callback(false);
+ }
+ );
+ },
+
+ _migrateBookmarks: Task.async(function*(rootGuid) {
+ let {bookmarks, folderMap} = this._fetchBookmarksFromDB();
+ if (!bookmarks.length) {
+ return;
+ }
+ yield this._importBookmarks(bookmarks, folderMap, rootGuid);
+ }),
+
+ _importBookmarks: Task.async(function*(bookmarks, folderMap, rootGuid) {
+ if (!MigrationUtils.isStartupMigration) {
+ rootGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("Edge", rootGuid);
+ }
+
+ let exceptionThrown;
+ for (let bookmark of bookmarks) {
+ // If this is a folder, we might have created it already to put other bookmarks in.
+ if (bookmark.IsFolder && bookmark._guid) {
+ continue;
+ }
+
+ // If this is a folder, just create folders up to and including that folder.
+ // Otherwise, create folders until we have a parent for this bookmark.
+ // This avoids duplicating logic for the bookmarks bar.
+ let folderId = bookmark.IsFolder ? bookmark.ItemId : bookmark.ParentId;
+ let parentGuid = yield this._getGuidForFolder(folderId, folderMap, rootGuid).catch(ex => {
+ if (!exceptionThrown) {
+ exceptionThrown = ex;
+ }
+ Cu.reportError(ex);
+ });
+
+ // If this was a folder, we're done with this item
+ if (bookmark.IsFolder) {
+ continue;
+ }
+
+ if (!parentGuid) {
+ // If we couldn't sort out a parent, fall back to importing on the root:
+ parentGuid = rootGuid;
+ }
+ let placesInfo = {
+ parentGuid,
+ url: bookmark.URL,
+ dateAdded: bookmark.DateUpdated || new Date(),
+ title: bookmark.Title,
+ };
+
+ yield MigrationUtils.insertBookmarkWrapper(placesInfo).catch(ex => {
+ if (!exceptionThrown) {
+ exceptionThrown = ex;
+ }
+ Cu.reportError(ex);
+ });
+ }
+
+ if (exceptionThrown) {
+ throw exceptionThrown;
+ }
+ }),
+
+ _fetchBookmarksFromDB() {
+ let folderMap = new Map();
+ let columns = [
+ {name: "URL", type: "string"},
+ {name: "Title", type: "string"},
+ {name: "DateUpdated", type: "date"},
+ {name: "IsFolder", type: "boolean"},
+ {name: "IsDeleted", type: "boolean"},
+ {name: "ParentId", type: "guid"},
+ {name: "ItemId", type: "guid"}
+ ];
+ let filterFn = row => {
+ if (row.IsDeleted) {
+ return false;
+ }
+ if (row.IsFolder) {
+ folderMap.set(row.ItemId, row);
+ }
+ return true;
+ };
+ let bookmarks = readTableFromEdgeDB(this.TABLE_NAME, columns, filterFn, this.db);
+ return {bookmarks, folderMap};
+ },
+
+ _getGuidForFolder: Task.async(function*(folderId, folderMap, rootGuid) {
+ // If the folderId is not known as a folder in the folder map, we assume
+ // we just need the root
+ if (!folderMap.has(folderId)) {
+ return rootGuid;
+ }
+ let folder = folderMap.get(folderId);
+ // If the folder already has a places guid, just return that.
+ if (folder._guid) {
+ return folder._guid;
+ }
+
+ // Hacks! The bookmarks bar is special:
+ if (folder.Title == "_Favorites_Bar_") {
+ let toolbarGuid = PlacesUtils.bookmarks.toolbarGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ toolbarGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("Edge", toolbarGuid);
+ }
+ folder._guid = toolbarGuid;
+ return folder._guid;
+ }
+ // Otherwise, get the right parent guid recursively:
+ let parentGuid = yield this._getGuidForFolder(folder.ParentId, folderMap, rootGuid);
+ let folderInfo = {
+ title: folder.Title,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ dateAdded: folder.DateUpdated || new Date(),
+ parentGuid,
+ };
+ // and add ourselves as a kid, and return the guid we got.
+ let parentBM = yield MigrationUtils.insertBookmarkWrapper(folderInfo);
+ folder._guid = parentBM.guid;
+ return folder._guid;
+ }),
+};
+
+function EdgeProfileMigrator() {
+ this.wrappedJSObject = this;
+}
+
+EdgeProfileMigrator.prototype = Object.create(MigratorPrototype);
+
+EdgeProfileMigrator.prototype.getESEMigratorForTesting = function(dbOverride) {
+ return new EdgeBookmarksMigrator(dbOverride);
+};
+
+EdgeProfileMigrator.prototype.getResources = function() {
+ let resources = [
+ new EdgeBookmarksMigrator(),
+ MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
+ new EdgeTypedURLMigrator(),
+ new EdgeReadingListMigrator(),
+ ];
+ let windowsVaultFormPasswordsMigrator =
+ MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
+ windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords";
+ resources.push(windowsVaultFormPasswordsMigrator);
+ return resources.filter(r => r.exists);
+};
+
+EdgeProfileMigrator.prototype.getLastUsedDate = function() {
+ // Don't do this if we don't have a single profile (see the comment for
+ // sourceProfiles) or if we can't find the database file:
+ if (this.sourceProfiles !== null || !gEdgeDatabase) {
+ return Promise.resolve(new Date(0));
+ }
+ let logFilePath = OS.Path.join(gEdgeDatabase.parent.path, "LogFiles", "edb.log");
+ let dbPath = gEdgeDatabase.path;
+ let cookieMigrator = MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE);
+ let cookiePaths = cookieMigrator._cookiesFolders.map(f => f.path);
+ let datePromises = [logFilePath, dbPath, ... cookiePaths].map(path => {
+ return OS.File.stat(path).catch(() => null).then(info => {
+ return info ? info.lastModificationDate : 0;
+ });
+ });
+ datePromises.push(new Promise(resolve => {
+ let typedURLs = new Map();
+ try {
+ typedURLs = MSMigrationUtils.getTypedURLs(kEdgeRegistryRoot);
+ } catch (ex) {}
+ let times = [0, ... typedURLs.values()];
+ resolve(Math.max.apply(Math, times));
+ }));
+ return Promise.all(datePromises).then(dates => {
+ return new Date(Math.max.apply(Math, dates));
+ });
+};
+
+/* Somewhat counterintuitively, this returns:
+ * - |null| to indicate "There is only 1 (default) profile" (on win10+)
+ * - |[]| to indicate "There are no profiles" (on <=win8.1) which will avoid using this migrator.
+ * See MigrationUtils.jsm for slightly more info on how sourceProfiles is used.
+ */
+EdgeProfileMigrator.prototype.__defineGetter__("sourceProfiles", function() {
+ let isWin10OrHigher = AppConstants.isPlatformAndVersionAtLeast("win", "10");
+ return isWin10OrHigher ? null : [];
+});
+
+EdgeProfileMigrator.prototype.__defineGetter__("sourceLocked", function() {
+ // There is an exclusive lock on some databases. Assume they are locked for now.
+ return true;
+});
+
+
+EdgeProfileMigrator.prototype.classDescription = "Edge Profile Migrator";
+EdgeProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=edge";
+EdgeProfileMigrator.prototype.classID = Components.ID("{62e8834b-2d17-49f5-96ff-56344903a2ae}");
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([EdgeProfileMigrator]);
diff --git a/browser/components/migration/FirefoxProfileMigrator.js b/browser/components/migration/FirefoxProfileMigrator.js
new file mode 100644
index 000000000..60ffcf627
--- /dev/null
+++ b/browser/components/migration/FirefoxProfileMigrator.js
@@ -0,0 +1,255 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et */
+ /* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * Migrates from a Firefox 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.
+ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
+ "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SessionMigration",
+ "resource:///modules/sessionstore/SessionMigration.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileAge",
+ "resource://gre/modules/ProfileAge.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+ "resource://gre/modules/AppConstants.jsm");
+
+
+function FirefoxProfileMigrator() {
+ this.wrappedJSObject = this; // for testing...
+}
+
+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(Ci.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 [...this._getAllProfiles().keys()].map(x => ({id: x, name: x})).sort(sorter);
+ }
+});
+
+FirefoxProfileMigrator.prototype._getFileObject = function(dir, fileName) {
+ let file = dir.clone();
+ file.append(fileName);
+
+ // File resources are monolithic. We don't make partial copies since
+ // they are not expected to work alone. Return null to avoid trying to
+ // copy non-existing files.
+ return file.exists() ? file : null;
+};
+
+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;
+
+ return this._getResourcesInternal(sourceProfileDir, currentProfileDir);
+};
+
+FirefoxProfileMigrator.prototype.getLastUsedDate = function() {
+ // We always pretend we're really old, so that we don't mess
+ // up the determination of which browser is the most 'recent'
+ // to import from.
+ return Promise.resolve(new Date(0));
+};
+
+FirefoxProfileMigrator.prototype._getResourcesInternal = function(sourceProfileDir, currentProfileDir) {
+ let getFileResource = function(aMigrationType, aFileNames) {
+ let files = [];
+ for (let fileName of aFileNames) {
+ let file = this._getFileObject(sourceProfileDir, fileName);
+ if (file)
+ files.push(file);
+ }
+ if (!files.length) {
+ return null;
+ }
+ return {
+ type: aMigrationType,
+ migrate: function(aCallback) {
+ for (let file of files) {
+ file.copyTo(currentProfileDir, "");
+ }
+ aCallback(true);
+ }
+ };
+ }.bind(this);
+
+ 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",
+ "signedInUser.json"]);
+ let formData = getFileResource(types.FORMDATA, ["formhistory.sqlite"]);
+ let bookmarksBackups = getFileResource(types.OTHERDATA,
+ [PlacesBackups.profileRelativeFolderPath]);
+ let dictionary = getFileResource(types.OTHERDATA, ["persdict.dat"]);
+
+ let sessionCheckpoints = this._getFileObject(sourceProfileDir, "sessionCheckpoints.json");
+ let sessionFile = this._getFileObject(sourceProfileDir, "sessionstore.js");
+ let session;
+ if (sessionFile) {
+ session = {
+ type: types.SESSION,
+ migrate: function(aCallback) {
+ sessionCheckpoints.copyTo(currentProfileDir, "sessionCheckpoints.json");
+ let newSessionFile = currentProfileDir.clone();
+ newSessionFile.append("sessionstore.js");
+ let migrationPromise = SessionMigration.migrate(sessionFile.path, newSessionFile.path);
+ migrationPromise.then(function() {
+ let buildID = Services.appinfo.platformBuildID;
+ let mstone = Services.appinfo.platformVersion;
+ // Force the browser to one-off resume the session that we give it:
+ Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true);
+ // Reset the homepage_override prefs so that the browser doesn't override our
+ // session with the "what's new" page:
+ Services.prefs.setCharPref("browser.startup.homepage_override.mstone", mstone);
+ Services.prefs.setCharPref("browser.startup.homepage_override.buildID", buildID);
+ // It's too early in startup for the pref service to have a profile directory,
+ // so we have to manually tell it where to save the prefs file.
+ let newPrefsFile = currentProfileDir.clone();
+ newPrefsFile.append("prefs.js");
+ Services.prefs.savePrefFile(newPrefsFile);
+ aCallback(true);
+ }, function() {
+ aCallback(false);
+ });
+ }
+ };
+ }
+
+ // Telemetry related migrations.
+ let times = {
+ name: "times", // name is used only by tests.
+ type: types.OTHERDATA,
+ migrate: aCallback => {
+ let file = this._getFileObject(sourceProfileDir, "times.json");
+ if (file) {
+ file.copyTo(currentProfileDir, "");
+ }
+ // And record the fact a migration (ie, a reset) happened.
+ let timesAccessor = new ProfileAge(currentProfileDir.path);
+ timesAccessor.recordProfileReset().then(
+ () => aCallback(true),
+ () => aCallback(false)
+ );
+ }
+ };
+ let telemetry = {
+ name: "telemetry", // name is used only by tests...
+ type: types.OTHERDATA,
+ migrate: aCallback => {
+ let createSubDir = (name) => {
+ let dir = currentProfileDir.clone();
+ dir.append(name);
+ dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ return dir;
+ };
+
+ // If the 'datareporting' directory exists we migrate files from it.
+ let haveStateFile = false;
+ let dataReportingDir = this._getFileObject(sourceProfileDir, "datareporting");
+ if (dataReportingDir && dataReportingDir.isDirectory()) {
+ // Copy only specific files.
+ let toCopy = ["state.json", "session-state.json"];
+
+ let dest = createSubDir("datareporting");
+ let enumerator = dataReportingDir.directoryEntries;
+ while (enumerator.hasMoreElements()) {
+ let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
+ if (file.isDirectory() || toCopy.indexOf(file.leafName) == -1) {
+ continue;
+ }
+
+ if (file.leafName == "state.json") {
+ haveStateFile = true;
+ }
+ file.copyTo(dest, "");
+ }
+ }
+
+ if (!haveStateFile) {
+ // Fall back to migrating the state file that contains the client id from healthreport/.
+ // We first moved the client id management from the FHR implementation to the datareporting
+ // service.
+ // Consequently, we try to migrate an existing FHR state file here as a fallback.
+ let healthReportDir = this._getFileObject(sourceProfileDir, "healthreport");
+ if (healthReportDir && healthReportDir.isDirectory()) {
+ let stateFile = this._getFileObject(healthReportDir, "state.json");
+ if (stateFile) {
+ let dest = createSubDir("healthreport");
+ stateFile.copyTo(dest, "");
+ }
+ }
+ }
+
+ aCallback(true);
+ }
+ };
+
+ return [places, cookies, passwords, formData, dictionary, bookmarksBackups,
+ session, times, telemetry].filter(r => r);
+};
+
+Object.defineProperty(FirefoxProfileMigrator.prototype, "startupOnlyMigrator", {
+ get: () => 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/browser/components/migration/IEProfileMigrator.js b/browser/components/migration/IEProfileMigrator.js
new file mode 100644
index 000000000..ac055686c
--- /dev/null
+++ b/browser/components/migration/IEProfileMigrator.js
@@ -0,0 +1,542 @@
+/* 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 kLoginsKey = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
+const kMainKey = "Software\\Microsoft\\Internet Explorer\\Main";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
+Cu.import("resource:///modules/MSMigrationUtils.jsm");
+
+
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+ "resource://gre/modules/ctypes.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
+ "resource://gre/modules/OSCrypto.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm");
+
+Cu.importGlobalProperties(["URL"]);
+
+// Resources
+
+function History() {
+}
+
+History.prototype = {
+ type: MigrationUtils.resourceTypes.HISTORY,
+
+ get exists() {
+ return true;
+ },
+
+ migrate: function H_migrate(aCallback) {
+ let places = [];
+ let typedURLs = MSMigrationUtils.getTypedURLs("Software\\Microsoft\\Internet Explorer");
+ 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 = typedURLs.has(uri.spec) ?
+ Ci.nsINavHistoryService.TRANSITION_TYPED :
+ Ci.nsINavHistoryService.TRANSITION_LINK;
+ // use the current date if we have no visits for this entry.
+ // Note that the entry will have a time in microseconds (PRTime),
+ // and Date.now() returns milliseconds. Places expects PRTime,
+ // so we multiply the Date.now return value to make up the difference.
+ let lastVisitTime = entry.get("time") || (Date.now() * 1000);
+
+ 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;
+ }
+
+ MigrationUtils.insertVisitsWrapper(places, {
+ _success: false,
+ handleResult: function() {
+ // Importing any entry is considered a successful import.
+ this._success = true;
+ },
+ handleError: function() {},
+ handleCompletion: function() {
+ aCallback(this._success);
+ }
+ });
+ }
+};
+
+// IE form password migrator supporting windows from XP until 7 and IE from 7 until 11
+function IE7FormPasswords() {
+ // used to distinguish between this migrator and other passwords migrators in tests.
+ this.name = "IE7FormPasswords";
+}
+
+IE7FormPasswords.prototype = {
+ type: MigrationUtils.resourceTypes.PASSWORDS,
+
+ get exists() {
+ // work only on windows until 7
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ return false;
+ }
+
+ try {
+ let nsIWindowsRegKey = Ci.nsIWindowsRegKey;
+ let key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(nsIWindowsRegKey);
+ key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kLoginsKey,
+ nsIWindowsRegKey.ACCESS_READ);
+ let count = key.valueCount;
+ key.close();
+ return count > 0;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ migrate(aCallback) {
+ let historyEnumerator = Cc["@mozilla.org/profile/migrator/iehistoryenumerator;1"].
+ createInstance(Ci.nsISimpleEnumerator);
+ let uris = []; // the uris of the websites that are going to be migrated
+ 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 are not going to import the logins that are performed in these URLs
+ // we can just skip them.
+ if (["http", "https", "ftp"].indexOf(uri.scheme) == -1) {
+ continue;
+ }
+
+ uris.push(uri);
+ }
+ this._migrateURIs(uris);
+ aCallback(true);
+ },
+
+ /**
+ * Migrate the logins that were saved for the uris arguments.
+ * @param {nsIURI[]} uris - the uris that are going to be migrated.
+ */
+ _migrateURIs(uris) {
+ this.ctypesKernelHelpers = new MSMigrationUtils.CtypesKernelHelpers();
+ this._crypto = new OSCrypto();
+ let nsIWindowsRegKey = Ci.nsIWindowsRegKey;
+ let key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(nsIWindowsRegKey);
+ key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kLoginsKey,
+ nsIWindowsRegKey.ACCESS_READ);
+
+ let urlsSet = new Set(); // set of the already processed urls.
+ // number of the successfully decrypted registry values
+ let successfullyDecryptedValues = 0;
+ /* The logins are stored in the registry, where the key is a hashed URL and its
+ * value contains the encrypted details for all logins for that URL.
+ *
+ * First iterate through IE history, hashing each URL and looking for a match. If
+ * found, decrypt the value, using the URL as a salt. Finally add any found logins
+ * to the Firefox password manager.
+ */
+
+ for (let uri of uris) {
+ try {
+ // remove the query and the ref parts of the URL
+ let urlObject = new URL(uri.spec);
+ let url = urlObject.origin + urlObject.pathname;
+ // if the current url is already processed, it should be skipped
+ if (urlsSet.has(url)) {
+ continue;
+ }
+ urlsSet.add(url);
+ // hash value of the current uri
+ let hashStr = this._crypto.getIELoginHash(url);
+ if (!key.hasValue(hashStr)) {
+ continue;
+ }
+ let value = key.readBinaryValue(hashStr);
+ // if no value was found, the uri is skipped
+ if (value == null) {
+ continue;
+ }
+ let data;
+ try {
+ // the url is used as salt to decrypt the registry value
+ data = this._crypto.decryptData(value, url, true);
+ } catch (e) {
+ continue;
+ }
+ // extract the login details from the decrypted data
+ let ieLogins = this._extractDetails(data, uri);
+ // if at least a credential was found in the current data, successfullyDecryptedValues should
+ // be incremented by one
+ if (ieLogins.length) {
+ successfullyDecryptedValues++;
+ }
+ this._addLogins(ieLogins);
+ } catch (e) {
+ Cu.reportError("Error while importing logins for " + uri.spec + ": " + e);
+ }
+ }
+ // if the number of the imported values is less than the number of values in the key, it means
+ // that not all the values were imported and an error should be reported
+ if (successfullyDecryptedValues < key.valueCount) {
+ Cu.reportError("We failed to decrypt and import some logins. " +
+ "This is likely because we didn't find the URLs where these " +
+ "passwords were submitted in the IE history and which are needed to be used " +
+ "as keys in the decryption.");
+ }
+
+ key.close();
+ this._crypto.finalize();
+ this.ctypesKernelHelpers.finalize();
+ },
+
+ _crypto: null,
+
+ /**
+ * Add the logins to the password manager.
+ * @param {Object[]} logins - array of the login details.
+ */
+ _addLogins(ieLogins) {
+ for (let ieLogin of ieLogins) {
+ try {
+ // create a new login
+ let login = {
+ username: ieLogin.username,
+ password: ieLogin.password,
+ hostname: ieLogin.url,
+ timeCreated: ieLogin.creation,
+ };
+ MigrationUtils.insertLoginWrapper(login);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ /**
+ * Extract the details of one or more logins from the raw decrypted data.
+ * @param {string} data - the decrypted data containing raw information.
+ * @param {nsURI} uri - the nsURI of page where the login has occur.
+ * @returns {Object[]} array of objects where each of them contains the username, password, URL,
+ * and creation time representing all the logins found in the data arguments.
+ */
+ _extractDetails(data, uri) {
+ // the structure of the header of the IE7 decrypted data for all the logins sharing the same URL
+ let loginData = new ctypes.StructType("loginData", [
+ // Bytes 0-3 are not needed and not documented
+ {"unknown1": ctypes.uint32_t},
+ // Bytes 4-7 are the header size
+ {"headerSize": ctypes.uint32_t},
+ // Bytes 8-11 are the data size
+ {"dataSize": ctypes.uint32_t},
+ // Bytes 12-19 are not needed and not documented
+ {"unknown2": ctypes.uint32_t},
+ {"unknown3": ctypes.uint32_t},
+ // Bytes 20-23 are the data count: each username and password is considered as a data
+ {"dataMax": ctypes.uint32_t},
+ // Bytes 24-35 are not needed and not documented
+ {"unknown4": ctypes.uint32_t},
+ {"unknown5": ctypes.uint32_t},
+ {"unknown6": ctypes.uint32_t}
+ ]);
+
+ // the structure of a IE7 decrypted login item
+ let loginItem = new ctypes.StructType("loginItem", [
+ // Bytes 0-3 are the offset of the username
+ {"usernameOffset": ctypes.uint32_t},
+ // Bytes 4-11 are the date
+ {"loDateTime": ctypes.uint32_t},
+ {"hiDateTime": ctypes.uint32_t},
+ // Bytes 12-15 are not needed and not documented
+ {"foo": ctypes.uint32_t},
+ // Bytes 16-19 are the offset of the password
+ {"passwordOffset": ctypes.uint32_t},
+ // Bytes 20-31 are not needed and not documented
+ {"unknown1": ctypes.uint32_t},
+ {"unknown2": ctypes.uint32_t},
+ {"unknown3": ctypes.uint32_t}
+ ]);
+
+ let url = uri.prePath;
+ let results = [];
+ let arr = this._crypto.stringToArray(data);
+ // convert data to ctypes.unsigned_char.array(arr.length)
+ let cdata = ctypes.unsigned_char.array(arr.length)(arr);
+ // Bytes 0-35 contain the loginData data structure for all the logins sharing the same URL
+ let currentLoginData = ctypes.cast(cdata, loginData);
+ let headerSize = currentLoginData.headerSize;
+ let currentInfoIndex = loginData.size;
+ // pointer to the current login item
+ let currentLoginItemPointer = ctypes.cast(cdata.addressOfElement(currentInfoIndex),
+ loginItem.ptr);
+ // currentLoginData.dataMax is the data count: each username and password is considered as
+ // a data. So, the number of logins is the number of data dived by 2
+ let numLogins = currentLoginData.dataMax / 2;
+ for (let n = 0; n < numLogins; n++) {
+ // Bytes 0-31 starting from currentInfoIndex contain the loginItem data structure for the
+ // current login
+ let currentLoginItem = currentLoginItemPointer.contents;
+ let creation = this.ctypesKernelHelpers.
+ fileTimeToSecondsSinceEpoch(currentLoginItem.hiDateTime,
+ currentLoginItem.loDateTime) * 1000;
+ let currentResult = {
+ creation: creation,
+ url: url,
+ };
+ // The username is UTF-16 and null-terminated.
+ currentResult.username =
+ ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.usernameOffset),
+ ctypes.char16_t.ptr).readString();
+ // The password is UTF-16 and null-terminated.
+ currentResult.password =
+ ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.passwordOffset),
+ ctypes.char16_t.ptr).readString();
+ results.push(currentResult);
+ // move to the next login item
+ currentLoginItemPointer = currentLoginItemPointer.increment();
+ }
+ return results;
+ },
+};
+
+function Settings() {
+}
+
+Settings.prototype = {
+ type: MigrationUtils.resourceTypes.SETTINGS,
+
+ get exists() {
+ return true;
+ },
+
+ migrate: function S_migrate(aCallback) {
+ // Converts from yes/no to a boolean.
+ let yesNoToBoolean = v => v == "yes";
+
+ // Converts source format like "en-us,ar-kw;q=0.7,ar-om;q=0.3" into
+ // destination format like "en-us, ar-kw, ar-om".
+ // Final string is sorted by quality (q=) param.
+ function parseAcceptLanguageList(v) {
+ return v.match(/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/gi)
+ .sort(function(a, b) {
+ let qA = parseFloat(a.split(";q=")[1]) || 1.0;
+ let qB = parseFloat(b.split(";q=")[1]) || 1.0;
+ return qB - qA;
+ })
+ .map(a => a.split(";")[0]);
+ }
+
+ // For reference on some of the available IE Registry settings:
+ // * http://msdn.microsoft.com/en-us/library/cc980058%28v=prot.13%29.aspx
+ // * http://msdn.microsoft.com/en-us/library/cc980059%28v=prot.13%29.aspx
+
+ // Note that only settings exposed in our UI should be migrated.
+
+ this._set("Software\\Microsoft\\Internet Explorer\\International",
+ "AcceptLanguage",
+ "intl.accept_languages",
+ parseAcceptLanguageList);
+ // TODO (bug 745853): For now, only x-western font is translated.
+ this._set("Software\\Microsoft\\Internet Explorer\\International\\Scripts\\3",
+ "IEFixedFontName",
+ "font.name.monospace.x-western");
+ this._set(kMainKey,
+ "Use FormSuggest",
+ "browser.formfill.enable",
+ yesNoToBoolean);
+ this._set(kMainKey,
+ "FormSuggest Passwords",
+ "signon.rememberSignons",
+ yesNoToBoolean);
+ this._set(kMainKey,
+ "Anchor Underline",
+ "browser.underline_anchors",
+ yesNoToBoolean);
+ this._set(kMainKey,
+ "Display Inline Images",
+ "permissions.default.image",
+ v => yesNoToBoolean(v) ? 1 : 2);
+ this._set(kMainKey,
+ "Move System Caret",
+ "accessibility.browsewithcaret",
+ yesNoToBoolean);
+ this._set("Software\\Microsoft\\Internet Explorer\\Settings",
+ "Always Use My Colors",
+ "browser.display.document_color_use",
+ v => (!v ? 0 : 2));
+ this._set("Software\\Microsoft\\Internet Explorer\\Settings",
+ "Always Use My Font Face",
+ "browser.display.use_document_fonts",
+ v => !v);
+ this._set(kMainKey,
+ "SmoothScroll",
+ "general.smoothScroll",
+ Boolean);
+ this._set("Software\\Microsoft\\Internet Explorer\\TabbedBrowsing\\",
+ "WarnOnClose",
+ "browser.tabs.warnOnClose",
+ Boolean);
+ this._set("Software\\Microsoft\\Internet Explorer\\TabbedBrowsing\\",
+ "OpenInForeground",
+ "browser.tabs.loadInBackground",
+ v => !v);
+
+ aCallback(true);
+ },
+
+ /**
+ * Reads a setting from the Registry and stores the converted result into
+ * the appropriate Firefox preference.
+ *
+ * @param aPath
+ * Registry path under HKCU.
+ * @param aKey
+ * Name of the key.
+ * @param aPref
+ * Firefox preference.
+ * @param [optional] aTransformFn
+ * Conversion function from the Registry format to the pref format.
+ */
+ _set: function S__set(aPath, aKey, aPref, aTransformFn) {
+ let value = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ aPath, aKey);
+ // Don't import settings that have never been flipped.
+ if (value === undefined)
+ return;
+
+ if (aTransformFn)
+ value = aTransformFn(value);
+
+ switch (typeof value) {
+ case "string":
+ Services.prefs.setCharPref(aPref, value);
+ break;
+ case "number":
+ Services.prefs.setIntPref(aPref, value);
+ break;
+ case "boolean":
+ Services.prefs.setBoolPref(aPref, value);
+ break;
+ default:
+ throw new Error("Unexpected value type: " + (typeof value));
+ }
+ }
+};
+
+function IEProfileMigrator()
+{
+ this.wrappedJSObject = this; // export this to be able to use it in the unittest.
+}
+
+IEProfileMigrator.prototype = Object.create(MigratorPrototype);
+
+IEProfileMigrator.prototype.getResources = function IE_getResources() {
+ let resources = [
+ MSMigrationUtils.getBookmarksMigrator(),
+ new History(),
+ MSMigrationUtils.getCookiesMigrator(),
+ new Settings(),
+ ];
+ // Only support the form password migrator for Windows XP to 7.
+ if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) {
+ resources.push(new IE7FormPasswords());
+ }
+ let windowsVaultFormPasswordsMigrator =
+ MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
+ windowsVaultFormPasswordsMigrator.name = "IEVaultFormPasswords";
+ resources.push(windowsVaultFormPasswordsMigrator);
+ return resources.filter(r => r.exists);
+};
+
+IEProfileMigrator.prototype.getLastUsedDate = function IE_getLastUsedDate() {
+ let datePromises = ["Favs", "CookD"].map(dirId => {
+ let {path} = Services.dirsvc.get(dirId, Ci.nsIFile);
+ return OS.File.stat(path).catch(() => null).then(info => {
+ return info ? info.lastModificationDate : 0;
+ });
+ });
+ datePromises.push(new Promise(resolve => {
+ let typedURLs = new Map();
+ try {
+ typedURLs = MSMigrationUtils.getTypedURLs("Software\\Microsoft\\Internet Explorer");
+ } catch (ex) {}
+ let dates = [0, ... typedURLs.values()];
+ resolve(Math.max.apply(Math, dates));
+ }));
+ return Promise.all(datePromises).then(dates => {
+ return new Date(Math.max.apply(Math, dates));
+ });
+};
+
+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/browser/components/migration/MSMigrationUtils.jsm b/browser/components/migration/MSMigrationUtils.jsm
new file mode 100644
index 000000000..1e0250b06
--- /dev/null
+++ b/browser/components/migration/MSMigrationUtils.jsm
@@ -0,0 +1,889 @@
+/* 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 = ["MSMigrationUtils"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm");
+
+Cu.importGlobalProperties(["FileReader"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+ "resource://gre/modules/ctypes.jsm");
+
+const EDGE_COOKIE_PATH_OPTIONS = ["", "#!001\\", "#!002\\"];
+const EDGE_COOKIES_SUFFIX = "MicrosoftEdge\\Cookies";
+const EDGE_FAVORITES = "AC\\MicrosoftEdge\\User\\Default\\Favorites";
+const FREE_CLOSE_FAILED = 0;
+const INTERNET_EXPLORER_EDGE_GUID = [0x3CCD5499,
+ 0x4B1087A8,
+ 0x886015A2,
+ 0x553BDD88];
+const RESULT_SUCCESS = 0;
+const VAULT_ENUMERATE_ALL_ITEMS = 512;
+const WEB_CREDENTIALS_VAULT_ID = [0x4BF4C442,
+ 0x41A09B8A,
+ 0x4ADD80B3,
+ 0x28DB4D70];
+
+Cu.importGlobalProperties(["File"]);
+
+const wintypes = {
+ BOOL: ctypes.int,
+ DWORD: ctypes.uint32_t,
+ DWORDLONG: ctypes.uint64_t,
+ CHAR: ctypes.char,
+ PCHAR: ctypes.char.ptr,
+ LPCWSTR: ctypes.char16_t.ptr,
+ PDWORD: ctypes.uint32_t.ptr,
+ VOIDP: ctypes.voidptr_t,
+ WORD: ctypes.uint16_t,
+};
+
+// TODO: Bug 1202978 - Refactor MSMigrationUtils ctypes helpers
+function CtypesKernelHelpers() {
+ this._structs = {};
+ this._functions = {};
+ this._libs = {};
+
+ this._structs.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
+ {wYear: wintypes.WORD},
+ {wMonth: wintypes.WORD},
+ {wDayOfWeek: wintypes.WORD},
+ {wDay: wintypes.WORD},
+ {wHour: wintypes.WORD},
+ {wMinute: wintypes.WORD},
+ {wSecond: wintypes.WORD},
+ {wMilliseconds: wintypes.WORD}
+ ]);
+
+ this._structs.FILETIME = new ctypes.StructType("FILETIME", [
+ {dwLowDateTime: wintypes.DWORD},
+ {dwHighDateTime: wintypes.DWORD}
+ ]);
+
+ try {
+ this._libs.kernel32 = ctypes.open("Kernel32");
+
+ this._functions.FileTimeToSystemTime =
+ this._libs.kernel32.declare("FileTimeToSystemTime",
+ ctypes.default_abi,
+ wintypes.BOOL,
+ this._structs.FILETIME.ptr,
+ this._structs.SYSTEMTIME.ptr);
+ } catch (ex) {
+ this.finalize();
+ }
+}
+
+CtypesKernelHelpers.prototype = {
+ /**
+ * Must be invoked once after last use of any of the provided helpers.
+ */
+ finalize() {
+ this._structs = {};
+ this._functions = {};
+ for (let key in this._libs) {
+ let lib = this._libs[key];
+ try {
+ lib.close();
+ } catch (ex) {}
+ }
+ this._libs = {};
+ },
+
+ /**
+ * Converts a FILETIME struct (2 DWORDS), to a SYSTEMTIME struct,
+ * and then deduces the number of seconds since the epoch (which
+ * is the data we want for the cookie expiry date).
+ *
+ * @param aTimeHi
+ * Least significant DWORD.
+ * @param aTimeLo
+ * Most significant DWORD.
+ * @return the number of seconds since the epoch
+ */
+ fileTimeToSecondsSinceEpoch(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);
+
+ // System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
+ // then divide by 1000 to get seconds, and round down:
+ return Math.floor(Date.UTC(systemTime.wYear,
+ systemTime.wMonth - 1,
+ systemTime.wDay,
+ systemTime.wHour,
+ systemTime.wMinute,
+ systemTime.wSecond,
+ systemTime.wMilliseconds) / 1000);
+ }
+};
+
+function CtypesVaultHelpers() {
+ this._structs = {};
+ this._functions = {};
+
+ this._structs.GUID = new ctypes.StructType("GUID", [
+ {id: wintypes.DWORD.array(4)},
+ ]);
+
+ this._structs.VAULT_ITEM_ELEMENT = new ctypes.StructType("VAULT_ITEM_ELEMENT", [
+ // not documented
+ {schemaElementId: wintypes.DWORD},
+ // not documented
+ {unknown1: wintypes.DWORD},
+ // vault type
+ {type: wintypes.DWORD},
+ // not documented
+ {unknown2: wintypes.DWORD},
+ // value of the item
+ {itemValue: wintypes.LPCWSTR},
+ // not documented
+ {unknown3: wintypes.CHAR.array(12)},
+ ]);
+
+ this._structs.VAULT_ELEMENT = new ctypes.StructType("VAULT_ELEMENT", [
+ // vault item schemaId
+ {schemaId: this._structs.GUID},
+ // a pointer to the name of the browser VAULT_ITEM_ELEMENT
+ {pszCredentialFriendlyName: wintypes.LPCWSTR},
+ // a pointer to the url VAULT_ITEM_ELEMENT
+ {pResourceElement: this._structs.VAULT_ITEM_ELEMENT.ptr},
+ // a pointer to the username VAULT_ITEM_ELEMENT
+ {pIdentityElement: this._structs.VAULT_ITEM_ELEMENT.ptr},
+ // not documented
+ {pAuthenticatorElement: this._structs.VAULT_ITEM_ELEMENT.ptr},
+ // not documented
+ {pPackageSid: this._structs.VAULT_ITEM_ELEMENT.ptr},
+ // time stamp in local format
+ {lowLastModified: wintypes.DWORD},
+ {highLastModified: wintypes.DWORD},
+ // not documented
+ {flags: wintypes.DWORD},
+ // not documented
+ {dwPropertiesCount: wintypes.DWORD},
+ // not documented
+ {pPropertyElements: this._structs.VAULT_ITEM_ELEMENT.ptr},
+ ]);
+
+ try {
+ this._vaultcliLib = ctypes.open("vaultcli.dll");
+
+ this._functions.VaultOpenVault =
+ this._vaultcliLib.declare("VaultOpenVault",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ // GUID
+ this._structs.GUID.ptr,
+ // Flags
+ wintypes.DWORD,
+ // Vault Handle
+ wintypes.VOIDP.ptr);
+ this._functions.VaultEnumerateItems =
+ this._vaultcliLib.declare("VaultEnumerateItems",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ // Vault Handle
+ wintypes.VOIDP,
+ // Flags
+ wintypes.DWORD,
+ // Items Count
+ wintypes.PDWORD,
+ // Items
+ ctypes.voidptr_t);
+ this._functions.VaultCloseVault =
+ this._vaultcliLib.declare("VaultCloseVault",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ // Vault Handle
+ wintypes.VOIDP);
+ this._functions.VaultGetItem =
+ this._vaultcliLib.declare("VaultGetItem",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ // Vault Handle
+ wintypes.VOIDP,
+ // Schema Id
+ this._structs.GUID.ptr,
+ // Resource
+ this._structs.VAULT_ITEM_ELEMENT.ptr,
+ // Identity
+ this._structs.VAULT_ITEM_ELEMENT.ptr,
+ // Package Sid
+ this._structs.VAULT_ITEM_ELEMENT.ptr,
+ // HWND Owner
+ wintypes.DWORD,
+ // Flags
+ wintypes.DWORD,
+ // Items
+ this._structs.VAULT_ELEMENT.ptr.ptr);
+ this._functions.VaultFree =
+ this._vaultcliLib.declare("VaultFree",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ // Memory
+ this._structs.VAULT_ELEMENT.ptr);
+ } catch (ex) {
+ this.finalize();
+ }
+}
+
+CtypesVaultHelpers.prototype = {
+ /**
+ * Must be invoked once after last use of any of the provided helpers.
+ */
+ finalize() {
+ this._structs = {};
+ this._functions = {};
+ try {
+ this._vaultcliLib.close();
+ } catch (ex) {}
+ this._vaultcliLib = null;
+ }
+};
+
+/**
+ * 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) {
+ return e.result == Cr.NS_ERROR_HOST_IS_IP_ADDRESS;
+ }
+ return false;
+}
+
+var gEdgeDir;
+function getEdgeLocalDataFolder() {
+ if (gEdgeDir) {
+ return gEdgeDir.clone();
+ }
+ let packages = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
+ packages.append("Packages");
+ let edgeDir = packages.clone();
+ edgeDir.append("Microsoft.MicrosoftEdge_8wekyb3d8bbwe");
+ try {
+ if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
+ gEdgeDir = edgeDir;
+ return edgeDir.clone();
+ }
+
+ // Let's try the long way:
+ let dirEntries = packages.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let subDir = dirEntries.getNext();
+ subDir.QueryInterface(Ci.nsIFile);
+ if (subDir.leafName.startsWith("Microsoft.MicrosoftEdge") && subDir.isReadable() &&
+ subDir.isDirectory()) {
+ gEdgeDir = subDir;
+ return subDir.clone();
+ }
+ }
+ } catch (ex) {
+ Cu.reportError("Exception trying to find the Edge favorites directory: " + ex);
+ }
+ return null;
+}
+
+
+function Bookmarks(migrationType) {
+ this._migrationType = migrationType;
+}
+
+Bookmarks.prototype = {
+ type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+ get exists() {
+ return !!this._favoritesFolder;
+ },
+
+ get importedAppLabel() {
+ return this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE ? "IE" : "Edge";
+ },
+
+ __favoritesFolder: null,
+ get _favoritesFolder() {
+ if (!this.__favoritesFolder) {
+ if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
+ let favoritesFolder = Services.dirsvc.get("Favs", Ci.nsIFile);
+ if (favoritesFolder.exists() && favoritesFolder.isReadable()) {
+ this.__favoritesFolder = favoritesFolder;
+ }
+ } else if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
+ let edgeDir = getEdgeLocalDataFolder();
+ if (edgeDir) {
+ edgeDir.appendRelativePath(EDGE_FAVORITES);
+ if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
+ this.__favoritesFolder = edgeDir;
+ }
+ }
+ }
+ }
+ return this.__favoritesFolder;
+ },
+
+ __toolbarFolderName: null,
+ get _toolbarFolderName() {
+ if (!this.__toolbarFolderName) {
+ if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
+ // 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";
+ } else {
+ this.__toolbarFolderName = "Links";
+ }
+ }
+ return this.__toolbarFolderName;
+ },
+
+ migrate: function B_migrate(aCallback) {
+ return Task.spawn(function* () {
+ // Import to the bookmarks menu.
+ let folderGuid = PlacesUtils.bookmarks.menuGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ folderGuid =
+ yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
+ }
+ yield this._migrateFolder(this._favoritesFolder, folderGuid);
+ }.bind(this)).then(() => aCallback(true),
+ e => { Cu.reportError(e); aCallback(false) });
+ },
+
+ _migrateFolder: Task.async(function* (aSourceFolder, aDestFolderGuid) {
+ // TODO (bug 741993): the favorites order is stored in the Registry, at
+ // HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites
+ // for IE, and in a similar location for Edge.
+ // Until we support it, bookmarks are imported in alphabetical order.
+ let entries = aSourceFolder.directoryEntries;
+ let succeeded = true;
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Ci.nsIFile);
+ try {
+ // Make sure that entry.path == entry.target to not follow .lnk folder
+ // shortcuts which could lead to infinite cycles.
+ // Don't use isSymlink(), since it would throw for invalid
+ // lnk files pointing to URLs or to unresolvable paths.
+ if (entry.path == entry.target && entry.isDirectory()) {
+ let folderGuid;
+ if (entry.leafName == this._toolbarFolderName &&
+ entry.parent.equals(this._favoritesFolder)) {
+ // Import to the bookmarks toolbar.
+ folderGuid = PlacesUtils.bookmarks.toolbarGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ folderGuid =
+ yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
+ }
+ }
+ else {
+ // Import to a new folder.
+ folderGuid = (yield MigrationUtils.insertBookmarkWrapper({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: aDestFolderGuid,
+ title: entry.leafName
+ })).guid;
+ }
+
+ if (entry.isReadable()) {
+ // Recursively import the folder.
+ yield this._migrateFolder(entry, folderGuid);
+ }
+ }
+ 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];
+
+ yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid: aDestFolderGuid, url: uri, title
+ });
+ }
+ }
+ } catch (ex) {
+ Components.utils.reportError("Unable to import " + this.importedAppLabel + " favorite (" + entry.leafName + "): " + ex);
+ succeeded = false;
+ }
+ }
+ if (!succeeded) {
+ throw new Error("Failed to import all bookmarks correctly.");
+ }
+ }),
+
+};
+
+function Cookies(migrationType) {
+ this._migrationType = migrationType;
+}
+
+Cookies.prototype = {
+ type: MigrationUtils.resourceTypes.COOKIES,
+
+ get exists() {
+ if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_IE) {
+ return !!this._cookiesFolder;
+ }
+ return !!this._cookiesFolders;
+ },
+
+ __cookiesFolder: null,
+ get _cookiesFolder() {
+ // Edge stores cookies in a number of places, and this shouldn't get called:
+ if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_IE) {
+ throw new Error("Shouldn't be looking for a single cookie folder unless we're migrating IE");
+ }
+
+ // 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;
+ },
+
+ __cookiesFolders: null,
+ get _cookiesFolders() {
+ if (this._migrationType != MSMigrationUtils.MIGRATION_TYPE_EDGE) {
+ throw new Error("Shouldn't be looking for multiple cookie folders unless we're migrating Edge");
+ }
+
+ let folders = [];
+ let edgeDir = getEdgeLocalDataFolder();
+ if (edgeDir) {
+ edgeDir.append("AC");
+ for (let path of EDGE_COOKIE_PATH_OPTIONS) {
+ let folder = edgeDir.clone();
+ let fullPath = path + EDGE_COOKIES_SUFFIX;
+ folder.appendRelativePath(fullPath);
+ if (folder.exists() && folder.isReadable() && folder.isDirectory()) {
+ folders.push(folder);
+ }
+ }
+ }
+ this.__cookiesFolders = folders.length ? folders : null;
+ return this.__cookiesFolders;
+ },
+
+ migrate(aCallback) {
+ this.ctypesKernelHelpers = new CtypesKernelHelpers();
+
+ let cookiesGenerator = (function* genCookie() {
+ let success = false;
+ let folders = this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE ?
+ this.__cookiesFolders : [this.__cookiesFolder];
+ for (let folder of folders) {
+ let entries = folder.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 undefined;
+ }
+ }
+
+ this.ctypesKernelHelpers.finalize();
+
+ aCallback(success);
+ }).apply(this);
+ cookiesGenerator.next();
+ },
+
+ _readCookieFile(aFile, aCallback) {
+ let fileReader = new FileReader();
+ let 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);
+ }
+ };
+ fileReader.addEventListener("loadend", onLoadEnd, false);
+ fileReader.readAsText(File.createFromNsIFile(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 "*"
+ *
+ * Unfortunately, "*" can also occur inside the value of the cookie, so we
+ * can't rely exclusively on it as a record separator.
+ *
+ * @note All the times are in FILETIME format.
+ */
+ _parseCookieBuffer(aTextBuffer) {
+ // Note the last record is an empty string...
+ let records = [];
+ let lines = aTextBuffer.split("\n");
+ while (lines.length > 0) {
+ let record = lines.splice(0, 9);
+ // ... which means this is going to be a 1-element array for that record
+ if (record.length > 1) {
+ records.push(record);
+ }
+ }
+ for (let record of records) {
+ let [name, value, hostpath, flags,
+ expireTimeLo, expireTimeHi] = record;
+
+ // IE stores deleted cookies with a zero-length value, skip them.
+ if (value.length == 0)
+ continue;
+
+ // IE sometimes has cookies created by apps that use "~~local~~/local/file/path"
+ // as the hostpath, ignore those:
+ if (hostpath.startsWith("~~local~~"))
+ 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;
+ }
+
+ // Fallback: expire in 1h (NB: time is in seconds since epoch, so we have
+ // to divide the result of Date.now() (which is in milliseconds) by 1000).
+ let expireTime = Math.floor(Date.now() / 1000) + 3600;
+ try {
+ expireTime = this.ctypesKernelHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
+ Number(expireTimeLo));
+ } catch (ex) {
+ Cu.reportError("Failed to get expiry time for cookie for " + host);
+ }
+
+ Services.cookies.add(host,
+ path,
+ name,
+ value,
+ Number(flags) & 0x1, // secure
+ false, // httpOnly
+ false, // session
+ expireTime,
+ {});
+ }
+ }
+};
+
+function getTypedURLs(registryKeyPath) {
+ // The list of typed URLs is a sort of annotation stored in the registry.
+ // The number of entries stored is not UI-configurable, but has changed
+ // between different Windows versions. We just keep reading up to the first
+ // non-existing entry to support different limits / states of the registry.
+ let typedURLs = new Map();
+ let typedURLKey = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(Ci.nsIWindowsRegKey);
+ let typedURLTimeKey = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(Ci.nsIWindowsRegKey);
+ let cTypes = new CtypesKernelHelpers();
+ try {
+ typedURLKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ registryKeyPath + "\\TypedURLs",
+ Ci.nsIWindowsRegKey.ACCESS_READ);
+ try {
+ typedURLTimeKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ registryKeyPath + "\\TypedURLsTime",
+ Ci.nsIWindowsRegKey.ACCESS_READ);
+ } catch (ex) {
+ typedURLTimeKey = null;
+ }
+ let entryName;
+ for (let entry = 1; typedURLKey.hasValue((entryName = "url" + entry)); entry++) {
+ let url = typedURLKey.readStringValue(entryName);
+ let timeTyped = 0;
+ if (typedURLTimeKey && typedURLTimeKey.hasValue(entryName)) {
+ let urlTime = "";
+ try {
+ urlTime = typedURLTimeKey.readBinaryValue(entryName);
+ } catch (ex) {
+ Cu.reportError("Couldn't read url time for " + entryName);
+ }
+ if (urlTime.length == 8) {
+ let urlTimeHex = [];
+ for (let i = 0; i < 8; i++) {
+ let c = urlTime.charCodeAt(i).toString(16);
+ if (c.length == 1)
+ c = "0" + c;
+ urlTimeHex.unshift(c);
+ }
+ try {
+ let hi = parseInt(urlTimeHex.slice(0, 4).join(""), 16);
+ let lo = parseInt(urlTimeHex.slice(4, 8).join(""), 16);
+ // Convert to seconds since epoch:
+ timeTyped = cTypes.fileTimeToSecondsSinceEpoch(hi, lo);
+ // Callers expect PRTime, which is microseconds since epoch:
+ timeTyped *= 1000 * 1000;
+ } catch (ex) {
+ // Ignore conversion exceptions. Callers will have to deal
+ // with the fallback value (0).
+ }
+ }
+ }
+ typedURLs.set(url, timeTyped);
+ }
+ } catch (ex) {
+ Cu.reportError("Error reading typed URL history: " + ex);
+ } finally {
+ if (typedURLKey) {
+ typedURLKey.close();
+ }
+ if (typedURLTimeKey) {
+ typedURLTimeKey.close();
+ }
+ cTypes.finalize();
+ }
+ return typedURLs;
+}
+
+
+// Migrator for form passwords on Windows 8 and higher.
+function WindowsVaultFormPasswords () {
+}
+
+WindowsVaultFormPasswords.prototype = {
+ type: MigrationUtils.resourceTypes.PASSWORDS,
+
+ get exists() {
+ // work only on windows 8+
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ // check if there are passwords available for migration.
+ return this.migrate(() => {}, true);
+ }
+ return false;
+ },
+
+ /**
+ * If aOnlyCheckExists is false, import the form passwords on Windows 8 and higher from the vault
+ * and then call the aCallback.
+ * Otherwise, check if there are passwords in the vault.
+ * @param {function} aCallback - a callback called when the migration is done.
+ * @param {boolean} [aOnlyCheckExists=false] - if aOnlyCheckExists is true, just check if there are some
+ * passwords to migrate. Import the passwords from the vault and call aCallback otherwise.
+ * @return true if there are passwords in the vault and aOnlyCheckExists is set to true,
+ * false if there is no password in the vault and aOnlyCheckExists is set to true, undefined if
+ * aOnlyCheckExists is set to false.
+ */
+ migrate(aCallback, aOnlyCheckExists = false) {
+ // check if the vault item is an IE/Edge one
+ function _isIEOrEdgePassword(id) {
+ return id[0] == INTERNET_EXPLORER_EDGE_GUID[0] &&
+ id[1] == INTERNET_EXPLORER_EDGE_GUID[1] &&
+ id[2] == INTERNET_EXPLORER_EDGE_GUID[2] &&
+ id[3] == INTERNET_EXPLORER_EDGE_GUID[3];
+ }
+
+ let ctypesVaultHelpers = new CtypesVaultHelpers();
+ let ctypesKernelHelpers = new CtypesKernelHelpers();
+ let migrationSucceeded = true;
+ let successfulVaultOpen = false;
+ let error, vault;
+ try {
+ // web credentials vault id
+ let vaultGuid = new ctypesVaultHelpers._structs.GUID(WEB_CREDENTIALS_VAULT_ID);
+ error = new wintypes.DWORD();
+ // web credentials vault
+ vault = new wintypes.VOIDP();
+ // open the current vault using the vaultGuid
+ error = ctypesVaultHelpers._functions.VaultOpenVault(vaultGuid.address(), 0, vault.address());
+ if (error != RESULT_SUCCESS) {
+ throw new Error("Unable to open Vault: " + error);
+ }
+ successfulVaultOpen = true;
+
+ let item = new ctypesVaultHelpers._structs.VAULT_ELEMENT.ptr();
+ let itemCount = new wintypes.DWORD();
+ // enumerate all the available items. This api is going to return a table of all the
+ // available items and item is going to point to the first element of this table.
+ error = ctypesVaultHelpers._functions.VaultEnumerateItems(vault, VAULT_ENUMERATE_ALL_ITEMS,
+ itemCount.address(),
+ item.address());
+ if (error != RESULT_SUCCESS) {
+ throw new Error("Unable to enumerate Vault items: " + error);
+ }
+ for (let j = 0; j < itemCount.value; j++) {
+ try {
+ // if it's not an ie/edge password, skip it
+ if (!_isIEOrEdgePassword(item.contents.schemaId.id)) {
+ continue;
+ }
+ let url = item.contents.pResourceElement.contents.itemValue.readString();
+ let realURL;
+ try {
+ realURL = Services.io.newURI(url, null, null);
+ } catch (ex) { /* leave realURL as null */ }
+ if (!realURL || ["http", "https", "ftp"].indexOf(realURL.scheme) == -1) {
+ // Ignore items for non-URLs or URLs that aren't HTTP(S)/FTP
+ continue;
+ }
+
+ // if aOnlyCheckExists is set to true, the purpose of the call is to return true if there is at
+ // least a password which is true in this case because a password was by now already found
+ if (aOnlyCheckExists) {
+ return true;
+ }
+ let username = item.contents.pIdentityElement.contents.itemValue.readString();
+ // the current login credential object
+ let credential = new ctypesVaultHelpers._structs.VAULT_ELEMENT.ptr();
+ error = ctypesVaultHelpers._functions.VaultGetItem(vault,
+ item.contents.schemaId.address(),
+ item.contents.pResourceElement,
+ item.contents.pIdentityElement, null,
+ 0, 0, credential.address());
+ if (error != RESULT_SUCCESS) {
+ throw new Error("Unable to get item: " + error);
+ }
+
+ let password = credential.contents.pAuthenticatorElement.contents.itemValue.readString();
+ let creation = Date.now();
+ try {
+ // login manager wants time in milliseconds since epoch, so convert
+ // to seconds since epoch and multiply to get milliseconds:
+ creation = ctypesKernelHelpers.
+ fileTimeToSecondsSinceEpoch(item.contents.highLastModified,
+ item.contents.lowLastModified) * 1000;
+ } catch (ex) {
+ // Ignore exceptions in the dates and just create the login for right now.
+ }
+ // create a new login
+ let login = {
+ username, password,
+ hostname: realURL.prePath,
+ timeCreated: creation,
+ };
+ MigrationUtils.insertLoginWrapper(login);
+
+ // close current item
+ error = ctypesVaultHelpers._functions.VaultFree(credential);
+ if (error == FREE_CLOSE_FAILED) {
+ throw new Error("Unable to free item: " + error);
+ }
+ } catch (e) {
+ migrationSucceeded = false;
+ Cu.reportError(e);
+ } finally {
+ // move to next item in the table returned by VaultEnumerateItems
+ item = item.increment();
+ }
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ migrationSucceeded = false;
+ } finally {
+ if (successfulVaultOpen) {
+ // close current vault
+ error = ctypesVaultHelpers._functions.VaultCloseVault(vault);
+ if (error == FREE_CLOSE_FAILED) {
+ Cu.reportError("Unable to close vault: " + error);
+ }
+ }
+ ctypesKernelHelpers.finalize();
+ ctypesVaultHelpers.finalize();
+ aCallback(migrationSucceeded);
+ }
+ if (aOnlyCheckExists) {
+ return false;
+ }
+ return undefined;
+ }
+};
+
+var MSMigrationUtils = {
+ MIGRATION_TYPE_IE: 1,
+ MIGRATION_TYPE_EDGE: 2,
+ CtypesKernelHelpers: CtypesKernelHelpers,
+ getBookmarksMigrator(migrationType = this.MIGRATION_TYPE_IE) {
+ return new Bookmarks(migrationType);
+ },
+ getCookiesMigrator(migrationType = this.MIGRATION_TYPE_IE) {
+ return new Cookies(migrationType);
+ },
+ getWindowsVaultFormPasswordsMigrator() {
+ return new WindowsVaultFormPasswords();
+ },
+ getTypedURLs,
+ getEdgeLocalDataFolder,
+};
diff --git a/browser/components/migration/MigrationUtils.jsm b/browser/components/migration/MigrationUtils.jsm
new file mode 100644
index 000000000..104efe005
--- /dev/null
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -0,0 +1,1117 @@
+/* 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 { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+const TOPIC_WILL_IMPORT_BOOKMARKS = "initial-migration-will-import-default-bookmarks";
+const TOPIC_DID_IMPORT_BOOKMARKS = "initial-migration-did-import-default-bookmarks";
+const TOPIC_PLACES_DEFAULTS_FINISHED = "places-browser-init-complete";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.importGlobalProperties(["URL"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate",
+ "resource:///modules/AutoMigrate.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
+ "resource://gre/modules/BookmarkHTMLUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+ "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
+ "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ResponsivenessMonitor",
+ "resource://gre/modules/ResponsivenessMonitor.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+ "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm");
+
+var gMigrators = null;
+var gProfileStartup = null;
+var gMigrationBundle = null;
+var gPreviousDefaultBrowserKey = "";
+
+let gKeepUndoData = false;
+let gUndoData = null;
+
+XPCOMUtils.defineLazyGetter(this, "gAvailableMigratorKeys", function() {
+ if (AppConstants.platform == "win") {
+ return [
+ "firefox", "edge", "ie", "chrome", "chromium", "360se",
+ "canary"
+ ];
+ }
+ if (AppConstants.platform == "macosx") {
+ return ["firefox", "safari", "chrome", "chromium", "canary"];
+ }
+ if (AppConstants.XP_UNIX) {
+ return ["firefox", "chrome", "chromium"];
+ }
+ return [];
+});
+
+function getMigrationBundle() {
+ if (!gMigrationBundle) {
+ gMigrationBundle = Services.strings.createBundle(
+ "chrome://browser/locale/migration/migration.properties");
+ }
+ return gMigrationBundle;
+}
+
+/**
+ * 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() {
+ return 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, returning 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 in order to provide an estimate of when the last time was
+ * that somebody used the browser. It is OK that this is somewhat fuzzy -
+ * history may not be available (or be wiped or not present due to e.g.
+ * incognito mode).
+ *
+ * @return a Promise that resolves to the last used date.
+ *
+ * @note If not overridden, the promise will resolve to the unix epoch.
+ */
+ getLastUsedDate() {
+ return Promise.resolve(new Date(0));
+ },
+
+ /**
+ * 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() {
+ return false;
+ },
+
+ /**
+ * OVERRIDE IF AND ONLY IF your migrator supports importing the homepage.
+ * @see nsIBrowserProfileMigrator
+ */
+ get sourceHomePageURL() {
+ return "";
+ },
+
+ /**
+ * Override if the data to migrate is locked/in-use and the user should
+ * probably shutdown the source browser.
+ */
+ get sourceLocked() {
+ return false;
+ },
+
+ /**
+ * DO NOT OVERRIDE - After deCOMing migration, the UI will just call
+ * getResources.
+ *
+ * @see nsIBrowserProfileMigrator
+ */
+ getMigrateData: function MP_getMigrateData(aProfile) {
+ let resources = this._getMaybeCachedResources(aProfile);
+ if (!resources) {
+ return [];
+ }
+ let types = resources.map(r => r.type);
+ return types.reduce((a, b) => { a |= b; return a }, 0);
+ },
+
+ getBrowserKey: function MP_getBrowserKey() {
+ return this.contractID.match(/\=([^\=]+)$/)[1];
+ },
+
+ /**
+ * 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 = resources.filter(r => aItems & r.type);
+
+ // Used to periodically give back control to the main-thread loop.
+ let unblockMainThread = function() {
+ return new Promise(resolve => {
+ Services.tm.mainThread.dispatch(resolve, Ci.nsIThread.DISPATCH_NORMAL);
+ });
+ };
+
+ let getHistogramIdForResourceType = (resourceType, template) => {
+ if (resourceType == MigrationUtils.resourceTypes.HISTORY) {
+ return template.replace("*", "HISTORY");
+ }
+ if (resourceType == MigrationUtils.resourceTypes.BOOKMARKS) {
+ return template.replace("*", "BOOKMARKS");
+ }
+ if (resourceType == MigrationUtils.resourceTypes.PASSWORDS) {
+ return template.replace("*", "LOGINS");
+ }
+ return null;
+ };
+
+ let browserKey = this.getBrowserKey();
+
+ let maybeStartTelemetryStopwatch = resourceType => {
+ let histogramId = getHistogramIdForResourceType(resourceType, "FX_MIGRATION_*_IMPORT_MS");
+ if (histogramId) {
+ TelemetryStopwatch.startKeyed(histogramId, browserKey);
+ }
+ return histogramId;
+ };
+
+ let maybeStartResponsivenessMonitor = resourceType => {
+ let responsivenessMonitor;
+ let responsivenessHistogramId =
+ getHistogramIdForResourceType(resourceType, "FX_MIGRATION_*_JANK_MS");
+ if (responsivenessHistogramId) {
+ responsivenessMonitor = new ResponsivenessMonitor();
+ }
+ return {responsivenessMonitor, responsivenessHistogramId};
+ };
+
+ let maybeFinishResponsivenessMonitor = (responsivenessMonitor, histogramId) => {
+ if (responsivenessMonitor) {
+ let accumulatedDelay = responsivenessMonitor.finish();
+ if (histogramId) {
+ try {
+ Services.telemetry.getKeyedHistogramById(histogramId)
+ .add(browserKey, accumulatedDelay);
+ } catch (ex) {
+ Cu.reportError(histogramId + ": " + ex);
+ }
+ }
+ }
+ };
+
+ let collectQuantityTelemetry = () => {
+ for (let resourceType of Object.keys(MigrationUtils._importQuantities)) {
+ let histogramId =
+ "FX_MIGRATION_" + resourceType.toUpperCase() + "_QUANTITY";
+ try {
+ Services.telemetry.getKeyedHistogramById(histogramId)
+ .add(browserKey, MigrationUtils._importQuantities[resourceType]);
+ } catch (ex) {
+ Cu.reportError(histogramId + ": " + ex);
+ }
+ }
+ };
+
+ // Called either directly or through the bookmarks import callback.
+ let doMigrate = Task.async(function*() {
+ let resourcesGroupedByItems = new Map();
+ resources.forEach(function(resource) {
+ if (!resourcesGroupedByItems.has(resource.type)) {
+ resourcesGroupedByItems.set(resource.type, new Set());
+ }
+ resourcesGroupedByItems.get(resource.type).add(resource);
+ });
+
+ if (resourcesGroupedByItems.size == 0)
+ throw new Error("No items to import");
+
+ let notify = function(aMsg, aItemType) {
+ Services.obs.notifyObservers(null, aMsg, aItemType);
+ };
+
+ for (let resourceType of Object.keys(MigrationUtils._importQuantities)) {
+ MigrationUtils._importQuantities[resourceType] = 0;
+ }
+ notify("Migration:Started");
+ for (let [migrationType, itemResources] of resourcesGroupedByItems) {
+ notify("Migration:ItemBeforeMigrate", migrationType);
+
+ let stopwatchHistogramId = maybeStartTelemetryStopwatch(migrationType);
+
+ let {responsivenessMonitor, responsivenessHistogramId} =
+ maybeStartResponsivenessMonitor(migrationType);
+
+ let itemSuccess = false;
+ for (let res of itemResources) {
+ let completeDeferred = PromiseUtils.defer();
+ let resourceDone = function(aSuccess) {
+ itemResources.delete(res);
+ itemSuccess |= aSuccess;
+ if (itemResources.size == 0) {
+ notify(itemSuccess ?
+ "Migration:ItemAfterMigrate" : "Migration:ItemError",
+ migrationType);
+ resourcesGroupedByItems.delete(migrationType);
+
+ if (stopwatchHistogramId) {
+ TelemetryStopwatch.finishKeyed(stopwatchHistogramId, browserKey);
+ }
+
+ maybeFinishResponsivenessMonitor(responsivenessMonitor, responsivenessHistogramId);
+
+ if (resourcesGroupedByItems.size == 0) {
+ collectQuantityTelemetry();
+ notify("Migration:Ended");
+ }
+ }
+ completeDeferred.resolve();
+ };
+
+ // If migrate throws, an error occurred, and the callback
+ // (itemMayBeDone) might haven't been called.
+ try {
+ res.migrate(resourceDone);
+ } catch (ex) {
+ Cu.reportError(ex);
+ resourceDone(false);
+ }
+
+ // Certain resources must be ran sequentially or they could fail,
+ // for example bookmarks and history (See bug 1272652).
+ if (migrationType == MigrationUtils.resourceTypes.BOOKMARKS ||
+ migrationType == MigrationUtils.resourceTypes.HISTORY) {
+ yield completeDeferred.promise;
+ }
+
+ yield unblockMainThread();
+ }
+ }
+ });
+
+ if (MigrationUtils.isStartupMigration && !this.startupOnlyMigrator) {
+ MigrationUtils.profileStartup.doStartup();
+ // 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.
+ Task.spawn(function* () {
+ // Tell nsBrowserGlue we're importing default bookmarks.
+ let browserGlue = Cc["@mozilla.org/browser/browserglue;1"].
+ getService(Ci.nsIObserver);
+ browserGlue.observe(null, TOPIC_WILL_IMPORT_BOOKMARKS, "");
+
+ // Import the default bookmarks. We ignore whether or not we succeed.
+ yield BookmarkHTMLUtils.importFromURL(
+ "chrome://browser/locale/bookmarks.html", true).catch(r => r);
+
+ // We'll tell nsBrowserGlue we've imported bookmarks, but before that
+ // we need to make sure we're going to know when it's finished
+ // initializing places:
+ let placesInitedPromise = new Promise(resolve => {
+ let onPlacesInited = function() {
+ Services.obs.removeObserver(onPlacesInited, TOPIC_PLACES_DEFAULTS_FINISHED);
+ resolve();
+ };
+ Services.obs.addObserver(onPlacesInited, TOPIC_PLACES_DEFAULTS_FINISHED, false);
+ });
+ browserGlue.observe(null, TOPIC_DID_IMPORT_BOOKMARKS, "");
+ yield placesInitedPromise;
+ doMigrate();
+ });
+ 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 = { };
+ }
+ this._resourcesByProfile[profileKey] = this.getResources(aProfile);
+ return this._resourcesByProfile[profileKey];
+ }
+};
+
+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,
+ SESSION: Ci.nsIBrowserProfileMigrator.SESSION,
+ },
+
+ /**
+ * 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 aReplacements
+ * [optioanl] Array of replacements to run on the retrieved string.
+ * @return the retrieved string.
+ *
+ * @see nsIStringBundle
+ */
+ getLocalizedString: function MU_getLocalizedString(aKey, aReplacements) {
+ aKey = aKey.replace(/_(canary|chromium)$/, "_chrome");
+
+ 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);
+ },
+
+ _getLocalePropertyForBrowser(browserId) {
+ switch (browserId) {
+ case "edge":
+ return "sourceNameEdge";
+ case "ie":
+ return "sourceNameIE";
+ case "safari":
+ return "sourceNameSafari";
+ case "canary":
+ return "sourceNameCanary";
+ case "chrome":
+ return "sourceNameChrome";
+ case "chromium":
+ return "sourceNameChromium";
+ case "firefox":
+ return "sourceNameFirefox";
+ case "360se":
+ return "sourceName360se";
+ }
+ return null;
+ },
+
+ getBrowserName(browserId) {
+ let prop = this._getLocalePropertyForBrowser(browserId);
+ if (prop) {
+ return this.getLocalizedString(prop);
+ }
+ return null;
+ },
+
+ /**
+ * 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 sourceNameStr
+ * 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 parentGuid
+ * the GUID of the folder in which the new folder should be created.
+ * @return the GUID of the new folder.
+ */
+ createImportedBookmarksFolder: Task.async(function* (sourceNameStr, parentGuid) {
+ let source = this.getLocalizedString("sourceName" + sourceNameStr);
+ let title = this.getLocalizedString("importedBookmarksFolder", [source]);
+ return (yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title
+ })).guid;
+ }),
+
+ /**
+ * Get all the rows corresponding to a select query from a database, without
+ * requiring a lock on the database. If fetching data fails (because someone
+ * else tried to write to the DB at the same time, for example), we will
+ * retry the fetch after a 100ms timeout, up to 10 times.
+ *
+ * @param path
+ * the file path to the database we want to open.
+ * @param description
+ * a developer-readable string identifying what kind of database we're
+ * trying to open.
+ * @param selectQuery
+ * the SELECT query to use to fetch the rows.
+ *
+ * @return a promise that resolves to an array of rows. The promise will be
+ * rejected if the read/fetch failed even after retrying.
+ */
+ getRowsFromDBWithoutLocks(path, description, selectQuery) {
+ let dbOptions = {
+ readOnly: true,
+ ignoreLockingMode: true,
+ path,
+ };
+
+ const RETRYLIMIT = 10;
+ const RETRYINTERVAL = 100;
+ return Task.spawn(function* innerGetRows() {
+ let rows = null;
+ for (let retryCount = RETRYLIMIT; retryCount && !rows; retryCount--) {
+ // Attempt to get the rows. If this succeeds, we will bail out of the loop,
+ // close the database in a failsafe way, and pass the rows back.
+ // If fetching the rows throws, we will wait RETRYINTERVAL ms
+ // and try again. This will repeat a maximum of RETRYLIMIT times.
+ let db;
+ let didOpen = false;
+ let exceptionSeen;
+ try {
+ db = yield Sqlite.openConnection(dbOptions);
+ didOpen = true;
+ rows = yield db.execute(selectQuery);
+ } catch (ex) {
+ if (!exceptionSeen) {
+ Cu.reportError(ex);
+ }
+ exceptionSeen = ex;
+ } finally {
+ try {
+ if (didOpen) {
+ yield db.close();
+ }
+ } catch (ex) {}
+ }
+ if (exceptionSeen) {
+ yield new Promise(resolve => setTimeout(resolve, RETRYINTERVAL));
+ }
+ }
+ if (!rows) {
+ throw new Error("Couldn't get rows from the " + description + " database.");
+ }
+ return rows;
+ });
+ },
+
+ get _migrators() {
+ if (!gMigrators) {
+ gMigrators = new Map();
+ }
+ return gMigrators;
+ },
+
+ /*
+ * 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),
+ * edge (windows),
+ * safari (mac),
+ * canary (mac/windows),
+ * chrome (mac/windows/linux),
+ * chromium (mac/windows/linux),
+ * 360se (windows),
+ * 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) { Cu.reportError(ex) }
+ this._migrators.set(aKey, migrator);
+ }
+
+ try {
+ return migrator && migrator.sourceExists ? migrator : null;
+ } catch (ex) { Cu.reportError(ex); return null }
+ },
+
+ /**
+ * 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 migrator 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.
+ */
+ getMigratorKeyForDefaultBrowser() {
+ // Canary uses the same description as Chrome so we can't distinguish them.
+ const APP_DESC_TO_KEY = {
+ "Internet Explorer": "ie",
+ "Microsoft Edge": "edge",
+ "Safari": "safari",
+ "Firefox": "firefox",
+ "Nightly": "firefox",
+ "Google Chrome": "chrome", // Windows, Linux
+ "Chrome": "chrome", // OS X
+ "Chromium": "chromium", // Windows, OS X
+ "Chromium Web Browser": "chromium", // Linux
+ "360\u5b89\u5168\u6d4f\u89c8\u5668": "360se",
+ };
+
+ let key = "";
+ try {
+ let browserDesc =
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .getApplicationDescription("http");
+ key = APP_DESC_TO_KEY[browserDesc] || "";
+ // Handle devedition, as well as "FirefoxNightly" on OS X.
+ if (!key && browserDesc.startsWith("Firefox")) {
+ key = "firefox";
+ }
+ }
+ catch (ex) {
+ Cu.reportError("Could not detect default browser: " + ex);
+ }
+
+ // "firefox" is the least useful entry here, and might just be because we've set
+ // ourselves as the default (on Windows 7 and below). In that case, check if we
+ // have a registry key that tells us where to go:
+ if (key == "firefox" && AppConstants.isPlatformAndVersionAtMost("win", "6.2")) {
+ // Because we remove the registry key, reading the registry key only works once.
+ // We save the value for subsequent calls to avoid hard-to-trace bugs when multiple
+ // consumers ask for this key.
+ if (gPreviousDefaultBrowserKey) {
+ key = gPreviousDefaultBrowserKey;
+ } else {
+ // We didn't have a saved value, so check the registry.
+ const kRegPath = "Software\\Mozilla\\Firefox";
+ let oldDefault = WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kRegPath, "OldDefaultBrowserCommand");
+ if (oldDefault) {
+ // Remove the key:
+ WindowsRegistry.removeRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kRegPath, "OldDefaultBrowserCommand");
+ try {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFileWin);
+ file.initWithCommandLine(oldDefault);
+ key = APP_DESC_TO_KEY[file.getVersionInfoField("FileDescription")] || key;
+ // Save the value for future callers.
+ gPreviousDefaultBrowserKey = key;
+ } catch (ex) {
+ Cu.reportError("Could not convert old default browser value to description.");
+ }
+ }
+ }
+ }
+ return key;
+ },
+
+ // Whether or not we're in the process of startup migration
+ get isStartupMigration() {
+ return gProfileStartup != null;
+ },
+
+ /**
+ * In the case of startup migration, this is set to the nsIProfileStartup
+ * instance passed to ProfileMigrator's migrate.
+ *
+ * @see showMigrationWizard
+ */
+ get profileStartup() {
+ return 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 {Window} [aOpener]
+ * optional; the window that asks to open the wizard.
+ * @param {Array} [aParams]
+ * optional arguments for the migration wizard, in the form of an array
+ * This is passed as-is for the params argument of
+ * nsIWindowWatcher.openWindow. The array elements we expect are, in
+ * order:
+ * - {Number} migration entry point constant (see below)
+ * - {String} source browser identifier
+ * - {nsIBrowserProfileMigrator} actual migrator object
+ * - {Boolean} whether this is a startup migration
+ * - {Boolean} whether to skip the 'source' page
+ * - {String} an identifier for the profile to use when migrating
+ * NB: If you add new consumers, please add a migration entry point
+ * constant below, and specify at least the first element of the array
+ * (the migration entry point for purposes of telemetry).
+ */
+ showMigrationWizard:
+ function MU_showMigrationWizard(aOpener, aParams) {
+ let features = "chrome,dialog,modal,centerscreen,titlebar,resizable=no";
+ if (AppConstants.platform == "macosx" && !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";
+ }
+
+ // nsIWindowWatcher doesn't deal with raw arrays, so we convert the input
+ let params;
+ if (Array.isArray(aParams)) {
+ params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ for (let item of aParams) {
+ let comtaminatedVal;
+ if (item && item instanceof Ci.nsISupports) {
+ comtaminatedVal = item;
+ } else {
+ switch (typeof item) {
+ case "boolean":
+ comtaminatedVal = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ comtaminatedVal.data = item;
+ break;
+ case "number":
+ comtaminatedVal = Cc["@mozilla.org/supports-PRUint32;1"].
+ createInstance(Ci.nsISupportsPRUint32);
+ comtaminatedVal.data = item;
+ break;
+ case "string":
+ comtaminatedVal = Cc["@mozilla.org/supports-cstring;1"].
+ createInstance(Ci.nsISupportsCString);
+ comtaminatedVal.data = item;
+ break;
+
+ case "undefined":
+ case "object":
+ if (!item) {
+ comtaminatedVal = null;
+ break;
+ }
+ /* intentionally falling through to error out here for
+ non-null/undefined things: */
+ default:
+ throw new Error("Unexpected parameter type " + (typeof item) + ": " + item);
+ }
+ }
+ params.appendElement(comtaminatedVal, false);
+ }
+ } else {
+ params = aParams;
+ }
+
+ Services.ww.openWindow(aOpener,
+ "chrome://browser/content/migration/migration.xul",
+ "_blank",
+ features,
+ params);
+ },
+
+ /**
+ * 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("an 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 = this.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.
+ // Note that we don't need to check the default browser first, because
+ // if that one existed we would have used it in the block above this one.
+ if (!gAvailableMigratorKeys.some(key => !!this.getMigrator(key))) {
+ // None of the keys produced a usable migrator, so finish up here:
+ this.finishMigration();
+ return;
+ }
+ }
+
+ let isRefresh = migrator && skipSourcePage &&
+ migratorKey == AppConstants.MOZ_APP_NAME;
+
+ if (!isRefresh && AutoMigrate.enabled) {
+ try {
+ AutoMigrate.migrate(aProfileStartup, migratorKey, aProfileToMigrate);
+ return;
+ } catch (ex) {
+ // If automigration failed, continue and show the dialog.
+ Cu.reportError(ex);
+ }
+ }
+
+ let migrationEntryPoint = this.MIGRATION_ENTRYPOINT_FIRSTRUN;
+ if (isRefresh) {
+ migrationEntryPoint = this.MIGRATION_ENTRYPOINT_FXREFRESH;
+ }
+
+ let params = [
+ migrationEntryPoint,
+ migratorKey,
+ migrator,
+ aProfileStartup,
+ skipSourcePage,
+ aProfileToMigrate,
+ ];
+ this.showMigrationWizard(null, params);
+ },
+
+ _importQuantities: {
+ bookmarks: 0,
+ logins: 0,
+ history: 0,
+ },
+
+ insertBookmarkWrapper(bookmark) {
+ this._importQuantities.bookmarks++;
+ let insertionPromise = PlacesUtils.bookmarks.insert(bookmark);
+ if (!gKeepUndoData) {
+ return insertionPromise;
+ }
+ // If we keep undo data, add a promise handler that stores the undo data once
+ // the bookmark has been inserted in the DB, and then returns the bookmark.
+ let {parentGuid} = bookmark;
+ return insertionPromise.then(bm => {
+ let {guid, lastModified, type} = bm;
+ gUndoData.get("bookmarks").push({
+ parentGuid, guid, lastModified, type
+ });
+ return bm;
+ });
+ },
+
+ insertVisitsWrapper(places, options) {
+ this._importQuantities.history += places.length;
+ if (gKeepUndoData) {
+ this._updateHistoryUndo(places);
+ }
+ return PlacesUtils.asyncHistory.updatePlaces(places, options);
+ },
+
+ insertLoginWrapper(login) {
+ this._importQuantities.logins++;
+ let insertedLogin = LoginHelper.maybeImportLogin(login);
+ // Note that this means that if we import a login that has a newer password
+ // than we know about, we will update the login, and an undo of the import
+ // will not revert this. This seems preferable over removing the login
+ // outright or storing the old password in the undo file.
+ if (insertedLogin && gKeepUndoData) {
+ let {guid, timePasswordChanged} = insertedLogin;
+ gUndoData.get("logins").push({guid, timePasswordChanged});
+ }
+ },
+
+ initializeUndoData() {
+ gKeepUndoData = true;
+ gUndoData = new Map([["bookmarks", []], ["visits", []], ["logins", []]]);
+ },
+
+ _postProcessUndoData: Task.async(function*(state) {
+ if (!state) {
+ return state;
+ }
+ let bookmarkFolders = state.get("bookmarks").filter(b => b.type == PlacesUtils.bookmarks.TYPE_FOLDER);
+
+ let bookmarkFolderData = [];
+ let bmPromises = bookmarkFolders.map(({guid}) => {
+ // Ignore bookmarks where the promise doesn't resolve (ie that are missing)
+ // Also check that the bookmark fetch returns isn't null before adding it.
+ return PlacesUtils.bookmarks.fetch(guid).then(bm => bm && bookmarkFolderData.push(bm), () => {});
+ });
+
+ yield Promise.all(bmPromises);
+ let folderLMMap = new Map(bookmarkFolderData.map(b => [b.guid, b.lastModified]));
+ for (let bookmark of bookmarkFolders) {
+ let lastModified = folderLMMap.get(bookmark.guid);
+ // If the bookmark was deleted, the map will be returning null, so check:
+ if (lastModified) {
+ bookmark.lastModified = lastModified;
+ }
+ }
+ return state;
+ }),
+
+ stopAndRetrieveUndoData() {
+ let undoData = gUndoData;
+ gUndoData = null;
+ gKeepUndoData = false;
+ return this._postProcessUndoData(undoData);
+ },
+
+ _updateHistoryUndo(places) {
+ let visits = gUndoData.get("visits");
+ let visitMap = new Map(visits.map(v => [v.url, v]));
+ for (let place of places) {
+ let visitCount = place.visits.length;
+ let first = Math.min.apply(Math, place.visits.map(v => v.visitDate));
+ let last = Math.max.apply(Math, place.visits.map(v => v.visitDate));
+ let url = place.uri.spec;
+ try {
+ new URL(url);
+ } catch (ex) {
+ // This won't save and we won't need to 'undo' it, so ignore this URL.
+ continue;
+ }
+ if (!visitMap.has(url)) {
+ visitMap.set(url, {url, visitCount, first, last});
+ } else {
+ let currentData = visitMap.get(url);
+ currentData.visitCount += visitCount;
+ currentData.first = Math.min(currentData.first, first);
+ currentData.last = Math.max(currentData.last, last);
+ }
+ }
+ gUndoData.set("visits", Array.from(visitMap.values()));
+ },
+
+ /**
+ * Cleans up references to migrators and nsIProfileInstance instances.
+ */
+ finishMigration: function MU_finishMigration() {
+ gMigrators = null;
+ gProfileStartup = null;
+ gMigrationBundle = null;
+ },
+
+ gAvailableMigratorKeys,
+
+ MIGRATION_ENTRYPOINT_UNKNOWN: 0,
+ MIGRATION_ENTRYPOINT_FIRSTRUN: 1,
+ MIGRATION_ENTRYPOINT_FXREFRESH: 2,
+ MIGRATION_ENTRYPOINT_PLACES: 3,
+ MIGRATION_ENTRYPOINT_PASSWORDS: 4,
+
+ _sourceNameToIdMapping: {
+ "nothing": 1,
+ "firefox": 2,
+ "edge": 3,
+ "ie": 4,
+ "chrome": 5,
+ "chromium": 6,
+ "canary": 7,
+ "safari": 8,
+ "360se": 9,
+ },
+ getSourceIdForTelemetry(sourceName) {
+ return this._sourceNameToIdMapping[sourceName] || 0;
+ },
+});
diff --git a/browser/components/migration/ProfileMigrator.js b/browser/components/migration/ProfileMigrator.js
new file mode 100644
index 000000000..f67823bae
--- /dev/null
+++ b/browser/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/browser/components/migration/SafariProfileMigrator.js b/browser/components/migration/SafariProfileMigrator.js
new file mode 100644
index 000000000..6a2dbfcb1
--- /dev/null
+++ b/browser/components/migration/SafariProfileMigrator.js
@@ -0,0 +1,650 @@
+/* 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";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.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) {
+ return Task.spawn(function* () {
+ let dict = yield new Promise(resolve =>
+ PropertyListUtils.read(this._file, resolve)
+ );
+ if (!dict)
+ throw new Error("Could not read Bookmarks.plist");
+ let children = dict.get("Children");
+ if (!children)
+ throw new Error("Invalid Bookmarks.plist format");
+
+ let collection = dict.get("Title") == "com.apple.ReadingList" ?
+ this.READING_LIST_COLLECTION : this.ROOT_COLLECTION;
+ yield this._migrateCollection(children, collection);
+ }.bind(this)).then(() => aCallback(true),
+ e => { Cu.reportError(e); aCallback(false) });
+ },
+
+ // 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: Task.async(function* (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")
+ yield this._migrateCollection(children, this.TOOLBAR_COLLECTION);
+ else if (title == "BookmarksMenu")
+ yield this._migrateCollection(children, this.MENU_COLLECTION);
+ else if (title == "com.apple.ReadingList")
+ yield 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 folderGuid = -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.
+ folderGuid = PlacesUtils.bookmarks.unfiledGuid;
+ break;
+ }
+ case this.MENU_COLLECTION: {
+ folderGuid = PlacesUtils.bookmarks.menuGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ folderGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("Safari", folderGuid);
+ }
+ break;
+ }
+ case this.TOOLBAR_COLLECTION: {
+ folderGuid = PlacesUtils.bookmarks.toolbarGuid;
+ if (!MigrationUtils.isStartupMigration) {
+ folderGuid =
+ yield MigrationUtils.createImportedBookmarksFolder("Safari", folderGuid);
+ }
+ 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).
+ folderGuid = (yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: MigrationUtils.getLocalizedString("importedSafariReadingList"),
+ })).guid;
+ break;
+ }
+ default:
+ throw new Error("Unexpected value for aCollection!");
+ }
+ if (folderGuid == -1)
+ throw new Error("Invalid folder GUID");
+
+ yield this._migrateEntries(entriesFiltered, folderGuid);
+ }),
+
+ // migrate the given array of safari bookmarks to the given places
+ // folder.
+ _migrateEntries: Task.async(function* (entries, parentGuid) {
+ for (let entry of entries) {
+ let type = entry.get("WebBookmarkType");
+ if (type == "WebBookmarkTypeList" && entry.has("Children")) {
+ let title = entry.get("Title");
+ let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title
+ })).guid;
+
+ // Empty folders may not have a children array.
+ if (entry.has("Children"))
+ yield this._migrateEntries(entry.get("Children"), newFolderGuid, false);
+ }
+ else if (type == "WebBookmarkTypeLeaf" && entry.has("URLString")) {
+ let title;
+ if (entry.has("URIDictionary"))
+ title = entry.get("URIDictionary").get("title");
+
+ try {
+ yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid, url: entry.get("URLString"), title
+ });
+ } catch (ex) {
+ Cu.reportError("Invalid Safari bookmark: " + ex);
+ }
+ }
+ }
+ })
+};
+
+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"));
+ try {
+ places.push({ uri: NetUtil.newURI(entry.get("")),
+ title: entry.get("title"),
+ visits: [{ transitionType: transType,
+ visitDate: visitDate }] });
+ }
+ catch (ex) {
+ // Safari's History file may contain malformed URIs which
+ // will be ignored.
+ Cu.reportError(ex);
+ }
+ }
+ }
+ if (places.length > 0) {
+ MigrationUtils.insertVisitsWrapper(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(
+ new Uint8Array(bytes).buffer);
+ return this._dict;
+ }
+};
+
+function Preferences(aMainPreferencesPropertyListInstance) {
+ this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance;
+}
+Preferences.prototype = {
+ type: MigrationUtils.resourceTypes.SETTINGS,
+
+ migrate: function MPR_migrate(aCallback) {
+ this._mainPreferencesPropertyList.read(aDict => {
+ Task.spawn(function* () {
+ if (!aDict)
+ throw new Error("Could not read preferences file");
+
+ this._dict = aDict;
+
+ let invert = webkitVal => !webkitVal;
+ this._set("AutoFillPasswords", "signon.rememberSignons");
+ this._set("OpenNewTabsInFront", "browser.tabs.loadInBackground", invert);
+ this._set("WebKitJavaScriptCanOpenWindowsAutomatically",
+ "dom.disable_open_during_load", invert);
+
+ // layout.spellcheckDefault is a boolean stored as a number.
+ this._set("WebContinuousSpellCheckingEnabled",
+ "layout.spellcheckDefault", Number);
+
+ // Auto-load images
+ // Firefox has an elaborate set of Image preferences. The correlation is:
+ // Mode: Safari Firefox
+ // Blocked FALSE 2
+ // Allowed TRUE 1
+ // Allowed, originating site only -- 3
+ this._set("WebKitDisplayImagesKey", "permissions.default.image",
+ webkitVal => webkitVal ? 1 : 2);
+
+ this._migrateFontSettings();
+ yield this._migrateDownloadsFolder();
+ }.bind(this)).then(() => aCallback(true), ex => {
+ Cu.reportError(ex);
+ aCallback(false);
+ }).catch(Cu.reportError);
+ });
+ },
+
+ /**
+ * Attempts to migrates a preference from Safari. Returns whether the preference
+ * has been migrated.
+ * @param aSafariKey
+ * The dictionary key for the preference of Safari.
+ * @param aMozPref
+ * The gecko/firefox preference to which aSafariKey should be migrated
+ * @param [optional] aConvertFunction(aSafariValue)
+ * a function that converts the safari-preference value to the
+ * appropriate value for aMozPref. If it's not passed, then the
+ * Safari value is set as is.
+ * If aConvertFunction returns undefined, then aMozPref is not set
+ * at all.
+ * @return whether or not aMozPref was set.
+ */
+ _set: function MPR_set(aSafariKey, aMozPref, aConvertFunction) {
+ if (this._dict.has(aSafariKey)) {
+ let safariVal = this._dict.get(aSafariKey);
+ let mozVal = aConvertFunction !== undefined ?
+ aConvertFunction(safariVal) : safariVal;
+ switch (typeof mozVal) {
+ case "string":
+ Services.prefs.setCharPref(aMozPref, mozVal);
+ break;
+ case "number":
+ Services.prefs.setIntPref(aMozPref, mozVal);
+ break;
+ case "boolean":
+ Services.prefs.setBoolPref(aMozPref, mozVal);
+ break;
+ case "undefined":
+ return false;
+ default:
+ throw new Error("Unexpected value type: " + (typeof mozVal));
+ }
+ }
+ return true;
+ },
+
+ // Fonts settings are quite problematic for migration, for a couple of
+ // reasons:
+ // (a) Every font preference in Gecko is set for a particular language.
+ // In Safari, each font preference applies to all languages.
+ // (b) The current underlying implementation of nsIFontEnumerator cannot
+ // really tell you anything about a font: no matter what language or type
+ // you try to enumerate with EnumerateFonts, you get an array of all
+ // fonts in the systems (This also breaks our fonts dialog).
+ // (c) In Gecko, each langauge has a distinct serif and sans-serif font
+ // preference. Safari has only one default font setting. It seems that
+ // it checks if it's a serif or sans serif font, and when a site
+ // explicitly asks to use serif/sans-serif font, it uses the default font
+ // only if it applies to this type.
+ // (d) The solution of guessing the lang-group out of the default charset (as
+ // done in the old Safari migrator) can only work when:
+ // (1) The default charset preference is set.
+ // (2) It's not a unicode charset.
+ // For now, we use the language implied by the system locale as the
+ // lang-group. The only exception is minimal font size, which is an
+ // accessibility preference in Safari (under the Advanced tab). If it is set,
+ // we set it for all languages.
+ // As for the font type of the default font (serif/sans-serif), the default
+ // type for the given language is used (set in font.default.LANGGROUP).
+ _migrateFontSettings: function MPR__migrateFontSettings() {
+ // If "Never use font sizes smaller than [ ] is set", migrate it for all
+ // languages.
+ if (this._dict.has("WebKitMinimumFontSize")) {
+ let minimumSize = this._dict.get("WebKitMinimumFontSize");
+ if (typeof minimumSize == "number") {
+ let prefs = Services.prefs.getChildList("font.minimum-size");
+ for (let pref of prefs) {
+ Services.prefs.setIntPref(pref, minimumSize);
+ }
+ }
+ else {
+ Cu.reportError("WebKitMinimumFontSize was set to an invalid value: " +
+ minimumSize);
+ }
+ }
+
+ // In theory, the lang group could be "x-unicode". This will result
+ // in setting the fonts for "Other Languages".
+ let lang = this._getLocaleLangGroup();
+
+ let anySet = false;
+ let fontType = Services.prefs.getCharPref("font.default." + lang);
+ anySet |= this._set("WebKitFixedFont", "font.name.monospace." + lang);
+ anySet |= this._set("WebKitDefaultFixedFontSize", "font.size.fixed." + lang);
+ anySet |= this._set("WebKitStandardFont",
+ "font.name." + fontType + "." + lang);
+ anySet |= this._set("WebKitDefaultFontSize", "font.size.variable." + lang);
+
+ // If we set font settings for a particular language, we'll also set the
+ // fonts dialog to open with the fonts settings for that langauge.
+ if (anySet)
+ Services.prefs.setCharPref("font.language.group", lang);
+ },
+
+ // Get the language group for the system locale.
+ _getLocaleLangGroup: function MPR__getLocaleLangGroup() {
+ let locale = Services.locale.getLocaleComponentForUserAgent();
+
+ // See nsLanguageAtomService::GetLanguageGroup
+ let localeLangGroup = "x-unicode";
+ let bundle = Services.strings.createBundle(
+ "resource://gre/res/langGroups.properties");
+ try {
+ localeLangGroup = bundle.GetStringFromName(locale);
+ }
+ catch (ex) {
+ let hyphenAt = locale.indexOf("-");
+ if (hyphenAt != -1) {
+ try {
+ localeLangGroup = bundle.GetStringFromName(locale.substr(0, hyphenAt));
+ }
+ catch (ex2) { }
+ }
+ }
+ return localeLangGroup;
+ },
+
+ _migrateDownloadsFolder: Task.async(function* () {
+ if (!this._dict.has("DownloadsPath"))
+ return;
+
+ let downloadsFolder = FileUtils.File(this._dict.get("DownloadsPath"));
+
+ // If the download folder is set to the Desktop or to ~/Downloads, set the
+ // folderList pref appropriately so that "Desktop"/Downloads is shown with
+ // pretty name in the preferences dialog.
+ let folderListVal = 2;
+ if (downloadsFolder.equals(FileUtils.getDir("Desk", []))) {
+ folderListVal = 0;
+ }
+ else {
+ let systemDownloadsPath = yield Downloads.getSystemDownloadsDirectory();
+ let systemDownloadsFolder = FileUtils.File(systemDownloadsPath);
+ if (downloadsFolder.equals(systemDownloadsFolder))
+ folderListVal = 1;
+ }
+ Services.prefs.setIntPref("browser.download.folderList", folderListVal);
+ Services.prefs.setComplexValue("browser.download.dir", Ci.nsILocalFile,
+ downloadsFolder);
+ }),
+};
+
+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 = recentSearchStrings.map((searchString) => (
+ {op: "add",
+ fieldname: "searchbar-history",
+ value: searchString}));
+ FormHistory.update(changes);
+ }
+ }
+ }.bind(this), aCallback));
+ }
+};
+
+// On OS X, the cookie-accept policy preference is stored in a separate
+// property list.
+function WebFoundationCookieBehavior(aWebFoundationFile) {
+ this._file = aWebFoundationFile;
+}
+WebFoundationCookieBehavior.prototype = {
+ type: MigrationUtils.resourceTypes.SETTINGS,
+
+ migrate: function WFPL_migrate(aCallback) {
+ PropertyListUtils.read(this._file, MigrationUtils.wrapMigrateFunction(
+ function migrateCookieBehavior(aDict) {
+ if (!aDict)
+ throw new Error("Could not read com.apple.WebFoundation.plist");
+
+ if (aDict.has("NSHTTPAcceptCookies")) {
+ // Setting Safari Firefox
+ // Always Accept always 0
+ // Accept from Originating current page 1
+ // Never Accept never 2
+ let acceptCookies = aDict.get("NSHTTPAcceptCookies");
+ let cookieValue = 0;
+ if (acceptCookies == "never") {
+ cookieValue = 2;
+ } else if (acceptCookies == "current page") {
+ cookieValue = 1;
+ }
+ Services.prefs.setIntPref("network.cookie.cookieBehavior",
+ cookieValue);
+ }
+ }.bind(this), aCallback));
+ }
+};
+
+function SafariProfileMigrator() {
+}
+
+SafariProfileMigrator.prototype = Object.create(MigratorPrototype);
+
+SafariProfileMigrator.prototype.getResources = function SM_getResources() {
+ let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false);
+ 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 code is used on mac too, because
+ // Apple may fix this at some point.
+ pushProfileFileResource("ReadingList.plist", Bookmarks);
+
+ let prefs = this.mainPreferencesPropertyList;
+ if (prefs) {
+ resources.push(new Preferences(prefs));
+ resources.push(new SearchStrings(prefs));
+ }
+
+ let wfFile = FileUtils.getFile("UsrPrfs", ["com.apple.WebFoundation.plist"]);
+ if (wfFile.exists())
+ resources.push(new WebFoundationCookieBehavior(wfFile));
+
+ return resources;
+};
+
+SafariProfileMigrator.prototype.getLastUsedDate = function SM_getLastUsedDate() {
+ let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false);
+ let datePromises = ["Bookmarks.plist", "History.plist"].map(file => {
+ let path = OS.Path.join(profileDir.path, file);
+ return OS.File.stat(path).catch(() => null).then(info => {
+ return info ? info.lastModificationDate : 0;
+ });
+ });
+ return Promise.all(datePromises).then(dates => {
+ return new Date(Math.max.apply(Math, dates));
+ });
+};
+
+Object.defineProperty(SafariProfileMigrator.prototype, "mainPreferencesPropertyList", {
+ get: function get_mainPreferencesPropertyList() {
+ if (this._mainPreferencesPropertyList === undefined) {
+ let file = FileUtils.getDir("UsrPrfs", [], false);
+ if (file.exists()) {
+ file.append("com.apple.Safari.plist");
+ if (file.exists()) {
+ this._mainPreferencesPropertyList =
+ new MainPreferencesPropertyList(file);
+ return this._mainPreferencesPropertyList;
+ }
+ }
+ this._mainPreferencesPropertyList = null;
+ return this._mainPreferencesPropertyList;
+ }
+ 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/browser/components/migration/content/aboutWelcomeBack.xhtml b/browser/components/migration/content/aboutWelcomeBack.xhtml
new file mode 100644
index 000000000..d9fdb6c2c
--- /dev/null
+++ b/browser/components/migration/content/aboutWelcomeBack.xhtml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# 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/.
+-->
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % restorepageDTD SYSTEM "chrome://browser/locale/aboutSessionRestore.dtd">
+ %restorepageDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <head>
+ <title>&welcomeback2.tabtitle;</title>
+ <link rel="stylesheet" href="chrome://global/skin/in-content/info-pages.css" type="text/css" media="all"/>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutWelcomeBack.css" type="text/css" media="all"/>
+ <link rel="icon" type="image/png" href="chrome://global/skin/icons/information-16.png"/>
+
+ <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutSessionRestore.js"/>
+ </head>
+
+ <body dir="&locale.dir;">
+
+ <div class="container">
+
+ <div class="title">
+ <h1 class="title-text">&welcomeback2.pageTitle;</h1>
+ </div>
+
+ <div class="description">
+
+ <p>&welcomeback2.pageInfo1;</p>
+ <!-- Note a href in the anchor below is added by JS -->
+ <p>&welcomeback2.beforelink.pageInfo2;<a id="linkMoreTroubleshooting" target="_blank">&welcomeback2.link.pageInfo2;</a>&welcomeback2.afterlink.pageInfo2;</p>
+
+ <div>
+ <div class="radioRestoreContainer">
+ <input class="radioRestoreButton" id="radioRestoreAll" type="radio"
+ name="restore" checked="checked"/>
+ <label class="radioRestoreLabel" for="radioRestoreAll">&welcomeback2.label.restoreAll;</label>
+ </div>
+
+ <div class="radioRestoreContainer">
+ <input class="radioRestoreButton" id="radioRestoreChoose" type="radio"
+ name="restore"/>
+ <label class="radioRestoreLabel" for="radioRestoreChoose">&welcomeback2.label.restoreSome;</label>
+ </div>
+ </div>
+ </div>
+
+ <div class="tree-container">
+ <xul:tree id="tabList" flex="1" seltype="single" hidecolumnpicker="true"
+ onclick="onListClick(event);" onkeydown="onListKeyDown(event);"
+ _window_label="&restorepage.windowLabel;">
+ <xul:treecols>
+ <xul:treecol cycler="true" id="restore" type="checkbox" label="&restorepage.restoreHeader;"/>
+ <xul:splitter class="tree-splitter"/>
+ <xul:treecol primary="true" id="title" label="&restorepage.listHeader;" flex="1"/>
+ </xul:treecols>
+ <xul:treechildren flex="1"/>
+ </xul:tree>
+ </div>
+
+ <div class="button-container">
+ <xul:button class="primary"
+ id="errorTryAgain"
+ label="&welcomeback2.restoreButton;"
+ accesskey="&welcomeback2.restoreButton.access;"
+ oncommand="restoreSession();"/>
+ </div>
+
+ <input type="text" id="sessionData" style="display: none;"/>
+
+ </div>
+ </body>
+</html>
diff --git a/browser/components/migration/content/extra-migration-strings.properties b/browser/components/migration/content/extra-migration-strings.properties
new file mode 100644
index 000000000..208906b31
--- /dev/null
+++ b/browser/components/migration/content/extra-migration-strings.properties
@@ -0,0 +1,14 @@
+# Automigration undo notification.
+# %1$S will be replaced with the name of the browser we imported from, %2$S will be replaced with brandShortName
+automigration.undo.message.all = Pick up where you left off. We’ve imported these sites and your bookmarks, history and passwords from %1$S into %2$S.
+automigration.undo.message.bookmarks = Pick up where you left off. We’ve imported these sites and your bookmarks from %1$S into %2$S.
+automigration.undo.message.bookmarks.logins = Pick up where you left off. We’ve imported these sites and your bookmarks and passwords from %1$S into %2$S.
+automigration.undo.message.bookmarks.visits = Pick up where you left off. We’ve imported these sites and your bookmarks and history from %1$S into %2$S.
+automigration.undo.message.logins = Pick up where you left off. We’ve imported your passwords from %1$S into %2$S.
+automigration.undo.message.logins.visits = Pick up where you left off. We’ve imported these sites and your history and passwords from %1$S into %2$S.
+automigration.undo.message.visits = Pick up where you left off. We’ve imported these sites and your history from %1$S into %2$S.
+automigration.undo.keep2.label = OK, Got it
+automigration.undo.keep2.accesskey = O
+automigration.undo.dontkeep2.label = No Thanks
+automigration.undo.dontkeep2.accesskey = N
+automigration.undo.unknownbrowser = Unknown Browser
diff --git a/browser/components/migration/content/migration.js b/browser/components/migration/content/migration.js
new file mode 100644
index 000000000..eb2175628
--- /dev/null
+++ b/browser/components/migration/content/migration.js
@@ -0,0 +1,549 @@
+/* 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";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+const kIMig = Ci.nsIBrowserProfileMigrator;
+const kIPStartup = Ci.nsIProfileStartup;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/MigrationUtils.jsm");
+
+var MigrationWizard = { /* exported 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 ()
+ {
+ let os = Services.obs;
+ 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;
+
+ let args = window.arguments;
+ let entryPointId = args[0] || MigrationUtils.MIGRATION_ENTRYPOINT_UNKNOWN;
+ Services.telemetry.getHistogramById("FX_MIGRATION_ENTRY_POINT").add(entryPointId);
+ this.isInitialMigration = entryPointId == MigrationUtils.MIGRATION_ENTRYPOINT_FIRSTRUN;
+
+ if (args.length > 1) {
+ this._source = args[1];
+ this._migrator = args[2] instanceof kIMig ? args[2] : null;
+ this._autoMigrate = args[3].QueryInterface(kIPStartup);
+ this._skipImportSourcePage = args[4];
+ if (this._migrator && args[5]) {
+ let sourceProfiles = this._migrator.sourceProfiles;
+ this._selectedProfile = sourceProfiles.find(profile => profile.id == args[5]);
+ }
+
+ 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.
+ document.getElementById("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 ()
+ {
+ // Show warning message to close the selected browser when needed
+ function toggleCloseBrowserWarning() {
+ let visibility = "hidden";
+ if (group.selectedItem.id != "nothing") {
+ let migrator = MigrationUtils.getMigrator(group.selectedItem.id);
+ visibility = migrator.sourceLocked ? "visible" : "hidden";
+ }
+ document.getElementById("closeSourceBrowser").style.visibility = visibility;
+ }
+ this._wiz.canRewind = false;
+
+ var selectedMigrator = null;
+ this._availableMigrators = [];
+
+ // 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];
+ this._availableMigrators.push([migratorKey, migrator]);
+ } else {
+ // Hide this option
+ group.childNodes[i].hidden = true;
+ }
+ }
+ }
+ if (this.isInitialMigration) {
+ Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_BROWSER_COUNT")
+ .add(this._availableMigrators.length);
+ let defaultBrowser = MigrationUtils.getMigratorKeyForDefaultBrowser();
+ // This will record 0 for unknown default browser IDs.
+ defaultBrowser = MigrationUtils.getSourceIdForTelemetry(defaultBrowser);
+ Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_EXISTING_DEFAULT_BROWSER")
+ .add(defaultBrowser);
+ }
+
+ group.addEventListener("command", toggleCloseBrowserWarning);
+
+ if (selectedMigrator) {
+ group.selectedItem = selectedMigrator;
+ toggleCloseBrowserWarning();
+ } 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") {
+ // Need to do telemetry here because we're closing the dialog before we get to
+ // do actual migration. For actual migration, this doesn't happen until after
+ // migration takes place.
+ Services.telemetry.getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
+ .add(MigrationUtils.getSourceIdForTelemetry("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;
+ }
+ return undefined;
+ },
+
+ // 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 ()
+ {
+ 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");
+ var pageTitle, pageDesc, mainStr;
+ // These strings don't exist when not using official branding. If that's
+ // the case, just skip this page.
+ try {
+ pageTitle = brandBundle.getString("homePageMigrationPageTitle");
+ pageDesc = brandBundle.getString("homePageMigrationDescription");
+ 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 appName = MigrationUtils.getBrowserName(this._source);
+
+ // 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 && appName) {
+ 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);
+ },
+
+ onMigratingMigrate: function ()
+ {
+ this._migrator.migrate(this._itemsFlags, this._autoMigrate, this._selectedProfile);
+
+ Services.telemetry.getHistogramById("FX_MIGRATION_SOURCE_BROWSER")
+ .add(MigrationUtils.getSourceIdForTelemetry(this._source));
+ if (!this._autoMigrate) {
+ let hist = Services.telemetry.getKeyedHistogramById("FX_MIGRATION_USAGE");
+ let exp = 0;
+ let items = this._itemsFlags;
+ while (items) {
+ if (items & 1) {
+ hist.add(this._source, exp);
+ }
+ items = items >> 1;
+ exp++;
+ }
+ }
+ },
+
+ _listItems: function (aID)
+ {
+ var items = document.getElementById(aID);
+ while (items.hasChildNodes())
+ items.removeChild(items.firstChild);
+
+ var itemID;
+ for (var i = 0; i < 16; ++i) {
+ 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)
+ {
+ var label;
+ switch (aTopic) {
+ case "Migration:Started":
+ break;
+ case "Migration:ItemBeforeMigrate":
+ label = document.getElementById(aData + "_migrated");
+ if (label)
+ label.setAttribute("style", "font-weight: bold");
+ break;
+ case "Migration:ItemAfterMigrate":
+ label = document.getElementById(aData + "_migrated");
+ if (label)
+ label.removeAttribute("style");
+ break;
+ case "Migration:Ended":
+ if (this.isInitialMigration) {
+ // Ensure errors in reporting data recency do not affect the rest of the migration.
+ try {
+ this.reportDataRecencyTelemetry();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ if (this._autoMigrate) {
+ let hasImportedHomepage = !!(this._newHomePage && this._newHomePage != "DEFAULT");
+ Services.telemetry.getKeyedHistogramById("FX_MIGRATION_IMPORTED_HOMEPAGE")
+ .add(this._source, hasImportedHomepage);
+ 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":
+ let type = "undefined";
+ let numericType = parseInt(aData);
+ switch (numericType) {
+ 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.");
+ Services.telemetry.getKeyedHistogramById("FX_MIGRATION_ERRORS")
+ .add(this._source, Math.log2(numericType));
+ break;
+ }
+ },
+
+ onDonePageShow: function ()
+ {
+ this._wiz.getButton("cancel").disabled = true;
+ this._wiz.canRewind = false;
+ this._listItems("doneItems");
+ },
+
+ reportDataRecencyTelemetry() {
+ let histogram = Services.telemetry.getKeyedHistogramById("FX_STARTUP_MIGRATION_DATA_RECENCY");
+ let lastUsedPromises = [];
+ for (let [key, migrator] of this._availableMigrators) {
+ // No block-scoped let in for...of loop conditions, so get the source:
+ let localKey = key;
+ lastUsedPromises.push(migrator.getLastUsedDate().then(date => {
+ const ONE_YEAR = 24 * 365;
+ let diffInHours = Math.round((Date.now() - date) / (60 * 60 * 1000));
+ if (diffInHours > ONE_YEAR) {
+ diffInHours = ONE_YEAR;
+ }
+ histogram.add(localKey, diffInHours);
+ return [localKey, diffInHours];
+ }));
+ }
+ Promise.all(lastUsedPromises).then(migratorUsedTimeDiff => {
+ // Sort low to high.
+ migratorUsedTimeDiff.sort(([keyA, diffA], [keyB, diffB]) => diffA - diffB); /* eslint no-unused-vars: off */
+ let usedMostRecentBrowser = migratorUsedTimeDiff.length && this._source == migratorUsedTimeDiff[0][0];
+ let usedRecentBrowser =
+ Services.telemetry.getKeyedHistogramById("FX_STARTUP_MIGRATION_USED_RECENT_BROWSER");
+ usedRecentBrowser.add(this._source, usedMostRecentBrowser);
+ });
+ },
+};
diff --git a/browser/components/migration/content/migration.xul b/browser/components/migration/content/migration.xul
new file mode 100644
index 000000000..e85091002
--- /dev/null
+++ b/browser/components/migration/content/migration.xul
@@ -0,0 +1,109 @@
+<?xml version="1.0"?>
+# 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/.
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://browser/locale/migration/migration.dtd" >
+
+<wizard id="migrationWizard"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="Browser:MigrationWizard"
+ title="&migrationWizard.title;"
+ onload="MigrationWizard.init()"
+ onunload="MigrationWizard.uninit()"
+ style="width: 40em;"
+ buttons="accept,cancel"
+ branded="true">
+
+ <script type="application/javascript" src="chrome://browser/content/migration/migration.js"/>
+
+ <stringbundle id="brandBundle" src="chrome://branding/locale/brand.properties"/>
+
+ <wizardpage id="importSource" pageid="importSource" next="selectProfile"
+ label="&importSource.title;"
+ onpageadvanced="return MigrationWizard.onImportSourcePageAdvanced();">
+#ifdef XP_WIN
+ <description id="importAll" control="importSourceGroup">&importFrom.label;</description>
+#else
+ <description id="importAll" control="importSourceGroup">&importFromUnix.label;</description>
+#endif
+ <description id="importBookmarks" control="importSourceGroup" hidden="true">&importFromBookmarks.label;</description>
+
+ <radiogroup id="importSourceGroup" align="start">
+# NB: if you add items to this list, please also assign them a unique migrator ID in MigrationUtils.jsm
+ <radio id="firefox" label="&importFromFirefox.label;" accesskey="&importFromFirefox.accesskey;"/>
+#ifdef XP_WIN
+ <radio id="edge" label="&importFromEdge.label;" accesskey="&importFromEdge.accesskey;"/>
+ <radio id="ie" label="&importFromIE.label;" accesskey="&importFromIE.accesskey;"/>
+ <radio id="chrome" label="&importFromChrome.label;" accesskey="&importFromChrome.accesskey;"/>
+ <radio id="chromium" label="&importFromChromium.label;" accesskey="&importFromChromium.accesskey;"/>
+ <radio id="canary" label="&importFromCanary.label;" accesskey="&importFromCanary.accesskey;"/>
+ <radio id="360se" label="&importFrom360se.label;" accesskey="&importFrom360se.accesskey;"/>
+#elifdef XP_MACOSX
+ <radio id="safari" label="&importFromSafari.label;" accesskey="&importFromSafari.accesskey;"/>
+ <radio id="chrome" label="&importFromChrome.label;" accesskey="&importFromChrome.accesskey;"/>
+ <radio id="chromium" label="&importFromChromium.label;" accesskey="&importFromChromium.accesskey;"/>
+ <radio id="canary" label="&importFromCanary.label;" accesskey="&importFromCanary.accesskey;"/>
+#elifdef XP_UNIX
+ <radio id="chrome" label="&importFromChrome.label;" accesskey="&importFromChrome.accesskey;"/>
+ <radio id="chromium" label="&importFromChromium.label;" accesskey="&importFromChromium.accesskey;"/>
+#endif
+ <radio id="nothing" label="&importFromNothing.label;" accesskey="&importFromNothing.accesskey;" hidden="true"/>
+ </radiogroup>
+ <label id="noSources" hidden="true">&noMigrationSources.label;</label>
+ <spacer flex="1"/>
+ <description class="header" id="closeSourceBrowser" style="visibility:hidden">&closeSourceBrowser.label;</description>
+ </wizardpage>
+
+ <wizardpage id="selectProfile" pageid="selectProfile" label="&selectProfile.title;"
+ next="importItems"
+ onpageshow="return MigrationWizard.onSelectProfilePageShow();"
+ onpagerewound="return MigrationWizard.onSelectProfilePageRewound();"
+ onpageadvanced="return MigrationWizard.onSelectProfilePageAdvanced();">
+ <description control="profiles">&selectProfile.label;</description>
+
+ <radiogroup id="profiles" align="left"/>
+ </wizardpage>
+
+ <wizardpage id="importItems" pageid="importItems" label="&importItems.title;"
+ next="homePageImport"
+ onpageshow="return MigrationWizard.onImportItemsPageShow();"
+ onpagerewound="return MigrationWizard.onImportItemsPageRewound();"
+ onpageadvanced="return MigrationWizard.onImportItemsPageAdvanced();"
+ oncommand="MigrationWizard.onImportItemCommand();">
+ <description control="dataSources">&importItems.label;</description>
+
+ <vbox id="dataSources" style="overflow: auto; -moz-appearance: listbox" align="left" flex="1" role="group"/>
+ </wizardpage>
+
+ <wizardpage id="homePageImport" pageid="homePageImport"
+ next="migrating"
+ onpageshow="return MigrationWizard.onHomePageMigrationPageShow();"
+ onpageadvanced="return MigrationWizard.onHomePageMigrationPageAdvanced();">
+
+ <description id="homePageImportDesc" control="homePageRadioGroup"/>
+ <radiogroup id="homePageRadiogroup">
+ <radio id="homePageSingleStart" selected="true" />
+ <radio id="oldHomePage" hidden="true" />
+ </radiogroup>
+ </wizardpage>
+
+ <wizardpage id="migrating" pageid="migrating" label="&migrating.title;"
+ next="done"
+ onpageshow="MigrationWizard.onMigratingPageShow();">
+ <description control="migratingItems">&migrating.label;</description>
+
+ <vbox id="migratingItems" style="overflow: auto;" align="left" role="group"/>
+ </wizardpage>
+
+ <wizardpage id="done" pageid="done" label="&done.title;"
+ onpageshow="MigrationWizard.onDonePageShow();">
+ <description control="doneItems">&done.label;</description>
+
+ <vbox id="doneItems" style="overflow: auto;" align="left" role="group"/>
+ </wizardpage>
+
+</wizard>
+
diff --git a/browser/components/migration/jar.mn b/browser/components/migration/jar.mn
new file mode 100644
index 000000000..110788bc4
--- /dev/null
+++ b/browser/components/migration/jar.mn
@@ -0,0 +1,9 @@
+# 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/.
+
+browser.jar:
+* content/browser/migration/migration.xul (content/migration.xul)
+ content/browser/migration/migration.js (content/migration.js)
+ content/browser/migration/extra-migration-strings.properties (content/extra-migration-strings.properties)
+ content/browser/aboutWelcomeBack.xhtml (content/aboutWelcomeBack.xhtml)
diff --git a/browser/components/migration/moz.build b/browser/components/migration/moz.build
new file mode 100644
index 000000000..751ea0cd9
--- /dev/null
+++ b/browser/components/migration/moz.build
@@ -0,0 +1,62 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+MARIONETTE_UNIT_MANIFESTS += ['tests/marionette/manifest.ini']
+
+BROWSER_CHROME_MANIFESTS += [ 'tests/browser/browser.ini']
+
+JAR_MANIFESTS += ['jar.mn']
+
+XPIDL_SOURCES += [
+ 'nsIBrowserProfileMigrator.idl',
+]
+
+XPIDL_MODULE = 'migration'
+
+EXTRA_COMPONENTS += [
+ 'ChromeProfileMigrator.js',
+ 'FirefoxProfileMigrator.js',
+ 'ProfileMigrator.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'BrowserProfileMigrators.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'AutoMigrate.jsm',
+ 'MigrationUtils.jsm',
+]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ SOURCES += [
+ 'nsIEHistoryEnumerator.cpp',
+ ]
+ EXTRA_COMPONENTS += [
+ '360seProfileMigrator.js',
+ 'EdgeProfileMigrator.js',
+ 'IEProfileMigrator.js',
+ ]
+ EXTRA_JS_MODULES += [
+ 'ESEDBReader.jsm',
+ 'MSMigrationUtils.jsm',
+ ]
+ DEFINES['HAS_360SE_MIGRATOR'] = True
+ DEFINES['HAS_IE_MIGRATOR'] = True
+ DEFINES['HAS_EDGE_MIGRATOR'] = True
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+ EXTRA_COMPONENTS += [
+ 'SafariProfileMigrator.js',
+ ]
+ DEFINES['HAS_SAFARI_MIGRATOR'] = True
+
+FINAL_LIBRARY = 'browsercomps'
+
+with Files('**'):
+ BUG_COMPONENT = ('Firefox', 'Migration')
diff --git a/browser/components/migration/nsIBrowserProfileMigrator.idl b/browser/components/migration/nsIBrowserProfileMigrator.idl
new file mode 100644
index 000000000..a251c3683
--- /dev/null
+++ b/browser/components/migration/nsIBrowserProfileMigrator.idl
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIArray;
+interface nsIProfileStartup;
+
+[scriptable, uuid(22b56ffc-3149-43c5-b5a9-b3a6b678de93)]
+interface nsIBrowserProfileMigrator : nsISupports
+{
+ /**
+ * profile items to migrate. use with migrate().
+ */
+ const unsigned short ALL = 0x0000;
+ const unsigned short SETTINGS = 0x0001;
+ const unsigned short COOKIES = 0x0002;
+ const unsigned short HISTORY = 0x0004;
+ const unsigned short FORMDATA = 0x0008;
+ const unsigned short PASSWORDS = 0x0010;
+ const unsigned short BOOKMARKS = 0x0020;
+ const unsigned short OTHERDATA = 0x0040;
+ const unsigned short SESSION = 0x0080;
+
+ /**
+ * Copy user profile information to the current active profile.
+ * @param aItems list of data items to migrate. see above for values.
+ * @param aStartup helper interface which is non-null if called during startup.
+ * @param aProfile profile to migrate from, if there is more than one.
+ */
+ void migrate(in unsigned short aItems, in nsIProfileStartup aStartup, in jsval aProfile);
+
+ /**
+ * A bit field containing profile items that this migrator
+ * offers for import.
+ * @param aProfile the profile that we are looking for available data
+ * to import
+ * @param aDoingStartup "true" if the profile is not currently being used.
+ * @return bit field containing profile items (see above)
+ * @note a return value of 0 represents no items rather than ALL.
+ */
+ unsigned short getMigrateData(in jsval aProfile, in boolean aDoingStartup);
+
+ /**
+ * Get the last time data from this browser was modified
+ * @return a promise that resolves to a JS Date object
+ */
+ jsval getLastUsedDate();
+
+ /**
+ * Whether or not there is any data that can be imported from this
+ * browser (i.e. whether or not it is installed, and there exists
+ * a user profile)
+ */
+ readonly attribute boolean sourceExists;
+
+
+ /**
+ * An enumeration of available profiles. If the import source does
+ * not support profiles, this attribute is null.
+ */
+ readonly attribute jsval sourceProfiles;
+
+ /**
+ * The import source homepage. Returns null if not present/available
+ */
+ readonly attribute AUTF8String sourceHomePageURL;
+
+
+ /**
+ * Whether the source browser data is locked/in-use meaning migration likely
+ * won't succeed and the user should be warned.
+ */
+ readonly attribute boolean sourceLocked;
+};
diff --git a/browser/components/migration/nsIEHistoryEnumerator.cpp b/browser/components/migration/nsIEHistoryEnumerator.cpp
new file mode 100644
index 000000000..116e9a860
--- /dev/null
+++ b/browser/components/migration/nsIEHistoryEnumerator.cpp
@@ -0,0 +1,120 @@
+/* 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/. */
+
+#include "nsIEHistoryEnumerator.h"
+
+#include <urlhist.h>
+#include <shlguid.h>
+
+#include "nsArrayEnumerator.h"
+#include "nsCOMArray.h"
+#include "nsIURI.h"
+#include "nsIVariant.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsWindowsMigrationUtils.h"
+#include "prtime.h"
+
+////////////////////////////////////////////////////////////////////////////////
+//// nsIEHistoryEnumerator
+
+NS_IMPL_ISUPPORTS(nsIEHistoryEnumerator, nsISimpleEnumerator)
+
+nsIEHistoryEnumerator::nsIEHistoryEnumerator()
+{
+ ::CoInitialize(nullptr);
+}
+
+nsIEHistoryEnumerator::~nsIEHistoryEnumerator()
+{
+ ::CoUninitialize();
+}
+
+void
+nsIEHistoryEnumerator::EnsureInitialized()
+{
+ if (mURLEnumerator)
+ return;
+
+ HRESULT hr = ::CoCreateInstance(CLSID_CUrlHistory,
+ nullptr,
+ CLSCTX_INPROC_SERVER,
+ IID_IUrlHistoryStg2,
+ getter_AddRefs(mIEHistory));
+ if (FAILED(hr))
+ return;
+
+ hr = mIEHistory->EnumUrls(getter_AddRefs(mURLEnumerator));
+ if (FAILED(hr))
+ return;
+}
+
+NS_IMETHODIMP
+nsIEHistoryEnumerator::HasMoreElements(bool* _retval)
+{
+ *_retval = false;
+
+ EnsureInitialized();
+ MOZ_ASSERT(mURLEnumerator, "Should have instanced an IE History URLEnumerator");
+ if (!mURLEnumerator)
+ return NS_OK;
+
+ STATURL statURL;
+ ULONG fetched;
+
+ // First argument is not implemented, so doesn't matter what we pass.
+ HRESULT hr = mURLEnumerator->Next(1, &statURL, &fetched);
+ if (FAILED(hr) || fetched != 1UL) {
+ // Reached the last entry.
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ if (statURL.pwcsUrl) {
+ nsDependentString url(statURL.pwcsUrl);
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), url);
+ ::CoTaskMemFree(statURL.pwcsUrl);
+ if (NS_FAILED(rv)) {
+ // Got a corrupt or invalid URI, continue to the next entry.
+ return HasMoreElements(_retval);
+ }
+ }
+
+ nsDependentString title(statURL.pwcsTitle ? statURL.pwcsTitle : L"");
+
+ bool lastVisitTimeIsValid;
+ PRTime lastVisited = WinMigrationFileTimeToPRTime(&(statURL.ftLastVisited), &lastVisitTimeIsValid);
+
+ mCachedNextEntry = do_CreateInstance("@mozilla.org/hash-property-bag;1");
+ MOZ_ASSERT(mCachedNextEntry, "Should have instanced a new property bag");
+ if (mCachedNextEntry) {
+ mCachedNextEntry->SetPropertyAsInterface(NS_LITERAL_STRING("uri"), uri);
+ mCachedNextEntry->SetPropertyAsAString(NS_LITERAL_STRING("title"), title);
+ if (lastVisitTimeIsValid) {
+ mCachedNextEntry->SetPropertyAsInt64(NS_LITERAL_STRING("time"), lastVisited);
+ }
+
+ *_retval = true;
+ }
+
+ if (statURL.pwcsTitle)
+ ::CoTaskMemFree(statURL.pwcsTitle);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsIEHistoryEnumerator::GetNext(nsISupports** _retval)
+{
+ *_retval = nullptr;
+
+ if (!mCachedNextEntry)
+ return NS_ERROR_FAILURE;
+
+ NS_ADDREF(*_retval = mCachedNextEntry);
+ // Release the cached entry, so it can't be returned twice.
+ mCachedNextEntry = nullptr;
+
+ return NS_OK;
+}
diff --git a/browser/components/migration/nsIEHistoryEnumerator.h b/browser/components/migration/nsIEHistoryEnumerator.h
new file mode 100644
index 000000000..1572a8dd5
--- /dev/null
+++ b/browser/components/migration/nsIEHistoryEnumerator.h
@@ -0,0 +1,37 @@
+/* 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/. */
+
+#ifndef iehistoryenumerator___h___
+#define iehistoryenumerator___h___
+
+#include <urlhist.h>
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIWritablePropertyBag2.h"
+
+class nsIEHistoryEnumerator final : public nsISimpleEnumerator
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ nsIEHistoryEnumerator();
+
+private:
+ ~nsIEHistoryEnumerator();
+
+ /**
+ * Initializes the history reader, if needed.
+ */
+ void EnsureInitialized();
+
+ RefPtr<IUrlHistoryStg2> mIEHistory;
+ RefPtr<IEnumSTATURL> mURLEnumerator;
+
+ nsCOMPtr<nsIWritablePropertyBag2> mCachedNextEntry;
+};
+
+#endif
diff --git a/browser/components/migration/nsWindowsMigrationUtils.h b/browser/components/migration/nsWindowsMigrationUtils.h
new file mode 100644
index 000000000..0288d93d3
--- /dev/null
+++ b/browser/components/migration/nsWindowsMigrationUtils.h
@@ -0,0 +1,36 @@
+/* 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/. */
+
+#ifndef windowsmigrationutils__h__
+#define windowsmigrationutils__h__
+
+#include "prtime.h"
+
+static
+PRTime WinMigrationFileTimeToPRTime(FILETIME* filetime, bool* isValid)
+{
+ SYSTEMTIME st;
+ *isValid = ::FileTimeToSystemTime(filetime, &st);
+ if (!*isValid) {
+ return 0;
+ }
+ PRExplodedTime prt;
+ prt.tm_year = st.wYear;
+ // SYSTEMTIME's day-of-month parameter is 1-based,
+ // PRExplodedTime's is 0-based.
+ prt.tm_month = st.wMonth - 1;
+ prt.tm_mday = st.wDay;
+ prt.tm_hour = st.wHour;
+ prt.tm_min = st.wMinute;
+ prt.tm_sec = st.wSecond;
+ prt.tm_usec = st.wMilliseconds * 1000;
+ prt.tm_wday = 0;
+ prt.tm_yday = 0;
+ prt.tm_params.tp_gmt_offset = 0;
+ prt.tm_params.tp_dst_offset = 0;
+ return PR_ImplodeTime(&prt);
+}
+
+#endif
+
diff --git a/browser/components/migration/tests/browser/.eslintrc.js b/browser/components/migration/tests/browser/.eslintrc.js
new file mode 100644
index 000000000..3ea6eeb8c
--- /dev/null
+++ b/browser/components/migration/tests/browser/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js",
+ "../../../../../testing/mochitest/mochitest.eslintrc.js",
+ ]
+};
+
diff --git a/browser/components/migration/tests/browser/browser.ini b/browser/components/migration/tests/browser/browser.ini
new file mode 100644
index 000000000..94edfe7aa
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser.ini
@@ -0,0 +1,3 @@
+[browser_undo_notification.js]
+[browser_undo_notification_wording.js]
+[browser_undo_notification_multiple_dismissal.js]
diff --git a/browser/components/migration/tests/browser/browser_undo_notification.js b/browser/components/migration/tests/browser/browser_undo_notification.js
new file mode 100644
index 000000000..6c97922e0
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_undo_notification.js
@@ -0,0 +1,67 @@
+"use strict";
+
+let scope = {};
+Cu.import("resource:///modules/AutoMigrate.jsm", scope);
+let oldCanUndo = scope.AutoMigrate.canUndo;
+let oldUndo = scope.AutoMigrate.undo;
+registerCleanupFunction(function() {
+ scope.AutoMigrate.canUndo = oldCanUndo;
+ scope.AutoMigrate.undo = oldUndo;
+});
+
+const kExpectedNotificationId = "automigration-undo";
+
+add_task(function* autoMigrationUndoNotificationShows() {
+ let getNotification = browser =>
+ gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId);
+
+ scope.AutoMigrate.canUndo = () => true;
+ let undoCalled;
+ scope.AutoMigrate.undo = () => { undoCalled = true };
+ for (let url of ["about:newtab", "about:home"]) {
+ undoCalled = false;
+ // Can't use pushPrefEnv because of bug 1323779
+ Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser");
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false);
+ let browser = tab.linkedBrowser;
+ if (!getNotification(browser)) {
+ info(`Notification for ${url} not immediately present, waiting for it.`);
+ yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId);
+ }
+
+ ok(true, `Got notification for ${url}`);
+ let notification = getNotification(browser);
+ let notificationBox = notification.parentNode;
+ notification.querySelector("button.notification-button-default").click();
+ ok(!undoCalled, "Undo should not be called when clicking the default button");
+ is(notification, notificationBox._closedNotification, "Notification should be closing");
+ yield BrowserTestUtils.removeTab(tab);
+
+ undoCalled = false;
+ Services.prefs.setCharPref("browser.migrate.automigrate.browser", "chrome");
+ tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false);
+ browser = tab.linkedBrowser;
+ if (!getNotification(browser)) {
+ info(`Notification for ${url} not immediately present, waiting for it.`);
+ yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId);
+ }
+
+ ok(true, `Got notification for ${url}`);
+ notification = getNotification(browser);
+ notificationBox = notification.parentNode;
+ // Set up the survey:
+ yield SpecialPowers.pushPrefEnv({set: [
+ ["browser.migrate.automigrate.undo-survey", "https://example.com/?browser=%IMPORTEDBROWSER%"],
+ ["browser.migrate.automigrate.undo-survey-locales", "en-US"],
+ ]});
+ let tabOpenedPromise = BrowserTestUtils.waitForNewTab(gBrowser, "https://example.com/?browser=Google%20Chrome");
+ notification.querySelector("button:not(.notification-button-default)").click();
+ ok(undoCalled, "Undo should be called when clicking the non-default (Don't Keep) button");
+ is(notification, notificationBox._closedNotification, "Notification should be closing");
+ let surveyTab = yield tabOpenedPromise;
+ ok(surveyTab, "Should have opened a tab with a survey");
+ yield BrowserTestUtils.removeTab(surveyTab);
+ yield BrowserTestUtils.removeTab(tab);
+ }
+});
+
diff --git a/browser/components/migration/tests/browser/browser_undo_notification_multiple_dismissal.js b/browser/components/migration/tests/browser/browser_undo_notification_multiple_dismissal.js
new file mode 100644
index 000000000..90b5d0d08
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_undo_notification_multiple_dismissal.js
@@ -0,0 +1,122 @@
+"use strict";
+
+
+const kExpectedNotificationId = "automigration-undo";
+
+/**
+ * Pretend we can undo something, trigger a notification, pick the undo option,
+ * and verify that the notifications are all dismissed immediately.
+ */
+add_task(function* checkNotificationsDismissed() {
+ yield SpecialPowers.pushPrefEnv({set: [
+ ["browser.migrate.automigrate.enabled", true],
+ ["browser.migrate.automigrate.ui.enabled", true],
+ ]});
+ let getNotification = browser =>
+ gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId);
+
+ Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser");
+
+ let {guid, lastModified} = yield PlacesUtils.bookmarks.insert(
+ {title: "Some imported bookmark", parentGuid: PlacesUtils.bookmarks.toolbarGuid, url: "http://www.example.com"}
+ );
+
+ let testUndoData = {
+ visits: [],
+ bookmarks: [{guid, lastModified: lastModified.getTime()}],
+ logins: [],
+ };
+ let path = OS.Path.join(OS.Constants.Path.profileDir, "initialMigrationMetadata.jsonlz4");
+ registerCleanupFunction(() => {
+ return OS.File.remove(path, {ignoreAbsent: true});
+ });
+ yield OS.File.writeAtomic(path, JSON.stringify(testUndoData), {
+ encoding: "utf-8",
+ compression: "lz4",
+ tmpPath: path + ".tmp",
+ });
+
+ let firstTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", false);
+ if (!getNotification(firstTab.linkedBrowser)) {
+ info(`Notification not immediately present on first tab, waiting for it.`);
+ yield BrowserTestUtils.waitForNotificationBar(gBrowser, firstTab.linkedBrowser, kExpectedNotificationId);
+ }
+ let secondTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home", false);
+ if (!getNotification(secondTab.linkedBrowser)) {
+ info(`Notification not immediately present on second tab, waiting for it.`);
+ yield BrowserTestUtils.waitForNotificationBar(gBrowser, secondTab.linkedBrowser, kExpectedNotificationId);
+ }
+
+ // Create a listener for the removal in the first tab, and a listener for bookmarks removal,
+ // then click 'Don't keep' in the second tab, and verify that the notification is removed
+ // before we start removing bookmarks.
+ let haveRemovedBookmark = false;
+ let bmObserver;
+ let bookmarkRemovedPromise = new Promise(resolve => {
+ bmObserver = {
+ onItemRemoved(itemId, parentId, index, itemType, uri, removedGuid) {
+ if (guid == removedGuid) {
+ haveRemovedBookmark = true;
+ resolve();
+ }
+ },
+ };
+ PlacesUtils.bookmarks.addObserver(bmObserver, false);
+ registerCleanupFunction(() => PlacesUtils.bookmarks.removeObserver(bmObserver));
+ });
+
+ let firstTabNotificationRemovedPromise = new Promise(resolve => {
+ let notification = getNotification(firstTab.linkedBrowser);
+ // Save this reference because notification.parentNode will be null once it's removed.
+ let notificationBox = notification.parentNode;
+ let mut = new MutationObserver(mutations => {
+ // Yucky, but we have to detect either the removal via animation (with marginTop)
+ // or when the element is removed. We can't just detect the element being removed
+ // because this happens asynchronously (after the animation) and so it'd race
+ // with the rest of the undo happening.
+ for (let mutation of mutations) {
+ if (mutation.target == notification && mutation.attributeName == "style" &&
+ parseInt(notification.style.marginTop, 10) < 0) {
+ ok(!haveRemovedBookmark, "Should not have removed bookmark yet");
+ mut.disconnect();
+ resolve();
+ return;
+ }
+ if (mutation.target == notificationBox && mutation.removedNodes.length &&
+ mutation.removedNodes[0] == notification) {
+ ok(!haveRemovedBookmark, "Should not have removed bookmark yet");
+ mut.disconnect();
+ resolve();
+ return;
+ }
+ }
+ });
+ mut.observe(notification.parentNode, {childList: true});
+ mut.observe(notification, {attributes: true});
+ });
+
+ let prefResetPromise = new Promise(resolve => {
+ const kObservedPref = "browser.migrate.automigrate.browser";
+ let obs = () => {
+ Services.prefs.removeObserver(kObservedPref, obs);
+ ok(!Services.prefs.prefHasUserValue(kObservedPref),
+ "Pref should have been reset");
+ resolve();
+ };
+ Services.prefs.addObserver(kObservedPref, obs, false);
+ });
+
+ // Click "Don't keep" button:
+ let notificationToActivate = getNotification(secondTab.linkedBrowser);
+ notificationToActivate.querySelector("button:not(.notification-button-default)").click();
+ info("Waiting for notification to be removed in first (background) tab");
+ yield firstTabNotificationRemovedPromise;
+ info("Waiting for bookmark to be removed");
+ yield bookmarkRemovedPromise;
+ info("Waiting for prefs to be reset");
+ yield prefResetPromise;
+
+ info("Removing spare tabs");
+ yield BrowserTestUtils.removeTab(firstTab);
+ yield BrowserTestUtils.removeTab(secondTab);
+});
diff --git a/browser/components/migration/tests/browser/browser_undo_notification_wording.js b/browser/components/migration/tests/browser/browser_undo_notification_wording.js
new file mode 100644
index 000000000..f0a9ceec9
--- /dev/null
+++ b/browser/components/migration/tests/browser/browser_undo_notification_wording.js
@@ -0,0 +1,67 @@
+"use strict";
+
+let scope = {};
+Cu.import("resource:///modules/AutoMigrate.jsm", scope);
+let oldCanUndo = scope.AutoMigrate.canUndo;
+registerCleanupFunction(function() {
+ scope.AutoMigrate.canUndo = oldCanUndo;
+});
+
+const kExpectedNotificationId = "automigration-undo";
+
+add_task(function* autoMigrationUndoNotificationShows() {
+ let getNotification = browser =>
+ gBrowser.getNotificationBox(browser).getNotificationWithValue(kExpectedNotificationId);
+ let localizedVersionOf = str => {
+ if (str == "logins") {
+ return "passwords";
+ }
+ if (str == "visits") {
+ return "history";
+ }
+ return str;
+ };
+
+ scope.AutoMigrate.canUndo = () => true;
+ let url = "about:newtab";
+ Services.prefs.setCharPref("browser.migrate.automigrate.browser", "someunknownbrowser");
+ const kSubsets = [
+ ["bookmarks", "logins", "visits"],
+ ["bookmarks", "logins"],
+ ["bookmarks", "visits"],
+ ["logins", "visits"],
+ ["bookmarks"],
+ ["logins"],
+ ["visits"],
+ ];
+ const kAllItems = ["bookmarks", "logins", "visits"];
+ for (let subset of kSubsets) {
+ let state = new Map(subset.map(item => [item, [{}]]));
+ scope.AutoMigrate._setImportedItemPrefFromState(state);
+ let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url, false);
+ let browser = tab.linkedBrowser;
+
+ if (!getNotification(browser)) {
+ info(`Notification for ${url} not immediately present, waiting for it.`);
+ yield BrowserTestUtils.waitForNotificationBar(gBrowser, browser, kExpectedNotificationId);
+ }
+
+ ok(true, `Got notification for ${url}`);
+ let notification = getNotification(browser);
+ let notificationText = document.getAnonymousElementByAttribute(notification, "class", "messageText");
+ notificationText = notificationText.textContent;
+ for (let potentiallyImported of kAllItems) {
+ let localizedImportItem = localizedVersionOf(potentiallyImported);
+ if (subset.includes(potentiallyImported)) {
+ ok(notificationText.includes(localizedImportItem),
+ "Expected notification to contain " + localizedImportItem);
+ } else {
+ ok(!notificationText.includes(localizedImportItem),
+ "Expected notification not to contain " + localizedImportItem);
+ }
+ }
+
+ yield BrowserTestUtils.removeTab(tab);
+ }
+});
+
diff --git a/browser/components/migration/tests/marionette/manifest.ini b/browser/components/migration/tests/marionette/manifest.ini
new file mode 100644
index 000000000..3f404e724
--- /dev/null
+++ b/browser/components/migration/tests/marionette/manifest.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+run-if = buildapp == 'browser'
+
+[test_refresh_firefox.py]
+
diff --git a/browser/components/migration/tests/marionette/test_refresh_firefox.py b/browser/components/migration/tests/marionette/test_refresh_firefox.py
new file mode 100644
index 000000000..b348a3dcd
--- /dev/null
+++ b/browser/components/migration/tests/marionette/test_refresh_firefox.py
@@ -0,0 +1,416 @@
+import os
+import shutil
+
+from marionette_harness import MarionetteTestCase
+
+
+class TestFirefoxRefresh(MarionetteTestCase):
+ _username = "marionette-test-login"
+ _password = "marionette-test-password"
+ _bookmarkURL = "about:mozilla"
+ _bookmarkText = "Some bookmark from Marionette"
+
+ _cookieHost = "firefox-refresh.marionette-test.mozilla.org"
+ _cookiePath = "some/cookie/path"
+ _cookieName = "somecookie"
+ _cookieValue = "some cookie value"
+
+ _historyURL = "http://firefox-refresh.marionette-test.mozilla.org/"
+ _historyTitle = "Test visit for Firefox Reset"
+
+ _formHistoryFieldName = "some-very-unique-marionette-only-firefox-reset-field"
+ _formHistoryValue = "special-pumpkin-value"
+
+ _expectedURLs = ["about:robots", "about:mozilla"]
+
+ def savePassword(self):
+ self.runCode("""
+ let myLogin = new global.LoginInfo(
+ "test.marionette.mozilla.com",
+ "http://test.marionette.mozilla.com/some/form/",
+ null,
+ arguments[0],
+ arguments[1],
+ "username",
+ "password"
+ );
+ Services.logins.addLogin(myLogin)
+ """, script_args=[self._username, self._password])
+
+ def createBookmark(self):
+ self.marionette.execute_script("""
+ let url = arguments[0];
+ let title = arguments[1];
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder,
+ makeURI(url), 0, title);
+ """, script_args=[self._bookmarkURL, self._bookmarkText])
+
+ def createHistory(self):
+ error = self.runAsyncCode("""
+ // Copied from PlacesTestUtils, which isn't available in Marionette tests.
+ let didReturn;
+ PlacesUtils.asyncHistory.updatePlaces(
+ [{title: arguments[1], uri: makeURI(arguments[0]), visits: [{
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ visitDate: (Date.now() - 5000) * 1000,
+ referrerURI: makeURI("about:mozilla"),
+ }]
+ }],
+ {
+ handleError(resultCode, place) {
+ didReturn = true;
+ marionetteScriptFinished("Unexpected error in adding visit: " + resultCode);
+ },
+ handleResult() {},
+ handleCompletion() {
+ if (!didReturn) {
+ marionetteScriptFinished(false);
+ }
+ },
+ }
+ );
+ """, script_args=[self._historyURL, self._historyTitle])
+ if error:
+ print error
+
+ def createFormHistory(self):
+ error = self.runAsyncCode("""
+ let updateDefinition = {
+ op: "add",
+ fieldname: arguments[0],
+ value: arguments[1],
+ firstUsed: (Date.now() - 5000) * 1000,
+ };
+ let finished = false;
+ global.FormHistory.update(updateDefinition, {
+ handleError(error) {
+ finished = true;
+ marionetteScriptFinished(error);
+ },
+ handleCompletion() {
+ if (!finished) {
+ marionetteScriptFinished(false);
+ }
+ }
+ });
+ """, script_args=[self._formHistoryFieldName, self._formHistoryValue])
+ if error:
+ print error
+
+ def createCookie(self):
+ self.runCode("""
+ // Expire in 15 minutes:
+ let expireTime = Math.floor(Date.now() / 1000) + 15 * 60;
+ Services.cookies.add(arguments[0], arguments[1], arguments[2], arguments[3],
+ true, false, false, expireTime);
+ """, script_args=[self._cookieHost, self._cookiePath, self._cookieName, self._cookieValue])
+
+ def createSession(self):
+ self.runAsyncCode("""
+ const COMPLETE_STATE = Ci.nsIWebProgressListener.STATE_STOP +
+ Ci.nsIWebProgressListener.STATE_IS_NETWORK;
+ let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+ let expectedURLs = Array.from(arguments[0])
+ gBrowser.addTabsProgressListener({
+ onStateChange(browser, webprogress, request, flags, status) {
+ try {
+ request && request.QueryInterface(Ci.nsIChannel);
+ } catch (ex) {}
+ let uriLoaded = request.originalURI && request.originalURI.spec;
+ if ((flags & COMPLETE_STATE == COMPLETE_STATE) && uriLoaded &&
+ expectedURLs.includes(uriLoaded)) {
+ TabStateFlusher.flush(browser).then(function() {
+ expectedURLs.splice(expectedURLs.indexOf(uriLoaded), 1);
+ if (!expectedURLs.length) {
+ gBrowser.removeTabsProgressListener(this);
+ marionetteScriptFinished();
+ }
+ });
+ }
+ }
+ });
+ for (let url of expectedURLs) {
+ gBrowser.addTab(url);
+ }
+ """, script_args=[self._expectedURLs])
+
+ def checkPassword(self):
+ loginInfo = self.marionette.execute_script("""
+ let ary = Services.logins.findLogins({},
+ "test.marionette.mozilla.com",
+ "http://test.marionette.mozilla.com/some/form/",
+ null, {});
+ return ary.length ? ary : {username: "null", password: "null"};
+ """)
+ self.assertEqual(len(loginInfo), 1)
+ self.assertEqual(loginInfo[0]['username'], self._username)
+ self.assertEqual(loginInfo[0]['password'], self._password)
+
+ loginCount = self.marionette.execute_script("""
+ return Services.logins.getAllLogins().length;
+ """)
+ self.assertEqual(loginCount, 1, "No other logins are present")
+
+ def checkBookmark(self):
+ titleInBookmarks = self.marionette.execute_script("""
+ let url = arguments[0];
+ let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(makeURI(url), {}, {});
+ return bookmarkIds.length == 1 ? PlacesUtils.bookmarks.getItemTitle(bookmarkIds[0]) : "";
+ """, script_args=[self._bookmarkURL])
+ self.assertEqual(titleInBookmarks, self._bookmarkText)
+
+ def checkHistory(self):
+ historyResults = self.runAsyncCode("""
+ let placeInfos = [];
+ PlacesUtils.asyncHistory.getPlacesInfo(makeURI(arguments[0]), {
+ handleError(resultCode, place) {
+ placeInfos = null;
+ marionetteScriptFinished("Unexpected error in fetching visit: " + resultCode);
+ },
+ handleResult(placeInfo) {
+ placeInfos.push(placeInfo);
+ },
+ handleCompletion() {
+ if (placeInfos) {
+ if (!placeInfos.length) {
+ marionetteScriptFinished("No visits found");
+ } else {
+ marionetteScriptFinished(placeInfos);
+ }
+ }
+ },
+ });
+ """, script_args=[self._historyURL])
+ if type(historyResults) == str:
+ self.fail(historyResults)
+ return
+
+ historyCount = len(historyResults)
+ self.assertEqual(historyCount, 1, "Should have exactly 1 entry for URI, got %d" % historyCount)
+ if historyCount == 1:
+ self.assertEqual(historyResults[0]['title'], self._historyTitle)
+
+ def checkFormHistory(self):
+ formFieldResults = self.runAsyncCode("""
+ let results = [];
+ global.FormHistory.search(["value"], {fieldname: arguments[0]}, {
+ handleError(error) {
+ results = error;
+ },
+ handleResult(result) {
+ results.push(result);
+ },
+ handleCompletion() {
+ marionetteScriptFinished(results);
+ },
+ });
+ """, script_args=[self._formHistoryFieldName])
+ if type(formFieldResults) == str:
+ self.fail(formFieldResults)
+ return
+
+ formFieldResultCount = len(formFieldResults)
+ self.assertEqual(formFieldResultCount, 1, "Should have exactly 1 entry for this field, got %d" % formFieldResultCount)
+ if formFieldResultCount == 1:
+ self.assertEqual(formFieldResults[0]['value'], self._formHistoryValue)
+
+ formHistoryCount = self.runAsyncCode("""
+ let count;
+ let callbacks = {
+ handleResult: rv => count = rv,
+ handleCompletion() {
+ marionetteScriptFinished(count);
+ },
+ };
+ global.FormHistory.count({}, callbacks);
+ """)
+ self.assertEqual(formHistoryCount, 1, "There should be only 1 entry in the form history")
+
+ def checkCookie(self):
+ cookieInfo = self.runCode("""
+ try {
+ let cookieEnum = Services.cookies.getCookiesFromHost(arguments[0]);
+ let cookie = null;
+ while (cookieEnum.hasMoreElements()) {
+ let hostCookie = cookieEnum.getNext();
+ hostCookie.QueryInterface(Ci.nsICookie2);
+ // getCookiesFromHost returns any cookie from the BASE host.
+ if (hostCookie.rawHost != arguments[0])
+ continue;
+ if (cookie != null) {
+ return "more than 1 cookie! That shouldn't happen!";
+ }
+ cookie = hostCookie;
+ }
+ return {path: cookie.path, name: cookie.name, value: cookie.value};
+ } catch (ex) {
+ return "got exception trying to fetch cookie: " + ex;
+ }
+ """, script_args=[self._cookieHost])
+ if not isinstance(cookieInfo, dict):
+ self.fail(cookieInfo)
+ return
+ self.assertEqual(cookieInfo['path'], self._cookiePath)
+ self.assertEqual(cookieInfo['value'], self._cookieValue)
+ self.assertEqual(cookieInfo['name'], self._cookieName)
+
+ def checkSession(self):
+ tabURIs = self.runCode("""
+ return [... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec)
+ """)
+ self.assertSequenceEqual(tabURIs, ["about:welcomeback"])
+
+ tabURIs = self.runAsyncCode("""
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let fs = function() {
+ content.document.getElementById("errorTryAgain").click();
+ };
+ let {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
+ window.addEventListener("SSWindowStateReady", function testSSPostReset() {
+ window.removeEventListener("SSWindowStateReady", testSSPostReset, false);
+ Promise.all(gBrowser.browsers.map(b => TabStateFlusher.flush(b))).then(function() {
+ marionetteScriptFinished([... gBrowser.browsers].map(b => b.currentURI && b.currentURI.spec));
+ });
+ }, false);
+ mm.loadFrameScript("data:application/javascript,(" + fs.toString() + ")()", true);
+ """)
+ self.assertSequenceEqual(tabURIs, ["about:blank"] + self._expectedURLs)
+ pass
+
+ def checkProfile(self, hasMigrated=False):
+ self.checkPassword()
+ self.checkBookmark()
+ self.checkHistory()
+ self.checkFormHistory()
+ self.checkCookie()
+ if hasMigrated:
+ self.checkSession()
+
+ def createProfileData(self):
+ self.savePassword()
+ self.createBookmark()
+ self.createHistory()
+ self.createFormHistory()
+ self.createCookie()
+ self.createSession()
+
+ def setUpScriptData(self):
+ self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+ self.marionette.execute_script("""
+ global.LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+ global.profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].getService(Ci.nsIToolkitProfileService);
+ global.Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
+ global.FormHistory = Cu.import("resource://gre/modules/FormHistory.jsm", {}).FormHistory;
+ """, new_sandbox=False, sandbox='system')
+
+ def runCode(self, script, *args, **kwargs):
+ return self.marionette.execute_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
+
+ def runAsyncCode(self, script, *args, **kwargs):
+ return self.marionette.execute_async_script(script, new_sandbox=False, sandbox='system', *args, **kwargs)
+
+ def setUp(self):
+ MarionetteTestCase.setUp(self)
+ self.setUpScriptData()
+
+ self.reset_profile_path = None
+ self.desktop_backup_path = None
+
+ self.createProfileData()
+
+ def tearDown(self):
+ # Force yet another restart with a clean profile to disconnect from the
+ # profile and environment changes we've made, to leave a more or less
+ # blank slate for the next person.
+ self.marionette.restart(clean=True, in_app=False)
+ self.setUpScriptData()
+
+ # Super
+ MarionetteTestCase.tearDown(self)
+
+ # Some helpers to deal with removing a load of files
+ import errno, stat
+ def handleRemoveReadonly(func, path, exc):
+ excvalue = exc[1]
+ if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES:
+ os.chmod(path, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
+ func(path)
+ else:
+ raise
+
+ if self.desktop_backup_path:
+ shutil.rmtree(self.desktop_backup_path, ignore_errors=False, onerror=handleRemoveReadonly)
+
+ if self.reset_profile_path:
+ # Remove ourselves from profiles.ini
+ profileLeafName = os.path.basename(os.path.normpath(self.reset_profile_path))
+ self.runCode("""
+ let [salt, name] = arguments[0].split(".");
+ let profile = global.profSvc.getProfileByName(name);
+ profile.remove(false)
+ global.profSvc.flush();
+ """, script_args=[profileLeafName])
+ # And delete all the files.
+ shutil.rmtree(self.reset_profile_path, ignore_errors=False, onerror=handleRemoveReadonly)
+
+ def doReset(self):
+ self.runCode("""
+ // Ensure the current (temporary) profile is in profiles.ini:
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let profileName = "marionette-test-profile-" + Date.now();
+ let myProfile = global.profSvc.createProfile(profD, profileName);
+ global.profSvc.flush()
+
+ // Now add the reset parameters:
+ let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ let allMarionettePrefs = Services.prefs.getChildList("marionette.");
+ let prefObj = {};
+ for (let pref of allMarionettePrefs) {
+ let prefSuffix = pref.substr("marionette.".length);
+ let prefVal = global.Preferences.get(pref);
+ prefObj[prefSuffix] = prefVal;
+ }
+ let marionetteInfo = JSON.stringify(prefObj);
+ env.set("MOZ_MARIONETTE_PREF_STATE_ACROSS_RESTARTS", marionetteInfo);
+ env.set("MOZ_RESET_PROFILE_RESTART", "1");
+ env.set("XRE_PROFILE_PATH", arguments[0]);
+ env.set("XRE_PROFILE_NAME", profileName);
+ """, script_args=[self.marionette.instance.profile.profile])
+
+ profileLeafName = os.path.basename(os.path.normpath(self.marionette.instance.profile.profile))
+
+ # Now restart the browser to get it reset:
+ self.marionette.restart(clean=False, in_app=True)
+ self.setUpScriptData()
+
+ # Determine the new profile path (we'll need to remove it when we're done)
+ self.reset_profile_path = self.runCode("""
+ let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ return profD.path;
+ """)
+
+ # Determine the backup path
+ self.desktop_backup_path = self.runCode("""
+ let container;
+ try {
+ container = Services.dirsvc.get("Desk", Ci.nsIFile);
+ } catch (ex) {
+ container = Services.dirsvc.get("Home", Ci.nsIFile);
+ }
+ let bundle = Services.strings.createBundle("chrome://mozapps/locale/profile/profileSelection.properties");
+ let dirName = bundle.formatStringFromName("resetBackupDirectory", [Services.appinfo.name], 1);
+ container.append(dirName);
+ container.append(arguments[0]);
+ return container.path;
+ """, script_args = [profileLeafName])
+
+ self.assertTrue(os.path.isdir(self.reset_profile_path), "Reset profile path should be present")
+ self.assertTrue(os.path.isdir(self.desktop_backup_path), "Backup profile path should be present")
+
+ def testReset(self):
+ self.checkProfile()
+
+ self.doReset()
+
+ # Now check that we're doing OK...
+ self.checkProfile(hasMigrated=True)
diff --git a/browser/components/migration/tests/unit/.eslintrc.js b/browser/components/migration/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..ba65517f9
--- /dev/null
+++ b/browser/components/migration/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data
new file mode 100644
index 000000000..914149c71
--- /dev/null
+++ b/browser/components/migration/tests/unit/AppData/Local/Google/Chrome/User Data/Default/Login Data
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies
new file mode 100644
index 000000000..83d855cb3
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Default/Cookies
Binary files differ
diff --git a/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State
new file mode 100644
index 000000000..01b99455e
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Application Support/Google/Chrome/Local State
@@ -0,0 +1,22 @@
+{
+ "profile" : {
+ "info_cache" : {
+ "Default" : {
+ "active_time" : 1430950755.65137,
+ "is_using_default_name" : true,
+ "is_ephemeral" : false,
+ "is_omitted_from_profile_list" : false,
+ "user_name" : "",
+ "background_apps" : false,
+ "is_using_default_avatar" : true,
+ "avatar_icon" : "chrome://theme/IDR_PROFILE_AVATAR_0",
+ "name" : "Person 1"
+ }
+ },
+ "profiles_created" : 1,
+ "last_used" : "Default",
+ "last_active_profiles" : [
+ "Default"
+ ]
+ }
+}
diff --git a/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist
new file mode 100644
index 000000000..40783c7b1
--- /dev/null
+++ b/browser/components/migration/tests/unit/Library/Safari/Bookmarks.plist
Binary files differ
diff --git a/browser/components/migration/tests/unit/head_migration.js b/browser/components/migration/tests/unit/head_migration.js
new file mode 100644
index 000000000..d3c258d54
--- /dev/null
+++ b/browser/components/migration/tests/unit/head_migration.js
@@ -0,0 +1,69 @@
+"use strict";
+
+/* exported gProfD, promiseMigration, registerFakePath */
+
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.importGlobalProperties([ "URL" ]);
+
+Cu.import("resource:///modules/MigrationUtils.jsm");
+Cu.import("resource://gre/modules/LoginHelper.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://testing-common/TestUtils.jsm");
+Cu.import("resource://testing-common/PlacesTestUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+// Initialize profile.
+var gProfD = do_get_profile();
+
+Cu.import("resource://testing-common/AppInfo.jsm"); /* globals updateAppInfo */
+updateAppInfo();
+
+/**
+ * Migrates the requested resource and waits for the migration to be complete.
+ */
+function promiseMigration(migrator, resourceType, aProfile = null) {
+ // Ensure resource migration is available.
+ let availableSources = migrator.getMigrateData(aProfile, false);
+ Assert.ok((availableSources & resourceType) > 0, "Resource supported by migrator");
+
+ return new Promise (resolve => {
+ Services.obs.addObserver(function onMigrationEnded() {
+ Services.obs.removeObserver(onMigrationEnded, "Migration:Ended");
+ resolve();
+ }, "Migration:Ended", false);
+
+ migrator.migrate(resourceType, null, aProfile);
+ });
+}
+
+/**
+ * Replaces a directory service entry with a given nsIFile.
+ */
+function registerFakePath(key, file) {
+ // Register our own provider for the Library directory.
+ let provider = {
+ getFile(prop, persistent) {
+ persistent.value = true;
+ if (prop == key) {
+ return file;
+ }
+ throw Cr.NS_ERROR_FAILURE;
+ },
+ QueryInterface: XPCOMUtils.generateQI([ Ci.nsIDirectoryServiceProvider ])
+ };
+ Services.dirsvc.QueryInterface(Ci.nsIDirectoryService)
+ .registerProvider(provider);
+ do_register_cleanup(() => {
+ Services.dirsvc.QueryInterface(Ci.nsIDirectoryService)
+ .unregisterProvider(provider);
+ });
+}
diff --git a/browser/components/migration/tests/unit/test_Chrome_cookies.js b/browser/components/migration/tests/unit/test_Chrome_cookies.js
new file mode 100644
index 000000000..006693951
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_cookies.js
@@ -0,0 +1,51 @@
+"use strict";
+
+Cu.import("resource://gre/modules/ForgetAboutSite.jsm");
+
+add_task(function* () {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+ let migrator = MigrationUtils.getMigrator("chrome");
+
+ Assert.ok(migrator.sourceExists, "Sanity check the source exists");
+
+ const COOKIE = {
+ expiry: 2145934800,
+ host: "unencryptedcookie.invalid",
+ isHttpOnly: false,
+ isSession: false,
+ name: "testcookie",
+ path: "/",
+ value: "testvalue",
+ };
+
+ // Sanity check.
+ Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0,
+ "There are no cookies initially");
+
+ const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+ };
+
+ // Migrate unencrypted cookies.
+ yield promiseMigration(migrator, MigrationUtils.resourceTypes.COOKIES, PROFILE);
+
+ Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 1,
+ "Migrated the expected number of unencrypted cookies");
+ Assert.equal(Services.cookies.countCookiesFromHost("encryptedcookie.invalid"), 0,
+ "Migrated the expected number of encrypted cookies");
+
+ // Now check the cookie details.
+ let enumerator = Services.cookies.getCookiesFromHost(COOKIE.host, {});
+ Assert.ok(enumerator.hasMoreElements(), "Cookies available");
+ let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+
+ for (let prop of Object.keys(COOKIE)) {
+ Assert.equal(foundCookie[prop], COOKIE[prop], "Check cookie " + prop);
+ }
+
+ // Cleanup.
+ ForgetAboutSite.removeDataFromDomain(COOKIE.host);
+ Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0,
+ "There are no cookies after cleanup");
+});
diff --git a/browser/components/migration/tests/unit/test_Chrome_passwords.js b/browser/components/migration/tests/unit/test_Chrome_passwords.js
new file mode 100644
index 000000000..49147bd61
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Chrome_passwords.js
@@ -0,0 +1,219 @@
+"use strict";
+
+Cu.import("resource://gre/modules/OSCrypto.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PROFILE = {
+ id: "Default",
+ name: "Person 1",
+};
+
+const TEST_LOGINS = [
+ {
+ id: 1, // id of the row in the chrome login db
+ username: "username",
+ password: "password",
+ hostname: "https://c9.io",
+ formSubmitURL: "https://c9.io",
+ httpRealm: null,
+ usernameField: "inputEmail",
+ passwordField: "inputPassword",
+ timeCreated: 1437418416037,
+ timePasswordChanged: 1437418416037,
+ timesUsed: 1,
+ },
+ {
+ id: 2,
+ username: "username@gmail.com",
+ password: "password2",
+ hostname: "https://accounts.google.com",
+ formSubmitURL: "https://accounts.google.com",
+ httpRealm: null,
+ usernameField: "Email",
+ passwordField: "Passwd",
+ timeCreated: 1437418446598,
+ timePasswordChanged: 1437418446598,
+ timesUsed: 6,
+ },
+ {
+ id: 3,
+ username: "username",
+ password: "password3",
+ hostname: "https://www.facebook.com",
+ formSubmitURL: "https://www.facebook.com",
+ httpRealm: null,
+ usernameField: "email",
+ passwordField: "pass",
+ timeCreated: 1437418478851,
+ timePasswordChanged: 1437418478851,
+ timesUsed: 1,
+ },
+ {
+ id: 4,
+ username: "user",
+ password: "password",
+ hostname: "http://httpbin.org",
+ formSubmitURL: null,
+ httpRealm: "me@kennethreitz.com", // Digest auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787462368,
+ timePasswordChanged: 1437787462368,
+ timesUsed: 1,
+ },
+ {
+ id: 5,
+ username: "buser",
+ password: "bpassword",
+ hostname: "http://httpbin.org",
+ formSubmitURL: null,
+ httpRealm: "Fake Realm", // Basic auth.
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1437787539233,
+ timePasswordChanged: 1437787539233,
+ timesUsed: 1,
+ },
+];
+
+var crypto = new OSCrypto();
+var dbConn;
+
+function promiseSetPassword(login) {
+ return new Promise((resolve, reject) => {
+ let stmt = dbConn.createAsyncStatement(`
+ UPDATE logins
+ SET password_value = :password_value
+ WHERE rowid = :rowid
+ `);
+ let passwordValue = crypto.stringToArray(crypto.encryptData(login.password));
+ stmt.bindBlobByName("password_value", passwordValue, passwordValue.length);
+ stmt.params.rowid = login.id;
+
+ stmt.executeAsync({
+ handleError(aError) {
+ reject("Error with the query: " + aError.message);
+ },
+
+ handleCompletion(aReason) {
+ if (aReason === Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+ resolve();
+ } else {
+ reject("Query has failed: " + aReason);
+ }
+ },
+ });
+ stmt.finalize();
+ });
+}
+
+function checkLoginsAreEqual(passwordManagerLogin, chromeLogin, id) {
+ passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
+
+ Assert.equal(passwordManagerLogin.username, chromeLogin.username,
+ "The two logins ID " + id + " have the same username");
+ Assert.equal(passwordManagerLogin.password, chromeLogin.password,
+ "The two logins ID " + id + " have the same password");
+ Assert.equal(passwordManagerLogin.hostname, chromeLogin.hostname,
+ "The two logins ID " + id + " have the same hostname");
+ Assert.equal(passwordManagerLogin.formSubmitURL, chromeLogin.formSubmitURL,
+ "The two logins ID " + id + " have the same formSubmitURL");
+ Assert.equal(passwordManagerLogin.httpRealm, chromeLogin.httpRealm,
+ "The two logins ID " + id + " have the same httpRealm");
+ Assert.equal(passwordManagerLogin.usernameField, chromeLogin.usernameField,
+ "The two logins ID " + id + " have the same usernameElement");
+ Assert.equal(passwordManagerLogin.passwordField, chromeLogin.passwordField,
+ "The two logins ID " + id + " have the same passwordElement");
+ Assert.equal(passwordManagerLogin.timeCreated, chromeLogin.timeCreated,
+ "The two logins ID " + id + " have the same timeCreated");
+ Assert.equal(passwordManagerLogin.timePasswordChanged, chromeLogin.timePasswordChanged,
+ "The two logins ID " + id + " have the same timePasswordChanged");
+ Assert.equal(passwordManagerLogin.timesUsed, chromeLogin.timesUsed,
+ "The two logins ID " + id + " have the same timesUsed");
+}
+
+function generateDifferentLogin(login) {
+ let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+
+ newLogin.init(login.hostname, login.formSubmitURL, null,
+ login.username, login.password + 1, login.usernameField + 1,
+ login.passwordField + 1);
+ newLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ newLogin.timeCreated = login.timeCreated + 1;
+ newLogin.timePasswordChanged = login.timePasswordChanged + 1;
+ newLogin.timesUsed = login.timesUsed + 1;
+ return newLogin;
+}
+
+add_task(function* setup() {
+ let loginDataFile = do_get_file("AppData/Local/Google/Chrome/User Data/Default/Login Data");
+ dbConn = Services.storage.openUnsharedDatabase(loginDataFile);
+ registerFakePath("LocalAppData", do_get_file("AppData/Local/"));
+
+ do_register_cleanup(() => {
+ Services.logins.removeAllLogins();
+ dbConn.asyncClose();
+ crypto.finalize();
+ });
+});
+
+add_task(function* test_importIntoEmptyDB() {
+ for (let login of TEST_LOGINS) {
+ yield promiseSetPassword(login);
+ }
+
+ let migrator = MigrationUtils.getMigrator("chrome");
+ Assert.ok(migrator.sourceExists, "Sanity check the source exists");
+
+ let logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, 0, "There are no logins initially");
+
+ // Migrate the logins.
+ yield promiseMigration(migrator, MigrationUtils.resourceTypes.PASSWORDS, PROFILE);
+
+ logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, TEST_LOGINS.length, "Check login count after importing the data");
+ Assert.equal(logins.length, MigrationUtils._importQuantities.logins,
+ "Check telemetry matches the actual import.");
+
+ for (let i = 0; i < TEST_LOGINS.length; i++) {
+ checkLoginsAreEqual(logins[i], TEST_LOGINS[i], i + 1);
+ }
+});
+
+// Test that existing logins for the same primary key don't get overwritten
+add_task(function* test_importExistingLogins() {
+ let migrator = MigrationUtils.getMigrator("chrome");
+ Assert.ok(migrator.sourceExists, "Sanity check the source exists");
+
+ Services.logins.removeAllLogins();
+ let logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, 0, "There are no logins after removing all of them");
+
+ let newLogins = [];
+
+ // Create 3 new logins that are different but where the key properties are still the same.
+ for (let i = 0; i < 3; i++) {
+ newLogins.push(generateDifferentLogin(TEST_LOGINS[i]));
+ Services.logins.addLogin(newLogins[i]);
+ }
+
+ logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, newLogins.length, "Check login count after the insertion");
+
+ for (let i = 0; i < newLogins.length; i++) {
+ checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
+ }
+ // Migrate the logins.
+ yield promiseMigration(migrator, MigrationUtils.resourceTypes.PASSWORDS, PROFILE);
+
+ logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, TEST_LOGINS.length,
+ "Check there are still the same number of logins after re-importing the data");
+ Assert.equal(logins.length, MigrationUtils._importQuantities.logins,
+ "Check telemetry matches the actual import.");
+
+ for (let i = 0; i < newLogins.length; i++) {
+ checkLoginsAreEqual(logins[i], newLogins[i], i + 1);
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_Edge_availability.js b/browser/components/migration/tests/unit/test_Edge_availability.js
new file mode 100644
index 000000000..dba0e27bb
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_availability.js
@@ -0,0 +1,20 @@
+"use strict";
+
+const EDGE_AVAILABLE_MIGRATIONS =
+ MigrationUtils.resourceTypes.COOKIES |
+ MigrationUtils.resourceTypes.BOOKMARKS |
+ MigrationUtils.resourceTypes.HISTORY |
+ MigrationUtils.resourceTypes.PASSWORDS;
+
+add_task(function* () {
+ let migrator = MigrationUtils.getMigrator("edge");
+ Cu.import("resource://gre/modules/AppConstants.jsm");
+ Assert.equal(!!(migrator && migrator.sourceExists), AppConstants.isPlatformAndVersionAtLeast("win", "10"),
+ "Edge should be available for migration if and only if we're on Win 10+");
+ if (migrator) {
+ let migratableData = migrator.getMigrateData(null, false);
+ Assert.equal(migratableData, EDGE_AVAILABLE_MIGRATIONS,
+ "All the data types we expect should be available");
+ }
+});
+
diff --git a/browser/components/migration/tests/unit/test_Edge_db_migration.js b/browser/components/migration/tests/unit/test_Edge_db_migration.js
new file mode 100644
index 000000000..56ff612d5
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -0,0 +1,471 @@
+"use strict";
+
+Cu.import("resource://gre/modules/ctypes.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+let eseBackStage = Cu.import("resource:///modules/ESEDBReader.jsm");
+let ESE = eseBackStage.ESE;
+let KERNEL = eseBackStage.KERNEL;
+let gLibs = eseBackStage.gLibs;
+let COLUMN_TYPES = eseBackStage.COLUMN_TYPES;
+let declareESEFunction = eseBackStage.declareESEFunction;
+let loadLibraries = eseBackStage.loadLibraries;
+
+let gESEInstanceCounter = 1;
+
+ESE.JET_COLUMNCREATE_W = new ctypes.StructType("JET_COLUMNCREATE_W", [
+ {"cbStruct": ctypes.unsigned_long},
+ {"szColumnName": ESE.JET_PCWSTR},
+ {"coltyp": ESE.JET_COLTYP },
+ {"cbMax": ctypes.unsigned_long },
+ {"grbit": ESE.JET_GRBIT },
+ {"pvDefault": ctypes.voidptr_t},
+ {"cbDefault": ctypes.unsigned_long },
+ {"cp": ctypes.unsigned_long },
+ {"columnid": ESE.JET_COLUMNID},
+ {"err": ESE.JET_ERR},
+]);
+
+function createColumnCreationWrapper({name, type, cbMax}) {
+ // We use a wrapper object because we need to be sure the JS engine won't GC
+ // data that we're "only" pointing to.
+ let wrapper = {};
+ wrapper.column = new ESE.JET_COLUMNCREATE_W();
+ wrapper.column.cbStruct = ESE.JET_COLUMNCREATE_W.size;
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ wrapper.name = new wchar_tArray(name.length + 1);
+ wrapper.name.value = String(name);
+ wrapper.column.szColumnName = wrapper.name;
+ wrapper.column.coltyp = type;
+ let fallback = 0;
+ switch (type) {
+ case COLUMN_TYPES.JET_coltypText:
+ fallback = 255;
+ // Intentional fall-through
+ case COLUMN_TYPES.JET_coltypLongText:
+ wrapper.column.cbMax = cbMax || fallback || 64 * 1024;
+ break;
+ case COLUMN_TYPES.JET_coltypGUID:
+ wrapper.column.cbMax = 16;
+ break;
+ case COLUMN_TYPES.JET_coltypBit:
+ wrapper.column.cbMax = 1;
+ break;
+ case COLUMN_TYPES.JET_coltypLongLong:
+ wrapper.column.cbMax = 8;
+ break;
+ default:
+ throw new Error("Unknown column type!");
+ }
+
+ wrapper.column.columnid = new ESE.JET_COLUMNID();
+ wrapper.column.grbit = 0;
+ wrapper.column.pvDefault = null;
+ wrapper.column.cbDefault = 0;
+ wrapper.column.cp = 0;
+
+ return wrapper;
+}
+
+// "forward declarations" of indexcreate and setinfo structs, which we don't use.
+ESE.JET_INDEXCREATE = new ctypes.StructType("JET_INDEXCREATE");
+ESE.JET_SETINFO = new ctypes.StructType("JET_SETINFO");
+
+ESE.JET_TABLECREATE_W = new ctypes.StructType("JET_TABLECREATE_W", [
+ {"cbStruct": ctypes.unsigned_long},
+ {"szTableName": ESE.JET_PCWSTR},
+ {"szTemplateTableName": ESE.JET_PCWSTR},
+ {"ulPages": ctypes.unsigned_long},
+ {"ulDensity": ctypes.unsigned_long},
+ {"rgcolumncreate": ESE.JET_COLUMNCREATE_W.ptr},
+ {"cColumns": ctypes.unsigned_long},
+ {"rgindexcreate": ESE.JET_INDEXCREATE.ptr},
+ {"cIndexes": ctypes.unsigned_long},
+ {"grbit": ESE.JET_GRBIT},
+ {"tableid": ESE.JET_TABLEID},
+ {"cCreated": ctypes.unsigned_long},
+]);
+
+function createTableCreationWrapper(tableName, columns) {
+ let wrapper = {};
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ wrapper.name = new wchar_tArray(tableName.length + 1);
+ wrapper.name.value = String(tableName);
+ wrapper.table = new ESE.JET_TABLECREATE_W();
+ wrapper.table.cbStruct = ESE.JET_TABLECREATE_W.size;
+ wrapper.table.szTableName = wrapper.name;
+ wrapper.table.szTemplateTableName = null;
+ wrapper.table.ulPages = 1;
+ wrapper.table.ulDensity = 0;
+ let columnArrayType = ESE.JET_COLUMNCREATE_W.array(columns.length);
+ wrapper.columnAry = new columnArrayType();
+ wrapper.table.rgcolumncreate = wrapper.columnAry.addressOfElement(0);
+ wrapper.table.cColumns = columns.length;
+ wrapper.columns = [];
+ for (let i = 0; i < columns.length; i++) {
+ let column = columns[i];
+ let columnWrapper = createColumnCreationWrapper(column);
+ wrapper.columnAry.addressOfElement(i).contents = columnWrapper.column;
+ wrapper.columns.push(columnWrapper);
+ }
+ wrapper.table.rgindexcreate = null;
+ wrapper.table.cIndexes = 0;
+ return wrapper;
+}
+
+function convertValueForWriting(value, valueType) {
+ let buffer;
+ let valueOfValueType = ctypes.UInt64.lo(valueType);
+ switch (valueOfValueType) {
+ case COLUMN_TYPES.JET_coltypLongLong:
+ if (value instanceof Date) {
+ buffer = new KERNEL.FILETIME();
+ let sysTime = new KERNEL.SYSTEMTIME();
+ sysTime.wYear = value.getUTCFullYear();
+ sysTime.wMonth = value.getUTCMonth() + 1;
+ sysTime.wDay = value.getUTCDate();
+ sysTime.wHour = value.getUTCHours();
+ sysTime.wMinute = value.getUTCMinutes();
+ sysTime.wSecond = value.getUTCSeconds();
+ sysTime.wMilliseconds = value.getUTCMilliseconds();
+ let rv = KERNEL.SystemTimeToFileTime(sysTime.address(), buffer.address());
+ if (!rv) {
+ throw new Error("Failed to get FileTime.");
+ }
+ return [buffer, KERNEL.FILETIME.size];
+ }
+ throw new Error("Unrecognized value for longlong column");
+ case COLUMN_TYPES.JET_coltypLongText:
+ let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
+ buffer = new wchar_tArray(value.length + 1);
+ buffer.value = String(value);
+ return [buffer, buffer.length * 2];
+ case COLUMN_TYPES.JET_coltypBit:
+ buffer = new ctypes.uint8_t();
+ // Bizarre boolean values, but whatever:
+ buffer.value = value ? 255 : 0;
+ return [buffer, 1];
+ case COLUMN_TYPES.JET_coltypGUID:
+ let byteArray = ctypes.ArrayType(ctypes.uint8_t);
+ buffer = new byteArray(16);
+ let j = 0;
+ for (let i = 0; i < value.length; i++) {
+ if (!(/[0-9a-f]/i).test(value[i])) {
+ continue;
+ }
+ let byteAsHex = value.substr(i, 2);
+ buffer[j++] = parseInt(byteAsHex, 16);
+ i++;
+ }
+ return [buffer, 16];
+ }
+
+ throw new Error("Unknown type " + valueType);
+}
+
+let initializedESE = false;
+
+let eseDBWritingHelpers = {
+ setupDB(dbFile, tableName, columns, rows) {
+ if (!initializedESE) {
+ initializedESE = true;
+ loadLibraries();
+
+ KERNEL.SystemTimeToFileTime = gLibs.kernel.declare("SystemTimeToFileTime",
+ ctypes.default_abi, ctypes.bool, KERNEL.SYSTEMTIME.ptr, KERNEL.FILETIME.ptr);
+
+ declareESEFunction("CreateDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
+ ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT);
+ declareESEFunction("CreateTableColumnIndexW", ESE.JET_SESID, ESE.JET_DBID,
+ ESE.JET_TABLECREATE_W.ptr);
+ declareESEFunction("BeginTransaction", ESE.JET_SESID);
+ declareESEFunction("CommitTransaction", ESE.JET_SESID, ESE.JET_GRBIT);
+ declareESEFunction("PrepareUpdate", ESE.JET_SESID, ESE.JET_TABLEID,
+ ctypes.unsigned_long);
+ declareESEFunction("Update", ESE.JET_SESID, ESE.JET_TABLEID,
+ ctypes.voidptr_t, ctypes.unsigned_long,
+ ctypes.unsigned_long.ptr);
+ declareESEFunction("SetColumn", ESE.JET_SESID, ESE.JET_TABLEID,
+ ESE.JET_COLUMNID, ctypes.voidptr_t,
+ ctypes.unsigned_long, ESE.JET_GRBIT, ESE.JET_SETINFO.ptr);
+ ESE.SetSystemParameterW(null, 0, 64 /* JET_paramDatabasePageSize*/,
+ 8192, null);
+ }
+
+ let rootPath = dbFile.parent.path + "\\";
+ let logPath = rootPath + "LogFiles\\";
+
+ try {
+ this._instanceId = new ESE.JET_INSTANCE();
+ ESE.CreateInstanceW(this._instanceId.address(),
+ "firefox-dbwriter-" + (gESEInstanceCounter++));
+ this._instanceCreated = true;
+
+ ESE.SetSystemParameterW(this._instanceId.address(), 0,
+ 0 /* JET_paramSystemPath*/, 0, rootPath);
+ ESE.SetSystemParameterW(this._instanceId.address(), 0,
+ 1 /* JET_paramTempPath */, 0, rootPath);
+ ESE.SetSystemParameterW(this._instanceId.address(), 0,
+ 2 /* JET_paramLogFilePath*/, 0, logPath);
+ // Shouldn't try to call JetTerm if the following call fails.
+ this._instanceCreated = false;
+ ESE.Init(this._instanceId.address());
+ this._instanceCreated = true;
+ this._sessionId = new ESE.JET_SESID();
+ ESE.BeginSessionW(this._instanceId, this._sessionId.address(), null,
+ null);
+ this._sessionCreated = true;
+
+ this._dbId = new ESE.JET_DBID();
+ this._dbPath = rootPath + "spartan.edb";
+ ESE.CreateDatabaseW(this._sessionId, this._dbPath, null,
+ this._dbId.address(), 0);
+ this._opened = this._attached = true;
+
+ let tableCreationWrapper = createTableCreationWrapper(tableName, columns);
+ ESE.CreateTableColumnIndexW(this._sessionId, this._dbId,
+ tableCreationWrapper.table.address());
+ this._tableId = tableCreationWrapper.table.tableid;
+
+ let columnIdMap = new Map();
+ if (rows.length) {
+ // Iterate over the struct we passed into ESENT because they have the
+ // created column ids.
+ let columnCount = ctypes.UInt64.lo(tableCreationWrapper.table.cColumns);
+ let columnsPassed = tableCreationWrapper.table.rgcolumncreate;
+ for (let i = 0; i < columnCount; i++) {
+ let column = columnsPassed.contents;
+ columnIdMap.set(column.szColumnName.readString(), column);
+ columnsPassed = columnsPassed.increment();
+ }
+ ESE.ManualMove(this._sessionId, this._tableId,
+ -2147483648 /* JET_MoveFirst */, 0);
+ ESE.BeginTransaction(this._sessionId);
+ for (let row of rows) {
+ ESE.PrepareUpdate(this._sessionId, this._tableId, 0 /* JET_prepInsert */);
+ for (let columnName in row) {
+ let col = columnIdMap.get(columnName);
+ let colId = col.columnid;
+ let [val, valSize] = convertValueForWriting(row[columnName], col.coltyp);
+ /* JET_bitSetOverwriteLV */
+ ESE.SetColumn(this._sessionId, this._tableId, colId, val.address(), valSize, 4, null);
+ }
+ let actualBookmarkSize = new ctypes.unsigned_long();
+ ESE.Update(this._sessionId, this._tableId, null, 0, actualBookmarkSize.address());
+ }
+ ESE.CommitTransaction(this._sessionId, 0 /* JET_bitWaitLastLevel0Commit */);
+ }
+ } finally {
+ try {
+ this._close();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ _close() {
+ if (this._tableId) {
+ ESE.FailSafeCloseTable(this._sessionId, this._tableId);
+ delete this._tableId;
+ }
+ if (this._opened) {
+ ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
+ this._opened = false;
+ }
+ if (this._attached) {
+ ESE.FailSafeDetachDatabaseW(this._sessionId, this._dbPath);
+ this._attached = false;
+ }
+ if (this._sessionCreated) {
+ ESE.FailSafeEndSession(this._sessionId, 0);
+ this._sessionCreated = false;
+ }
+ if (this._instanceCreated) {
+ ESE.FailSafeTerm(this._instanceId);
+ this._instanceCreated = false;
+ }
+ },
+};
+
+add_task(function*() {
+ let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ tempFile.append("fx-xpcshell-edge-db");
+ tempFile.createUnique(tempFile.DIRECTORY_TYPE, 0o600);
+
+ let db = tempFile.clone();
+ db.append("spartan.edb");
+
+ let logs = tempFile.clone();
+ logs.append("LogFiles");
+ logs.create(tempFile.DIRECTORY_TYPE, 0o600);
+
+ let creationDate = new Date(Date.now() - 5000);
+ const kEdgeMenuParent = "62d07e2b-5f0d-4e41-8426-5f5ec9717beb";
+ let itemsInDB = [
+ {
+ URL: "http://www.mozilla.org/",
+ Title: "Mozilla",
+ DateUpdated: new Date(creationDate.valueOf() + 100),
+ ItemId: "1c00c10a-15f6-4618-92dd-22575102a4da",
+ ParentId: kEdgeMenuParent,
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "Folder",
+ DateUpdated: new Date(creationDate.valueOf() + 200),
+ ItemId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: false,
+ },
+ {
+ Title: "Item in folder",
+ URL: "http://www.iteminfolder.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 300),
+ ItemId: "c295ddaf-04a1-424a-866c-0ebde011e7c8",
+ ParentId: "564b21f2-05d6-4f7d-8499-304d00ccc3aa",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "Deleted folder",
+ DateUpdated: new Date(creationDate.valueOf() + 400),
+ ItemId: "a547573c-4d4d-4406-a736-5b5462d93bca",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: true,
+ },
+ {
+ Title: "Deleted item",
+ URL: "http://www.deleteditem.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 500),
+ ItemId: "37a574bb-b44b-4bbc-a414-908615536435",
+ ParentId: kEdgeMenuParent,
+ IsFolder: false,
+ IsDeleted: true,
+ },
+ {
+ Title: "Item in deleted folder (should be in root)",
+ URL: "http://www.itemindeletedfolder.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 600),
+ ItemId: "74dd1cc3-4c5d-471f-bccc-7bc7c72fa621",
+ ParentId: "a547573c-4d4d-4406-a736-5b5462d93bca",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ {
+ Title: "_Favorites_Bar_",
+ DateUpdated: new Date(creationDate.valueOf() + 700),
+ ItemId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
+ ParentId: kEdgeMenuParent,
+ IsFolder: true,
+ IsDeleted: false,
+ },
+ {
+ Title: "Item in favorites bar",
+ URL: "http://www.iteminfavoritesbar.org/",
+ DateUpdated: new Date(creationDate.valueOf() + 800),
+ ItemId: "9f2b1ff8-b651-46cf-8f41-16da8bcb6791",
+ ParentId: "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf",
+ IsFolder: false,
+ IsDeleted: false,
+ },
+ ];
+ eseDBWritingHelpers.setupDB(db, "Favorites", [
+ {type: COLUMN_TYPES.JET_coltypLongText, name: "URL", cbMax: 4096},
+ {type: COLUMN_TYPES.JET_coltypLongText, name: "Title", cbMax: 4096},
+ {type: COLUMN_TYPES.JET_coltypLongLong, name: "DateUpdated"},
+ {type: COLUMN_TYPES.JET_coltypGUID, name: "ItemId"},
+ {type: COLUMN_TYPES.JET_coltypBit, name: "IsDeleted"},
+ {type: COLUMN_TYPES.JET_coltypBit, name: "IsFolder"},
+ {type: COLUMN_TYPES.JET_coltypGUID, name: "ParentId"},
+ ], itemsInDB);
+
+ let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=edge"]
+ .createInstance(Ci.nsIBrowserProfileMigrator);
+ let bookmarksMigrator = migrator.wrappedJSObject.getESEMigratorForTesting(db);
+ Assert.ok(bookmarksMigrator.exists, "Should recognize table we just created");
+
+ let source = MigrationUtils.getLocalizedString("sourceNameEdge");
+ let sourceLabel = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
+
+ let seenBookmarks = [];
+ let bookmarkObserver = {
+ onItemAdded(itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid) {
+ if (title.startsWith("Deleted")) {
+ ok(false, "Should not see deleted items being bookmarked!");
+ }
+ seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid});
+ },
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemRemoved() {},
+ onItemChanged() {},
+ onItemVisited() {},
+ onItemMoved() {},
+ };
+ PlacesUtils.bookmarks.addObserver(bookmarkObserver, false);
+
+ let migrateResult = yield new Promise(resolve => bookmarksMigrator.migrate(resolve)).catch(ex => {
+ Cu.reportError(ex);
+ Assert.ok(false, "Got an exception trying to migrate data! " + ex);
+ return false;
+ });
+ PlacesUtils.bookmarks.removeObserver(bookmarkObserver);
+ Assert.ok(migrateResult, "Migration should succeed");
+ Assert.equal(seenBookmarks.length, 7, "Should have seen 7 items being bookmarked.");
+ Assert.equal(seenBookmarks.filter(bm => bm.title != sourceLabel).length,
+ MigrationUtils._importQuantities.bookmarks,
+ "Telemetry should have items except for 'From Microsoft Edge' folders");
+
+ let menuParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.menuGuid);
+ Assert.equal(menuParents.length, 1, "Should have a single folder added to the menu");
+ let toolbarParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid);
+ Assert.equal(toolbarParents.length, 1, "Should have a single item added to the toolbar");
+ let menuParentGuid = menuParents[0].itemGuid;
+ let toolbarParentGuid = toolbarParents[0].itemGuid;
+
+ let expectedTitlesInMenu = itemsInDB.filter(item => item.ParentId == kEdgeMenuParent).map(item => item.Title);
+ // Hacky, but seems like much the simplest way:
+ expectedTitlesInMenu.push("Item in deleted folder (should be in root)");
+ let expectedTitlesInToolbar = itemsInDB.filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf").map(item => item.Title);
+
+ let edgeNameStr = MigrationUtils.getLocalizedString("sourceNameEdge");
+ let importParentFolderName = MigrationUtils.getLocalizedString("importedBookmarksFolder", [edgeNameStr]);
+
+ for (let bookmark of seenBookmarks) {
+ let shouldBeInMenu = expectedTitlesInMenu.includes(bookmark.title);
+ let shouldBeInToolbar = expectedTitlesInToolbar.includes(bookmark.title);
+ if (bookmark.title == "Folder" || bookmark.title == importParentFolderName) {
+ Assert.equal(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Bookmark " + bookmark.title + " should be a folder");
+ } else {
+ Assert.notEqual(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Bookmark " + bookmark.title + " should not be a folder");
+ }
+
+ if (shouldBeInMenu) {
+ Assert.equal(bookmark.parentGuid, menuParentGuid, "Item '" + bookmark.title + "' should be in menu");
+ } else if (shouldBeInToolbar) {
+ Assert.equal(bookmark.parentGuid, toolbarParentGuid, "Item '" + bookmark.title + "' should be in toolbar");
+ } else if (bookmark.itemGuid == menuParentGuid || bookmark.itemGuid == toolbarParentGuid) {
+ Assert.ok(true, "Expect toolbar and menu folders to not be in menu or toolbar");
+ } else {
+ // Bit hacky, but we do need to check this.
+ Assert.equal(bookmark.title, "Item in folder", "Subfoldered item shouldn't be in menu or toolbar");
+ let parent = seenBookmarks.find(maybeParent => maybeParent.itemGuid == bookmark.parentGuid);
+ Assert.equal(parent && parent.title, "Folder", "Subfoldered item should be in subfolder labeled 'Folder'");
+ }
+
+ let dbItem = itemsInDB.find(someItem => bookmark.title == someItem.Title);
+ if (!dbItem) {
+ Assert.equal(bookmark.title, importParentFolderName, "Only the extra layer of folders isn't in the input we stuck in the DB.");
+ Assert.ok([menuParentGuid, toolbarParentGuid].includes(bookmark.itemGuid), "This item should be one of the containers");
+ } else {
+ Assert.equal(dbItem.URL || null, bookmark.url && bookmark.url.spec, "URL is correct");
+ Assert.equal(dbItem.DateUpdated.valueOf(), (new Date(bookmark.dateAdded / 1000)).valueOf(), "Date added is correct");
+ }
+ }
+});
+
diff --git a/browser/components/migration/tests/unit/test_IE7_passwords.js b/browser/components/migration/tests/unit/test_IE7_passwords.js
new file mode 100644
index 000000000..1ce016a7d
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE7_passwords.js
@@ -0,0 +1,397 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
+ "resource://gre/modules/WindowsRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto",
+ "resource://gre/modules/OSCrypto.jsm");
+
+const IE7_FORM_PASSWORDS_MIGRATOR_NAME = "IE7FormPasswords";
+const LOGINS_KEY = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2";
+const EXTENSION = "-backup";
+const TESTED_WEBSITES = {
+ twitter: {
+ uri: makeURI("https://twitter.com"),
+ hash: "A89D42BC6406E27265B1AD0782B6F376375764A301",
+ data: [12, 0, 0, 0, 56, 0, 0, 0, 38, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 8, 0, 0, 0, 18, 0, 0, 0, 68, 36, 67, 124, 118, 212, 208, 1, 9, 0, 0, 0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0, 103, 0, 104, 0, 0, 0, 49, 0, 50, 0, 51, 0, 52, 0, 53, 0, 54, 0, 55, 0, 56, 0, 57, 0, 0, 0],
+ logins: [
+ {
+ username: "abcdefgh",
+ password: "123456789",
+ hostname: "https://twitter.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439325854000,
+ timeLastUsed: 1439325854000,
+ timePasswordChanged: 1439325854000,
+ timesUsed: 1,
+ },
+ ],
+ },
+ facebook: {
+ uri: makeURI("https://www.facebook.com/"),
+ hash: "EF44D3E034009CB0FD1B1D81A1FF3F3335213BD796",
+ data: [12, 0, 0, 0, 152, 0, 0, 0, 160, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 88, 182, 125, 18, 121, 212, 208, 1, 9, 0, 0, 0, 40, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 60, 0, 0, 0, 134, 65, 33, 37, 121, 212, 208, 1, 9, 0, 0, 0, 80, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 100, 0, 0, 0, 45, 242, 246, 62, 121, 212, 208, 1, 9, 0, 0, 0, 120, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 140, 0, 0, 0, 28, 10, 193, 80, 121, 212, 208, 1, 9, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 48, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 48, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 49, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 49, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 50, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 50, 0, 0, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 51, 0, 0, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 51, 0, 0, 0],
+ logins: [
+ {
+ username: "username0",
+ password: "password0",
+ hostname: "https://www.facebook.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439326966000,
+ timeLastUsed: 1439326966000,
+ timePasswordChanged: 1439326966000,
+ timesUsed: 1,
+ },
+ {
+ username: "username1",
+ password: "password1",
+ hostname: "https://www.facebook.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439326997000,
+ timeLastUsed: 1439326997000,
+ timePasswordChanged: 1439326997000,
+ timesUsed: 1,
+ },
+ {
+ username: "username2",
+ password: "password2",
+ hostname: "https://www.facebook.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439327040000,
+ timeLastUsed: 1439327040000,
+ timePasswordChanged: 1439327040000,
+ timesUsed: 1,
+ },
+ {
+ username: "username3",
+ password: "password3",
+ hostname: "https://www.facebook.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439327070000,
+ timeLastUsed: 1439327070000,
+ timePasswordChanged: 1439327070000,
+ timesUsed: 1,
+ },
+ ],
+ },
+ live: {
+ uri: makeURI("https://login.live.com/"),
+ hash: "7B506F2D6B81D939A8E0456F036EE8970856FF705E",
+ data: [12, 0, 0, 0, 56, 0, 0, 0, 44, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 9, 0, 0, 0, 20, 0, 0, 0, 212, 17, 219, 140, 148, 212, 208, 1, 11, 0, 0, 0, 114, 0, 105, 0, 97, 0, 100, 0, 104, 0, 49, 6, 74, 6, 39, 6, 54, 6, 0, 0, 39, 6, 66, 6, 49, 6, 35, 6, 80, 0, 192, 0, 223, 0, 119, 0, 246, 0, 114, 0, 100, 0, 0, 0],
+ logins: [
+ {
+ username: "riadhرياض",
+ password: "اقرأPÀßwörd",
+ hostname: "https://login.live.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439338767000,
+ timeLastUsed: 1439338767000,
+ timePasswordChanged: 1439338767000,
+ timesUsed: 1,
+ },
+ ],
+ },
+ reddit: {
+ uri: makeURI("http://www.reddit.com/"),
+ hash: "B644028D1C109A91EC2C4B9D1F145E55A1FAE42065",
+ data: [12, 0, 0, 0, 152, 0, 0, 0, 212, 0, 0, 0, 87, 73, 67, 75, 24, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 8, 234, 114, 153, 212, 208, 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 3, 0, 0, 0, 14, 0, 0, 0, 97, 93, 131, 116, 153, 212, 208, 1, 16, 0, 0, 0, 48, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 4, 0, 0, 0, 58, 0, 0, 0, 88, 150, 78, 174, 153, 212, 208, 1, 29, 0, 0, 0, 118, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 15, 0, 0, 0, 150, 0, 0, 0, 79, 102, 137, 34, 154, 212, 208, 1, 30, 0, 0, 0, 97, 0, 0, 0, 0, 0, 252, 140, 173, 138, 146, 48, 0, 0, 66, 0, 105, 0, 116, 0, 116, 0, 101, 0, 32, 0, 98, 0, 101, 0, 115, 0, 116, 0, 228, 0, 116, 0, 105, 0, 103, 0, 101, 0, 110, 0, 0, 0, 205, 145, 110, 127, 198, 91, 1, 120, 0, 0, 31, 4, 48, 4, 64, 4, 62, 4, 59, 4, 76, 4, 32, 0, 67, 4, 65, 4, 63, 4, 53, 4, 72, 4, 61, 4, 62, 4, 32, 0, 65, 4, 49, 4, 64, 4, 62, 4, 72, 4, 53, 4, 61, 4, 46, 0, 32, 0, 18, 4, 62, 4, 57, 4, 66, 4, 56, 4, 0, 0, 40, 6, 51, 6, 69, 6, 32, 0, 39, 6, 68, 6, 68, 6, 71, 6, 32, 0, 39, 6, 68, 6, 49, 6, 45, 6, 69, 6, 70, 6, 0, 0, 118, 0, 101, 0, 117, 0, 105, 0, 108, 0, 108, 0, 101, 0, 122, 0, 32, 0, 108, 0, 101, 0, 32, 0, 118, 0, 233, 0, 114, 0, 105, 0, 102, 0, 105, 0, 101, 0, 114, 0, 32, 0, 224, 0, 32, 0, 110, 0, 111, 0, 117, 0, 118, 0, 101, 0, 97, 0, 117, 0, 0, 0],
+ logins: [
+ {
+ username: "購読を",
+ password: "Bitte bestätigen",
+ hostname: "http://www.reddit.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439340874000,
+ timeLastUsed: 1439340874000,
+ timePasswordChanged: 1439340874000,
+ timesUsed: 1,
+ },
+ {
+ username: "重置密码",
+ password: "Пароль успешно сброшен. Войти",
+ hostname: "http://www.reddit.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439340971000,
+ timeLastUsed: 1439340971000,
+ timePasswordChanged: 1439340971000,
+ timesUsed: 1,
+ },
+ {
+ username: "بسم الله الرحمن",
+ password: "veuillez le vérifier à nouveau",
+ hostname: "http://www.reddit.com",
+ formSubmitURL: "",
+ httpRealm: null,
+ usernameField: "",
+ passwordField: "",
+ timeCreated: 1439341166000,
+ timeLastUsed: 1439341166000,
+ timePasswordChanged: 1439341166000,
+ timesUsed: 1,
+ },
+ ],
+ },
+};
+
+const TESTED_URLS = [
+ "http://a.foo.com",
+ "http://b.foo.com",
+ "http://c.foo.com",
+ "http://www.test.net",
+ "http://www.test.net/home",
+ "http://www.test.net/index",
+ "https://a.bar.com",
+ "https://b.bar.com",
+ "https://c.bar.com",
+];
+
+var nsIWindowsRegKey = Ci.nsIWindowsRegKey;
+var Storage2Key;
+
+/*
+ * If the key value exists, it's going to be backed up and replaced, so the value could be restored.
+ * Otherwise a new value is going to be created.
+ */
+function backupAndStore(key, name, value) {
+ if (key.hasValue(name)) {
+ // backup the the current value
+ let type = key.getValueType(name);
+ // create a new value using use the current value name followed by EXTENSION as its new name
+ switch (type) {
+ case nsIWindowsRegKey.TYPE_STRING:
+ key.writeStringValue(name + EXTENSION, key.readStringValue(name));
+ break;
+ case nsIWindowsRegKey.TYPE_BINARY:
+ key.writeBinaryValue(name + EXTENSION, key.readBinaryValue(name));
+ break;
+ case nsIWindowsRegKey.TYPE_INT:
+ key.writeIntValue(name + EXTENSION, key.readIntValue(name));
+ break;
+ case nsIWindowsRegKey.TYPE_INT64:
+ key.writeInt64Value(name + EXTENSION, key.readInt64Value(name));
+ break;
+ }
+ }
+ key.writeBinaryValue(name, value);
+}
+
+// Remove all values where their names are members of the names array from the key of registry
+function removeAllValues(key, names) {
+ for (let name of names) {
+ key.removeValue(name);
+ }
+}
+
+// Restore all the backed up values
+function restore(key) {
+ let count = key.valueCount;
+ let names = []; // the names of the key values
+ for (let i = 0; i < count; ++i) {
+ names.push(key.getValueName(i));
+ }
+
+ for (let name of names) {
+ // backed up values have EXTENSION at the end of their names
+ if (name.lastIndexOf(EXTENSION) == name.length - EXTENSION.length) {
+ let valueName = name.substr(0, name.length - EXTENSION.length);
+ let type = key.getValueType(name);
+ // create a new value using the name before the backup and removed the backed up one
+ switch (type) {
+ case nsIWindowsRegKey.TYPE_STRING:
+ key.writeStringValue(valueName, key.readStringValue(name));
+ key.removeValue(name);
+ break;
+ case nsIWindowsRegKey.TYPE_BINARY:
+ key.writeBinaryValue(valueName, key.readBinaryValue(name));
+ key.removeValue(name);
+ break;
+ case nsIWindowsRegKey.TYPE_INT:
+ key.writeIntValue(valueName, key.readIntValue(name));
+ key.removeValue(name);
+ break;
+ case nsIWindowsRegKey.TYPE_INT64:
+ key.writeInt64Value(valueName, key.readInt64Value(name));
+ key.removeValue(name);
+ break;
+ }
+ }
+ }
+}
+
+function checkLoginsAreEqual(passwordManagerLogin, IELogin, id) {
+ passwordManagerLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ for (let attribute in IELogin) {
+ Assert.equal(passwordManagerLogin[attribute], IELogin[attribute],
+ "The two logins ID " + id + " have the same " + attribute);
+ }
+}
+
+function createRegistryPath(path) {
+ let loginPath = path.split("\\");
+ let parentKey = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(nsIWindowsRegKey);
+ let currentPath = [];
+ for (let currentKey of loginPath) {
+ parentKey.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, currentPath.join("\\"),
+ nsIWindowsRegKey.ACCESS_ALL);
+
+ if (!parentKey.hasChild(currentKey)) {
+ parentKey.createChild(currentKey, 0);
+ }
+ currentPath.push(currentKey);
+ parentKey.close();
+ }
+}
+
+function getFirstResourceOfType(type) {
+ let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=ie"]
+ .createInstance(Ci.nsISupports)
+ .wrappedJSObject;
+ let migrators = migrator.getResources();
+ for (let m of migrators) {
+ if (m.name == IE7_FORM_PASSWORDS_MIGRATOR_NAME && m.type == type) {
+ return m;
+ }
+ }
+ throw new Error("failed to find the " + type + " migrator");
+}
+
+function makeURI(aURL) {
+ return Services.io.newURI(aURL, null, null);
+}
+
+add_task(function* setup() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ Assert.throws(() => getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS),
+ "The migrator doesn't exist for win8+");
+ return;
+ }
+ // create the path to Storage2 in the registry if it doest exist.
+ createRegistryPath(LOGINS_KEY);
+ Storage2Key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(nsIWindowsRegKey);
+ Storage2Key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, LOGINS_KEY,
+ nsIWindowsRegKey.ACCESS_ALL);
+
+ // create a dummy value otherwise the migrator doesn't exist
+ if (!Storage2Key.hasValue("dummy")) {
+ Storage2Key.writeBinaryValue("dummy", "dummy");
+ }
+});
+
+add_task(function* test_passwordsNotAvailable() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ return;
+ }
+
+ let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
+ Assert.ok(migrator.exists, "The migrator has to exist");
+ let logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, 0, "There are no logins at the beginning of the test");
+
+ let uris = []; // the uris of the migrated logins
+ for (let url of TESTED_URLS) {
+ uris.push(makeURI(url));
+ // in this test, there is no IE login data in the registry, so after the migration, the number
+ // of logins in the store should be 0
+ migrator._migrateURIs(uris);
+ logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, 0,
+ "There are no logins after doing the migration without adding values to the registry");
+ }
+});
+
+add_task(function* test_passwordsAvailable() {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
+ return;
+ }
+
+ let crypto = new OSCrypto();
+ let hashes = []; // the hashes of all migrator websites, this is going to be used for the clean up
+
+ do_register_cleanup(() => {
+ Services.logins.removeAllLogins();
+ logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, 0, "There are no logins after the cleanup");
+ // remove all the values created in this test from the registry
+ removeAllValues(Storage2Key, hashes);
+ // restore all backed up values
+ restore(Storage2Key);
+
+ // clean the dummy value
+ if (Storage2Key.hasValue("dummy")) {
+ Storage2Key.removeValue("dummy");
+ }
+ Storage2Key.close();
+ crypto.finalize();
+ });
+
+ let migrator = getFirstResourceOfType(MigrationUtils.resourceTypes.PASSWORDS);
+ Assert.ok(migrator.exists, "The migrator has to exist");
+ let logins = Services.logins.getAllLogins({});
+ Assert.equal(logins.length, 0, "There are no logins at the beginning of the test");
+
+ let uris = []; // the uris of the migrated logins
+
+ let loginCount = 0;
+ for (let current in TESTED_WEBSITES) {
+ let website = TESTED_WEBSITES[current];
+ // backup the current the registry value if it exists and replace the existing value/create a
+ // new value with the encrypted data
+ backupAndStore(Storage2Key, website.hash,
+ crypto.encryptData(crypto.arrayToString(website.data),
+ website.uri.spec, true));
+ Assert.ok(migrator.exists, "The migrator has to exist");
+ uris.push(website.uri);
+ hashes.push(website.hash);
+
+ migrator._migrateURIs(uris);
+ logins = Services.logins.getAllLogins({});
+ // check that the number of logins in the password manager has increased as expected which means
+ // that all the values for the current website were imported
+ loginCount += website.logins.length;
+ Assert.equal(logins.length, loginCount,
+ "The number of logins has increased after the migration");
+ // NB: because telemetry records any login data passed to the login manager, it
+ // also gets told about logins that are duplicates or invalid (for one reason
+ // or another) and so its counts might exceed those of the login manager itself.
+ Assert.greaterOrEqual(MigrationUtils._importQuantities.logins, loginCount,
+ "Telemetry quantities equal or exceed the actual import.");
+ // Reset - this normally happens at the start of a new migration, but we're calling
+ // the migrator directly so can't rely on that:
+ MigrationUtils._importQuantities.logins = 0;
+
+ let startIndex = loginCount - website.logins.length;
+ // compares the imported password manager logins with their expected logins
+ for (let i = 0; i < website.logins.length; i++) {
+ checkLoginsAreEqual(logins[startIndex + i], website.logins[i],
+ " " + current + " - " + i + " ");
+ }
+ }
+});
diff --git a/browser/components/migration/tests/unit/test_IE_bookmarks.js b/browser/components/migration/tests/unit/test_IE_bookmarks.js
new file mode 100644
index 000000000..a166c0502
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -0,0 +1,44 @@
+"use strict";
+
+add_task(function* () {
+ let migrator = MigrationUtils.getMigrator("ie");
+ // Sanity check for the source.
+ Assert.ok(migrator.sourceExists);
+
+ // Wait for the imported bookmarks. Check that "From Internet Explorer"
+ // folders are created in the menu and on the toolbar.
+ let source = MigrationUtils.getLocalizedString("sourceNameIE");
+ let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
+
+ let expectedParents = [ PlacesUtils.bookmarksMenuFolderId,
+ PlacesUtils.toolbarFolderId ];
+
+ let itemCount = 0;
+ let bmObserver = {
+ onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
+ if (aTitle != label) {
+ itemCount++;
+ }
+ if (expectedParents.length > 0 && aTitle == label) {
+ let index = expectedParents.indexOf(aParentId);
+ Assert.notEqual(index, -1);
+ expectedParents.splice(index, 1);
+ }
+ },
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemRemoved() {},
+ onItemChanged() {},
+ onItemVisited() {},
+ onItemMoved() {},
+ };
+ PlacesUtils.bookmarks.addObserver(bmObserver, false);
+
+ yield promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.bookmarks.removeObserver(bmObserver);
+ Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount,
+ "Ensure telemetry matches actual number of imported items.");
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.equal(expectedParents.length, 0, "Got all the expected parents");
+});
diff --git a/browser/components/migration/tests/unit/test_IE_cookies.js b/browser/components/migration/tests/unit/test_IE_cookies.js
new file mode 100644
index 000000000..37a7462f2
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_IE_cookies.js
@@ -0,0 +1,111 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
+ "resource://gre/modules/ctypes.jsm");
+
+add_task(function* () {
+ let migrator = MigrationUtils.getMigrator("ie");
+ // Sanity check for the source.
+ Assert.ok(migrator.sourceExists);
+
+ const BOOL = ctypes.bool;
+ const LPCTSTR = ctypes.char16_t.ptr;
+ const DWORD = ctypes.uint32_t;
+ const LPDWORD = DWORD.ptr;
+
+ let wininet = ctypes.open("Wininet");
+
+ /*
+ BOOL InternetSetCookieW(
+ _In_ LPCTSTR lpszUrl,
+ _In_ LPCTSTR lpszCookieName,
+ _In_ LPCTSTR lpszCookieData
+ );
+ */
+ let setIECookie = wininet.declare("InternetSetCookieW",
+ ctypes.default_abi,
+ BOOL,
+ LPCTSTR,
+ LPCTSTR,
+ LPCTSTR);
+
+ /*
+ BOOL InternetGetCookieW(
+ _In_ LPCTSTR lpszUrl,
+ _In_ LPCTSTR lpszCookieName,
+ _Out_ LPCTSTR lpszCookieData,
+ _Inout_ LPDWORD lpdwSize
+ );
+ */
+ let getIECookie = wininet.declare("InternetGetCookieW",
+ ctypes.default_abi,
+ BOOL,
+ LPCTSTR,
+ LPCTSTR,
+ LPCTSTR,
+ LPDWORD);
+
+ // We need to randomize the cookie to avoid clashing with other cookies
+ // that might have been set by previous tests and not properly cleared.
+ let date = (new Date()).getDate();
+ const COOKIE = {
+ get host() {
+ return new URL(this.href).host;
+ },
+ href: `http://mycookietest.${Math.random()}.com`,
+ name: "testcookie",
+ value: "testvalue",
+ expiry: new Date(new Date().setDate(date + 2))
+ };
+ let data = ctypes.char16_t.array()(256);
+ let sizeRef = DWORD(256).address();
+
+ do_register_cleanup(() => {
+ // Remove the cookie.
+ try {
+ let expired = new Date(new Date().setDate(date - 2));
+ let rv = setIECookie(COOKIE.href, COOKIE.name,
+ `; expires=${expired.toUTCString()}`);
+ Assert.ok(rv, "Expired the IE cookie");
+ Assert.ok(!getIECookie(COOKIE.href, COOKIE.name, data, sizeRef),
+ "The cookie has been properly removed");
+ } catch (ex) {}
+
+ // Close the library.
+ try {
+ wininet.close();
+ } catch (ex) {}
+ });
+
+ // Create the persistent cookie in IE.
+ let value = `${COOKIE.value}; expires=${COOKIE.expiry.toUTCString()}`;
+ let rv = setIECookie(COOKIE.href, COOKIE.name, value);
+ Assert.ok(rv, "Added a persistent IE cookie: " + value);
+
+ // Sanity check the cookie has been created.
+ Assert.ok(getIECookie(COOKIE.href, COOKIE.name, data, sizeRef),
+ "Found the added persistent IE cookie");
+ do_print("Found cookie: " + data.readString());
+ Assert.equal(data.readString(), `${COOKIE.name}=${COOKIE.value}`,
+ "Found the expected cookie");
+
+ // Sanity check that there are no cookies.
+ Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 0,
+ "There are no cookies initially");
+
+ // Migrate cookies.
+ yield promiseMigration(migrator, MigrationUtils.resourceTypes.COOKIES);
+
+ Assert.equal(Services.cookies.countCookiesFromHost(COOKIE.host), 1,
+ "Migrated the expected number of cookies");
+
+ // Now check the cookie details.
+ let enumerator = Services.cookies.getCookiesFromHost(COOKIE.host, {});
+ Assert.ok(enumerator.hasMoreElements());
+ let foundCookie = enumerator.getNext().QueryInterface(Ci.nsICookie2);
+
+ Assert.equal(foundCookie.name, COOKIE.name);
+ Assert.equal(foundCookie.value, COOKIE.value);
+ Assert.equal(foundCookie.host, "." + COOKIE.host);
+ Assert.equal(foundCookie.expiry, Math.floor(COOKIE.expiry / 1000));
+});
diff --git a/browser/components/migration/tests/unit/test_Safari_bookmarks.js b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
new file mode 100644
index 000000000..edc32dc72
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
@@ -0,0 +1,46 @@
+"use strict";
+
+add_task(function* () {
+ registerFakePath("ULibDir", do_get_file("Library/"));
+
+ let migrator = MigrationUtils.getMigrator("safari");
+ // Sanity check for the source.
+ Assert.ok(migrator.sourceExists);
+
+ // Wait for the imported bookmarks. Check that "From Safari"
+ // folders are created on the toolbar.
+ let source = MigrationUtils.getLocalizedString("sourceNameSafari");
+ let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
+
+ let expectedParents = [ PlacesUtils.toolbarFolderId ];
+ let itemCount = 0;
+
+ let bmObserver = {
+ onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
+ if (aTitle != label) {
+ itemCount++;
+ }
+ if (expectedParents.length > 0 && aTitle == label) {
+ let index = expectedParents.indexOf(aParentId);
+ Assert.ok(index != -1, "Found expected parent");
+ expectedParents.splice(index, 1);
+ }
+ },
+ onBeginUpdateBatch() {},
+ onEndUpdateBatch() {},
+ onItemRemoved() {},
+ onItemChanged() {},
+ onItemVisited() {},
+ onItemMoved() {},
+ };
+ PlacesUtils.bookmarks.addObserver(bmObserver, false);
+
+ yield promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
+ PlacesUtils.bookmarks.removeObserver(bmObserver);
+
+ // Check the bookmarks have been imported to all the expected parents.
+ Assert.ok(!expectedParents.length, "No more expected parents");
+ Assert.equal(itemCount, 13, "Should import all 13 items.");
+ // Check that the telemetry matches:
+ Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, "Telemetry reporting correct.");
+});
diff --git a/browser/components/migration/tests/unit/test_automigration.js b/browser/components/migration/tests/unit/test_automigration.js
new file mode 100644
index 000000000..bc9076a6c
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_automigration.js
@@ -0,0 +1,695 @@
+"use strict";
+
+let AutoMigrateBackstage = Cu.import("resource:///modules/AutoMigrate.jsm"); /* globals AutoMigrate */
+
+let gShimmedMigratorKeyPicker = null;
+let gShimmedMigrator = null;
+
+const kUsecPerMin = 60 * 1000000;
+
+// This is really a proxy on MigrationUtils, but if we specify that directly,
+// we get in trouble because the object itself is frozen, and Proxies can't
+// return a different value to an object when directly proxying a frozen
+// object.
+AutoMigrateBackstage.MigrationUtils = new Proxy({}, {
+ get(target, name) {
+ if (name == "getMigratorKeyForDefaultBrowser" && gShimmedMigratorKeyPicker) {
+ return gShimmedMigratorKeyPicker;
+ }
+ if (name == "getMigrator" && gShimmedMigrator) {
+ return function() { return gShimmedMigrator };
+ }
+ return MigrationUtils[name];
+ },
+});
+
+do_register_cleanup(function() {
+ AutoMigrateBackstage.MigrationUtils = MigrationUtils;
+});
+
+// This should be replaced by using History.fetch with a fetchVisits option,
+// once that becomes available
+function* visitsForURL(url)
+{
+ let visitCount = 0;
+ let db = yield PlacesUtils.promiseDBConnection();
+ visitCount = yield db.execute(
+ `SELECT count(*) FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = v.place_id
+ WHERE url_hash = hash(:url) AND url = :url`,
+ {url});
+ visitCount = visitCount[0].getInt64(0);
+ return visitCount;
+}
+
+
+/**
+ * Test automatically picking a browser to migrate from
+ */
+add_task(function* checkMigratorPicking() {
+ Assert.throws(() => AutoMigrate.pickMigrator("firefox"),
+ /Can't automatically migrate from Firefox/,
+ "Should throw when explicitly picking Firefox.");
+
+ Assert.throws(() => AutoMigrate.pickMigrator("gobbledygook"),
+ /migrator object is not available/,
+ "Should throw when passing unknown migrator key");
+ gShimmedMigratorKeyPicker = function() {
+ return "firefox";
+ };
+ Assert.throws(() => AutoMigrate.pickMigrator(),
+ /Can't automatically migrate from Firefox/,
+ "Should throw when implicitly picking Firefox.");
+ gShimmedMigratorKeyPicker = function() {
+ return "gobbledygook";
+ };
+ Assert.throws(() => AutoMigrate.pickMigrator(),
+ /migrator object is not available/,
+ "Should throw when an unknown migrator is the default");
+ gShimmedMigratorKeyPicker = function() {
+ return "";
+ };
+ Assert.throws(() => AutoMigrate.pickMigrator(),
+ /Could not determine default browser key/,
+ "Should throw when an unknown migrator is the default");
+});
+
+
+/**
+ * Test automatically picking a profile to migrate from
+ */
+add_task(function* checkProfilePicking() {
+ let fakeMigrator = {sourceProfiles: [{id: "a"}, {id: "b"}]};
+ let profB = fakeMigrator.sourceProfiles[1];
+ Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
+ /Don't know how to pick a profile when more/,
+ "Should throw when there are multiple profiles.");
+ Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
+ /Profile specified was not found/,
+ "Should throw when the profile supplied doesn't exist.");
+ let profileToMigrate = AutoMigrate.pickProfile(fakeMigrator, "b");
+ Assert.equal(profileToMigrate, profB, "Should return profile supplied");
+
+ fakeMigrator.sourceProfiles = null;
+ Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
+ /Profile specified but only a default profile found./,
+ "Should throw when the profile supplied doesn't exist.");
+ profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
+ Assert.equal(profileToMigrate, null, "Should return default profile when that's the only one.");
+
+ fakeMigrator.sourceProfiles = [];
+ Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
+ /No profile data found/,
+ "Should throw when no profile data is present.");
+
+ fakeMigrator.sourceProfiles = [{id: "a"}];
+ let profA = fakeMigrator.sourceProfiles[0];
+ profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
+ Assert.equal(profileToMigrate, profA, "Should return the only profile if only one is present.");
+});
+
+/**
+ * Test the complete automatic process including browser and profile selection,
+ * and actual migration (which implies startup)
+ */
+add_task(function* checkIntegration() {
+ gShimmedMigrator = {
+ get sourceProfiles() {
+ do_print("Read sourceProfiles");
+ return null;
+ },
+ getMigrateData(profileToMigrate) {
+ this._getMigrateDataArgs = profileToMigrate;
+ return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
+ },
+ migrate(types, startup, profileToMigrate) {
+ this._migrateArgs = [types, startup, profileToMigrate];
+ },
+ };
+ gShimmedMigratorKeyPicker = function() {
+ return "gobbledygook";
+ };
+ AutoMigrate.migrate("startup");
+ Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
+ "getMigrateData called with 'null' as a profile");
+
+ let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
+ let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
+ Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
+ "migrate called with 'null' as a profile");
+});
+
+/**
+ * Test the undo preconditions and a no-op undo in the automigrator.
+ */
+add_task(function* checkUndoPreconditions() {
+ let shouldAddData = false;
+ gShimmedMigrator = {
+ get sourceProfiles() {
+ do_print("Read sourceProfiles");
+ return null;
+ },
+ getMigrateData(profileToMigrate) {
+ this._getMigrateDataArgs = profileToMigrate;
+ return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
+ },
+ migrate(types, startup, profileToMigrate) {
+ this._migrateArgs = [types, startup, profileToMigrate];
+ if (shouldAddData) {
+ // Insert a login and check that that worked.
+ MigrationUtils.insertLoginWrapper({
+ hostname: "www.mozilla.org",
+ formSubmitURL: "http://www.mozilla.org",
+ username: "user",
+ password: "pass",
+ });
+ }
+ TestUtils.executeSoon(function() {
+ Services.obs.notifyObservers(null, "Migration:Ended", undefined);
+ });
+ },
+ };
+
+ gShimmedMigratorKeyPicker = function() {
+ return "gobbledygook";
+ };
+ AutoMigrate.migrate("startup");
+ let migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
+ Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
+ "getMigrateData called with 'null' as a profile");
+
+ let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
+ let expectedTypes = BOOKMARKS | HISTORY | PASSWORDS;
+ Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
+ "migrate called with 'null' as a profile");
+
+ yield migrationFinishedPromise;
+ Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
+ "Should have set browser pref");
+ Assert.ok(!(yield AutoMigrate.canUndo()), "Should not be able to undo migration, as there's no data");
+ gShimmedMigrator._migrateArgs = null;
+ gShimmedMigrator._getMigrateDataArgs = null;
+ Preferences.reset("browser.migrate.automigrate.browser");
+ shouldAddData = true;
+
+ AutoMigrate.migrate("startup");
+ migrationFinishedPromise = TestUtils.topicObserved("Migration:Ended");
+ Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
+ "getMigrateData called with 'null' as a profile");
+ Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
+ "migrate called with 'null' as a profile");
+
+ yield migrationFinishedPromise;
+ let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
+ "http://www.mozilla.org", null);
+ Assert.equal(storedLogins.length, 1, "Should have 1 login");
+
+ Assert.ok(Preferences.has("browser.migrate.automigrate.browser"),
+ "Should have set browser pref");
+ Assert.ok((yield AutoMigrate.canUndo()), "Should be able to undo migration, as now there's data");
+
+ yield AutoMigrate.undo();
+ Assert.ok(true, "Should be able to finish an undo cycle.");
+
+ // Check that the undo removed the passwords:
+ storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
+ "http://www.mozilla.org", null);
+ Assert.equal(storedLogins.length, 0, "Should have no logins");
+});
+
+/**
+ * Fake a migration and then try to undo it to verify all data gets removed.
+ */
+add_task(function* checkUndoRemoval() {
+ MigrationUtils.initializeUndoData();
+ Preferences.set("browser.migrate.automigrate.browser", "automationbrowser");
+ // Insert a login and check that that worked.
+ MigrationUtils.insertLoginWrapper({
+ hostname: "www.mozilla.org",
+ formSubmitURL: "http://www.mozilla.org",
+ username: "user",
+ password: "pass",
+ });
+ let storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
+ "http://www.mozilla.org", null);
+ Assert.equal(storedLogins.length, 1, "Should have 1 login");
+
+ // Insert a bookmark and check that we have exactly 1 bookmark for that URI.
+ yield MigrationUtils.insertBookmarkWrapper({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://www.example.org/",
+ title: "Some example bookmark",
+ });
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
+ Assert.ok(bookmark, "Should have a bookmark before undo");
+ Assert.equal(bookmark.title, "Some example bookmark", "Should have correct bookmark before undo.");
+
+ // Insert 2 history visits
+ let now_uSec = Date.now() * 1000;
+ let visitedURI = Services.io.newURI("http://www.example.com/", null, null);
+ let frecencyUpdatePromise = new Promise(resolve => {
+ let expectedChanges = 2;
+ let observer = {
+ onFrecencyChanged: function() {
+ if (!--expectedChanges) {
+ PlacesUtils.history.removeObserver(observer);
+ resolve();
+ }
+ },
+ };
+ PlacesUtils.history.addObserver(observer, false);
+ });
+ yield MigrationUtils.insertVisitsWrapper([{
+ uri: visitedURI,
+ visits: [
+ {
+ transitionType: PlacesUtils.history.TRANSITION_LINK,
+ visitDate: now_uSec,
+ },
+ {
+ transitionType: PlacesUtils.history.TRANSITION_LINK,
+ visitDate: now_uSec - 100 * kUsecPerMin,
+ },
+ ]
+ }]);
+ yield frecencyUpdatePromise;
+
+ // Verify that both visits get reported.
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_VISIT;
+ let query = PlacesUtils.history.getNewQuery();
+ query.uri = visitedURI;
+ let visits = PlacesUtils.history.executeQuery(query, opts);
+ visits.root.containerOpen = true;
+ Assert.equal(visits.root.childCount, 2, "Should have 2 visits");
+ // Clean up:
+ visits.root.containerOpen = false;
+
+ yield AutoMigrate.saveUndoState();
+
+ // Verify that we can undo, then undo:
+ Assert.ok(AutoMigrate.canUndo(), "Should be possible to undo migration");
+ yield AutoMigrate.undo();
+
+ let histograms = [
+ "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_ERRORCOUNT",
+ "FX_STARTUP_MIGRATION_UNDO_LOGINS_ERRORCOUNT",
+ "FX_STARTUP_MIGRATION_UNDO_VISITS_ERRORCOUNT",
+ ];
+ for (let histogramId of histograms) {
+ let keyedHistogram = Services.telemetry.getKeyedHistogramById(histogramId);
+ let histogramData = keyedHistogram.snapshot().automationbrowser;
+ Assert.equal(histogramData.sum, 0, `Should have reported 0 errors to ${histogramId}.`);
+ Assert.greaterOrEqual(histogramData.counts[0], 1, `Should have reported value of 0 one time to ${histogramId}.`);
+ }
+ histograms = [
+ "FX_STARTUP_MIGRATION_UNDO_BOOKMARKS_MS",
+ "FX_STARTUP_MIGRATION_UNDO_LOGINS_MS",
+ "FX_STARTUP_MIGRATION_UNDO_VISITS_MS",
+ "FX_STARTUP_MIGRATION_UNDO_TOTAL_MS",
+ ];
+ for (let histogramId of histograms) {
+ Assert.greater(Services.telemetry.getKeyedHistogramById(histogramId).snapshot().automationbrowser.sum, 0,
+ `Should have reported non-zero time spent using undo for ${histogramId}`);
+ }
+
+ // Check that the undo removed the history visits:
+ visits = PlacesUtils.history.executeQuery(query, opts);
+ visits.root.containerOpen = true;
+ Assert.equal(visits.root.childCount, 0, "Should have no more visits");
+ visits.root.containerOpen = false;
+
+ // Check that the undo removed the bookmarks:
+ bookmark = yield PlacesUtils.bookmarks.fetch({url: "http://www.example.org/"});
+ Assert.ok(!bookmark, "Should have no bookmarks after undo");
+
+ // Check that the undo removed the passwords:
+ storedLogins = Services.logins.findLogins({}, "www.mozilla.org",
+ "http://www.mozilla.org", null);
+ Assert.equal(storedLogins.length, 0, "Should have no logins");
+});
+
+add_task(function* checkUndoBookmarksState() {
+ MigrationUtils.initializeUndoData();
+ const {TYPE_FOLDER, TYPE_BOOKMARK} = PlacesUtils.bookmarks;
+ let title = "Some example bookmark";
+ let url = "http://www.example.com";
+ let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
+ let {guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
+ title, url, parentGuid
+ });
+ Assert.deepEqual((yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks"),
+ [{lastModified, parentGuid, guid, type: TYPE_BOOKMARK}]);
+
+ MigrationUtils.initializeUndoData();
+ ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
+ title, parentGuid, type: TYPE_FOLDER
+ }));
+ let folder = {guid, lastModified, parentGuid, type: TYPE_FOLDER};
+ let folderGuid = folder.guid;
+ ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
+ title, url, parentGuid: folderGuid
+ }));
+ let kid1 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
+ ({guid, lastModified} = yield MigrationUtils.insertBookmarkWrapper({
+ title, url, parentGuid: folderGuid
+ }));
+ let kid2 = {guid, lastModified, parentGuid: folderGuid, type: TYPE_BOOKMARK};
+
+ let bookmarksUndo = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
+ Assert.equal(bookmarksUndo.length, 3);
+ // We expect that the last modified time from first kid #1 and then kid #2
+ // has been propagated to the folder:
+ folder.lastModified = kid2.lastModified;
+ // Not just using deepEqual on the entire array (which should work) because
+ // the failure messages get truncated by xpcshell which is unhelpful.
+ Assert.deepEqual(bookmarksUndo[0], folder);
+ Assert.deepEqual(bookmarksUndo[1], kid1);
+ Assert.deepEqual(bookmarksUndo[2], kid2);
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* testBookmarkRemovalByUndo() {
+ const {TYPE_FOLDER} = PlacesUtils.bookmarks;
+ MigrationUtils.initializeUndoData();
+ let title = "Some example bookmark";
+ let url = "http://www.mymagicaluniqueurl.com";
+ let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
+ let {guid} = yield MigrationUtils.insertBookmarkWrapper({
+ title: "Some folder", parentGuid, type: TYPE_FOLDER
+ });
+ let folderGuid = guid;
+ let itemsToRemove = [];
+ ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+ title: "Inner folder", parentGuid: folderGuid, type: TYPE_FOLDER
+ }));
+ let innerFolderGuid = guid;
+ itemsToRemove.push(innerFolderGuid);
+
+ ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+ title: "Inner inner folder", parentGuid: innerFolderGuid, type: TYPE_FOLDER
+ }));
+ itemsToRemove.push(guid);
+
+ ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+ title: "Inner nested item", url: "http://inner-nested-example.com", parentGuid: guid
+ }));
+ itemsToRemove.push(guid);
+
+ ({guid} = yield MigrationUtils.insertBookmarkWrapper({
+ title, url, parentGuid: folderGuid
+ }));
+ itemsToRemove.push(guid);
+
+ for (let toBeRemovedGuid of itemsToRemove) {
+ let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(toBeRemovedGuid);
+ Assert.ok(dbResultForGuid, "Should be able to find items that will be removed.");
+ }
+ let bookmarkUndoState = (yield MigrationUtils.stopAndRetrieveUndoData()).get("bookmarks");
+ // Now insert a separate item into this folder, not related to the migration.
+ let newItem = yield PlacesUtils.bookmarks.insert(
+ {title: "Not imported", parentGuid: folderGuid, url: "http://www.example.com"}
+ );
+
+ yield AutoMigrate._removeUnchangedBookmarks(bookmarkUndoState);
+ Assert.ok(true, "Successfully removed imported items.");
+
+ let itemFromDB = yield PlacesUtils.bookmarks.fetch(newItem.guid);
+ Assert.ok(itemFromDB, "Item we inserted outside of migration is still there.");
+ itemFromDB = yield PlacesUtils.bookmarks.fetch(folderGuid);
+ Assert.ok(itemFromDB, "Folder we inserted in migration is still there because of new kids.");
+ for (let removedGuid of itemsToRemove) {
+ let dbResultForGuid = yield PlacesUtils.bookmarks.fetch(removedGuid);
+ let dbgStr = dbResultForGuid && dbResultForGuid.title;
+ Assert.equal(null, dbResultForGuid, "Should not be able to find items that should have been removed, but found " + dbgStr);
+ }
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* checkUndoLoginsState() {
+ MigrationUtils.initializeUndoData();
+ MigrationUtils.insertLoginWrapper({
+ username: "foo",
+ password: "bar",
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com/",
+ timeCreated: new Date(),
+ });
+ let storedLogins = Services.logins.findLogins({}, "https://example.com", "", "");
+ let storedLogin = storedLogins[0];
+ storedLogin.QueryInterface(Ci.nsILoginMetaInfo);
+ let {guid, timePasswordChanged} = storedLogin;
+ let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");
+ Assert.deepEqual([{guid, timePasswordChanged}], undoLoginData);
+ Services.logins.removeAllLogins();
+});
+
+add_task(function* testLoginsRemovalByUndo() {
+ MigrationUtils.initializeUndoData();
+ MigrationUtils.insertLoginWrapper({
+ username: "foo",
+ password: "bar",
+ hostname: "https://example.com",
+ formSubmitURL: "https://example.com/",
+ timeCreated: new Date(),
+ });
+ MigrationUtils.insertLoginWrapper({
+ username: "foo",
+ password: "bar",
+ hostname: "https://example.org",
+ formSubmitURL: "https://example.org/",
+ timeCreated: new Date(new Date().getTime() - 10000),
+ });
+ // This should update the existing login
+ LoginHelper.maybeImportLogin({
+ username: "foo",
+ password: "bazzy",
+ hostname: "https://example.org",
+ formSubmitURL: "https://example.org/",
+ timePasswordChanged: new Date(),
+ });
+ Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
+ "Should be only 1 login for example.org (that was updated)");
+ let undoLoginData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("logins");
+
+ yield AutoMigrate._removeUnchangedLogins(undoLoginData);
+ Assert.equal(0, LoginHelper.searchLoginsWithObject({hostname: "https://example.com", formSubmitURL: "https://example.com/"}).length,
+ "unchanged example.com entry should have been removed.");
+ Assert.equal(1, LoginHelper.searchLoginsWithObject({hostname: "https://example.org", formSubmitURL: "https://example.org/"}).length,
+ "changed example.org entry should have persisted.");
+ Services.logins.removeAllLogins();
+});
+
+add_task(function* checkUndoVisitsState() {
+ MigrationUtils.initializeUndoData();
+ yield MigrationUtils.insertVisitsWrapper([{
+ uri: NetUtil.newURI("http://www.example.com/"),
+ title: "Example",
+ visits: [{
+ visitDate: new Date("2015-07-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }, {
+ visitDate: new Date("2015-09-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }, {
+ visitDate: new Date("2015-08-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }],
+ }, {
+ uri: NetUtil.newURI("http://www.example.org/"),
+ title: "Example",
+ visits: [{
+ visitDate: new Date("2016-04-03").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }, {
+ visitDate: new Date("2015-08-03").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }],
+ }, {
+ uri: NetUtil.newURI("http://www.example.com/"),
+ title: "Example",
+ visits: [{
+ visitDate: new Date("2015-10-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }],
+ }]);
+ let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");
+ Assert.deepEqual(Array.from(undoVisitData.map(v => v.url)).sort(),
+ ["http://www.example.com/", "http://www.example.org/"]);
+ Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.com/"), {
+ url: "http://www.example.com/",
+ visitCount: 4,
+ first: new Date("2015-07-10").getTime() * 1000,
+ last: new Date("2015-10-10").getTime() * 1000,
+ });
+ Assert.deepEqual(undoVisitData.find(v => v.url == "http://www.example.org/"), {
+ url: "http://www.example.org/",
+ visitCount: 2,
+ first: new Date("2015-08-03").getTime() * 1000,
+ last: new Date("2016-04-03").getTime() * 1000,
+ });
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* checkUndoVisitsState() {
+ MigrationUtils.initializeUndoData();
+ yield MigrationUtils.insertVisitsWrapper([{
+ uri: NetUtil.newURI("http://www.example.com/"),
+ title: "Example",
+ visits: [{
+ visitDate: new Date("2015-07-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }, {
+ visitDate: new Date("2015-09-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }, {
+ visitDate: new Date("2015-08-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }],
+ }, {
+ uri: NetUtil.newURI("http://www.example.org/"),
+ title: "Example",
+ visits: [{
+ visitDate: new Date("2016-04-03").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }, {
+ visitDate: new Date("2015-08-03").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }],
+ }, {
+ uri: NetUtil.newURI("http://www.example.com/"),
+ title: "Example",
+ visits: [{
+ visitDate: new Date("2015-10-10").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }],
+ }, {
+ uri: NetUtil.newURI("http://www.mozilla.org/"),
+ title: "Example",
+ visits: [{
+ visitDate: new Date("2015-01-01").getTime() * 1000,
+ transitionType: Ci.nsINavHistoryService.TRANSITION_LINK,
+ }],
+ }]);
+
+ // We have to wait until frecency updates have been handled in order
+ // to accurately determine whether we're doing the right thing.
+ let frecencyUpdatesHandled = new Promise(resolve => {
+ PlacesUtils.history.addObserver({
+ onFrecencyChanged(aURI) {
+ if (aURI.spec == "http://www.unrelated.org/") {
+ PlacesUtils.history.removeObserver(this);
+ resolve();
+ }
+ }
+ }, false);
+ });
+ yield PlacesUtils.history.insertMany([{
+ url: "http://www.example.com/",
+ title: "Example",
+ visits: [{
+ date: new Date("2015-08-16"),
+ }],
+ }, {
+ url: "http://www.example.org/",
+ title: "Example",
+ visits: [{
+ date: new Date("2016-01-03"),
+ }, {
+ date: new Date("2015-05-03"),
+ }],
+ }, {
+ url: "http://www.unrelated.org/",
+ title: "Unrelated",
+ visits: [{
+ date: new Date("2015-09-01"),
+ }],
+ }]);
+ yield frecencyUpdatesHandled;
+ let undoVisitData = (yield MigrationUtils.stopAndRetrieveUndoData()).get("visits");
+
+ let frecencyChangesExpected = new Map([
+ ["http://www.example.com/", PromiseUtils.defer()],
+ ["http://www.example.org/", PromiseUtils.defer()]
+ ]);
+ let uriDeletedExpected = new Map([
+ ["http://www.mozilla.org/", PromiseUtils.defer()],
+ ]);
+ let wrongMethodDeferred = PromiseUtils.defer();
+ let observer = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function(uri) {
+ wrongMethodDeferred.reject(new Error("Unexpected call to onVisit " + uri.spec));
+ },
+ onTitleChanged: function(uri) {
+ wrongMethodDeferred.reject(new Error("Unexpected call to onTitleChanged " + uri.spec));
+ },
+ onClearHistory: function() {
+ wrongMethodDeferred.reject("Unexpected call to onClearHistory");
+ },
+ onPageChanged: function(uri) {
+ wrongMethodDeferred.reject(new Error("Unexpected call to onPageChanged " + uri.spec));
+ },
+ onFrecencyChanged: function(aURI) {
+ do_print("frecency change");
+ Assert.ok(frecencyChangesExpected.has(aURI.spec),
+ "Should be expecting frecency change for " + aURI.spec);
+ frecencyChangesExpected.get(aURI.spec).resolve();
+ },
+ onManyFrecenciesChanged: function() {
+ do_print("Many frecencies changed");
+ wrongMethodDeferred.reject(new Error("This test can't deal with onManyFrecenciesChanged to be called"));
+ },
+ onDeleteURI: function(aURI) {
+ do_print("delete uri");
+ Assert.ok(uriDeletedExpected.has(aURI.spec),
+ "Should be expecting uri deletion for " + aURI.spec);
+ uriDeletedExpected.get(aURI.spec).resolve();
+ },
+ };
+ PlacesUtils.history.addObserver(observer, false);
+
+ yield AutoMigrate._removeSomeVisits(undoVisitData);
+ PlacesUtils.history.removeObserver(observer);
+ yield Promise.all(uriDeletedExpected.values());
+ yield Promise.all(frecencyChangesExpected.values());
+
+ Assert.equal(yield visitsForURL("http://www.example.com/"), 1,
+ "1 example.com visit (out of 5) should have persisted despite being within the range, due to limiting");
+ Assert.equal(yield visitsForURL("http://www.mozilla.org/"), 0,
+ "0 mozilla.org visits should have persisted (out of 1).");
+ Assert.equal(yield visitsForURL("http://www.example.org/"), 2,
+ "2 example.org visits should have persisted (out of 4).");
+ Assert.equal(yield visitsForURL("http://www.unrelated.org/"), 1,
+ "1 unrelated.org visits should have persisted as it's not involved in the import.");
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* checkHistoryRemovalCompletion() {
+ AutoMigrate._errorMap = {bookmarks: 0, visits: 0, logins: 0};
+ yield AutoMigrate._removeSomeVisits([{url: "http://www.example.com/", limit: -1}]);
+ ok(true, "Removing visits should complete even if removing some visits failed.");
+ Assert.equal(AutoMigrate._errorMap.visits, 1, "Should have logged the error for visits.");
+
+ // Unfortunately there's not a reliable way to make removing bookmarks be
+ // unhappy unless the DB is messed up (e.g. contains children but has
+ // parents removed already).
+ yield AutoMigrate._removeUnchangedBookmarks([
+ {guid: PlacesUtils.bookmarks, lastModified: new Date(0), parentGuid: 0},
+ {guid: "gobbledygook", lastModified: new Date(0), parentGuid: 0},
+ ]);
+ ok(true, "Removing bookmarks should complete even if some items are gone or bogus.");
+ Assert.equal(AutoMigrate._errorMap.bookmarks, 0,
+ "Should have ignored removing non-existing (or builtin) bookmark.");
+
+
+ yield AutoMigrate._removeUnchangedLogins([
+ {guid: "gobbledygook", timePasswordChanged: new Date(0)},
+ ]);
+ ok(true, "Removing logins should complete even if logins don't exist.");
+ Assert.equal(AutoMigrate._errorMap.logins, 0,
+ "Should have ignored removing non-existing logins.");
+});
diff --git a/browser/components/migration/tests/unit/test_fx_telemetry.js b/browser/components/migration/tests/unit/test_fx_telemetry.js
new file mode 100644
index 000000000..a276f52f8
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_fx_telemetry.js
@@ -0,0 +1,288 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* globals do_get_tempdir */
+
+"use strict";
+
+function run_test() {
+ run_next_test();
+}
+
+function readFile(file) {
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(Ci.nsIScriptableInputStream);
+ sis.init(stream);
+ let contents = sis.read(file.fileSize);
+ sis.close();
+ return contents;
+}
+
+function checkDirectoryContains(dir, files) {
+ print("checking " + dir.path + " - should contain " + Object.keys(files));
+ let seen = new Set();
+ let enumerator = dir.directoryEntries;
+ while (enumerator.hasMoreElements()) {
+ let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
+ print("found file: " + file.path);
+ Assert.ok(file.leafName in files, file.leafName + " exists, but shouldn't");
+
+ let expectedContents = files[file.leafName];
+ if (typeof expectedContents != "string") {
+ // it's a subdir - recurse!
+ Assert.ok(file.isDirectory(), "should be a subdir");
+ let newDir = dir.clone();
+ newDir.append(file.leafName);
+ checkDirectoryContains(newDir, expectedContents);
+ } else {
+ Assert.ok(!file.isDirectory(), "should be a regular file");
+ let contents = readFile(file);
+ Assert.equal(contents, expectedContents);
+ }
+ seen.add(file.leafName);
+ }
+ let missing = [];
+ for (let x in files) {
+ if (!seen.has(x)) {
+ missing.push(x);
+ }
+ }
+ Assert.deepEqual(missing, [], "no missing files in " + dir.path);
+}
+
+function getTestDirs() {
+ // we make a directory structure in a temp dir which mirrors what we are
+ // testing.
+ let tempDir = do_get_tempdir();
+ let srcDir = tempDir.clone();
+ srcDir.append("test_source_dir");
+ srcDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let targetDir = tempDir.clone();
+ targetDir.append("test_target_dir");
+ targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // no need to cleanup these dirs - the xpcshell harness will do it for us.
+ return [srcDir, targetDir];
+}
+
+function writeToFile(dir, leafName, contents) {
+ let file = dir.clone();
+ file.append(leafName);
+
+ let outputStream = FileUtils.openFileOutputStream(file);
+ outputStream.write(contents, contents.length);
+ outputStream.close();
+}
+
+function createSubDir(dir, subDirName) {
+ let subDir = dir.clone();
+ subDir.append(subDirName);
+ subDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ return subDir;
+}
+
+function promiseMigrator(name, srcDir, targetDir) {
+ let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=firefox"]
+ .createInstance(Ci.nsISupports)
+ .wrappedJSObject;
+ let migrators = migrator._getResourcesInternal(srcDir, targetDir);
+ for (let m of migrators) {
+ if (m.name == name) {
+ return new Promise(resolve => m.migrate(resolve));
+ }
+ }
+ throw new Error("failed to find the " + name + " migrator");
+}
+
+function promiseTelemetryMigrator(srcDir, targetDir) {
+ return promiseMigrator("telemetry", srcDir, targetDir);
+}
+
+add_task(function* test_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true with empty directories");
+ // check both are empty
+ checkDirectoryContains(srcDir, {});
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(function* test_migrate_files() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Set up datareporting files, some to copy, some not.
+ let stateContent = JSON.stringify({
+ clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c",
+ });
+ let sessionStateContent = "foobar 5432";
+ let subDir = createSubDir(srcDir, "datareporting");
+ writeToFile(subDir, "state.json", stateContent);
+ writeToFile(subDir, "session-state.json", sessionStateContent);
+ writeToFile(subDir, "other.file", "do not copy");
+
+ let archived = createSubDir(subDir, "archived");
+ writeToFile(archived, "other.file", "do not copy");
+
+ // Set up FHR files, they should not be copied.
+ writeToFile(srcDir, "healthreport.sqlite", "do not copy");
+ writeToFile(srcDir, "healthreport.sqlite-wal", "do not copy");
+ subDir = createSubDir(srcDir, "healthreport");
+ writeToFile(subDir, "state.json", "do not copy");
+ writeToFile(subDir, "other.file", "do not copy");
+
+ // Perform migration.
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true with important telemetry files copied");
+
+ checkDirectoryContains(targetDir, {
+ "datareporting": {
+ "state.json": stateContent,
+ "session-state.json": sessionStateContent,
+ },
+ });
+});
+
+add_task(function* test_fallback_fhr_state() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Test that we fall back to migrating FHR state if the datareporting
+ // state file does not exist.
+ let stateContent = JSON.stringify({
+ clientId: "68d5474e-19dc-45c1-8e9a-81fca592707c",
+ });
+ let subDir = createSubDir(srcDir, "healthreport");
+ writeToFile(subDir, "state.json", stateContent);
+
+ // Perform migration.
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ "healthreport": {
+ "state.json": stateContent,
+ },
+ });
+});
+
+
+add_task(function* test_datareporting_not_dir() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ writeToFile(srcDir, "datareporting", "I'm a file but should be a directory");
+
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true even though the directory was a file");
+
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(function* test_datareporting_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with an empty 'datareporting' subdir.
+ createSubDir(srcDir, "datareporting");
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {
+ "datareporting": {},
+ });
+});
+
+add_task(function* test_healthreport_empty() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Migrate with no 'datareporting' and an empty 'healthreport' subdir.
+ createSubDir(srcDir, "healthreport");
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ // We should end up with no migrated files.
+ checkDirectoryContains(targetDir, {});
+});
+
+add_task(function* test_datareporting_many() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Create some datareporting files.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let shouldBeCopied = "should be copied";
+ writeToFile(subDir, "state.json", shouldBeCopied);
+ writeToFile(subDir, "session-state.json", shouldBeCopied);
+ writeToFile(subDir, "something.else", "should not");
+ createSubDir(subDir, "emptyDir");
+
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ "datareporting" : {
+ "state.json": shouldBeCopied,
+ "session-state.json": shouldBeCopied,
+ }
+ });
+});
+
+add_task(function* test_no_session_state() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Check that migration still works properly if we only have state.json.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let stateContent = "abcd984";
+ writeToFile(subDir, "state.json", stateContent);
+
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ "datareporting" : {
+ "state.json": stateContent,
+ }
+ });
+});
+
+add_task(function* test_no_state() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // Check that migration still works properly if we only have session-state.json.
+ let subDir = createSubDir(srcDir, "datareporting");
+ let sessionStateContent = "abcd512";
+ writeToFile(subDir, "session-state.json", sessionStateContent);
+
+ let ok = yield promiseTelemetryMigrator(srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+
+ checkDirectoryContains(targetDir, {
+ "datareporting" : {
+ "session-state.json": sessionStateContent,
+ }
+ });
+});
+
+add_task(function* test_times_migration() {
+ let [srcDir, targetDir] = getTestDirs();
+
+ // create a times.json in the source directory.
+ let contents = JSON.stringify({created: 1234});
+ writeToFile(srcDir, "times.json", contents);
+
+ let earliest = Date.now();
+ let ok = yield promiseMigrator("times", srcDir, targetDir);
+ Assert.ok(ok, "callback should have been true");
+ let latest = Date.now();
+
+ let timesFile = targetDir.clone();
+ timesFile.append("times.json");
+
+ let raw = readFile(timesFile);
+ let times = JSON.parse(raw);
+ Assert.ok(times.reset >= earliest && times.reset <= latest);
+ // and it should have left the creation time alone.
+ Assert.equal(times.created, 1234);
+});
diff --git a/browser/components/migration/tests/unit/xpcshell.ini b/browser/components/migration/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..1b9f0a5f1
--- /dev/null
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -0,0 +1,26 @@
+[DEFAULT]
+head = head_migration.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ Library/**
+ AppData/**
+
+[test_automigration.js]
+[test_Chrome_cookies.js]
+skip-if = os != "mac" # Relies on ULibDir
+[test_Chrome_passwords.js]
+skip-if = os != "win"
+[test_Edge_availability.js]
+[test_Edge_db_migration.js]
+skip-if = os != "win" || os_version == "5.1" || os_version == "5.2" # Relies on post-XP bits of ESEDB
+[test_fx_telemetry.js]
+[test_IE_bookmarks.js]
+skip-if = os != "win"
+[test_IE_cookies.js]
+skip-if = os != "win"
+[test_IE7_passwords.js]
+skip-if = os != "win"
+[test_Safari_bookmarks.js]
+skip-if = os != "mac"