diff options
Diffstat (limited to 'application/basilisk/components/migration')
23 files changed, 6863 insertions, 0 deletions
diff --git a/application/basilisk/components/migration/.eslintrc.js b/application/basilisk/components/migration/.eslintrc.js new file mode 100644 index 000000000..570cd076b --- /dev/null +++ b/application/basilisk/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, "ArrayExpression": "first", "ObjectExpression": "first"}], + // "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/application/basilisk/components/migration/360seProfileMigrator.js b/application/basilisk/components/migration/360seProfileMigrator.js new file mode 100644 index 000000000..83e2880b1 --- /dev/null +++ b/application/basilisk/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() { + 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/application/basilisk/components/migration/AutoMigrate.jsm b/application/basilisk/components/migration/AutoMigrate.jsm new file mode 100644 index 000000000..b38747825 --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/BrowserProfileMigrators.manifest b/application/basilisk/components/migration/BrowserProfileMigrators.manifest new file mode 100644 index 000000000..e16fba13a --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/ChromeProfileMigrator.js b/application/basilisk/components/migration/ChromeProfileMigrator.js new file mode 100644 index 000000000..3a7ef2396 --- /dev/null +++ b/application/basilisk/components/migration/ChromeProfileMigrator.js @@ -0,0 +1,534 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 sts=2 et */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; + +const FILE_INPUT_STREAM_CID = "@mozilla.org/network/file-input-stream;1"; + +const S100NS_FROM1601TO1970 = 0x19DB1DED53E8000; +const S100NS_PER_MS = 10; + +const AUTH_TYPE = { + SCHEME_HTML: 0, + SCHEME_BASIC: 1, + SCHEME_DIGEST: 2 +}; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/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; +#ifdef XP_WIN + dirServiceID = "LocalAppData"; + subfolders = subfoldersWin.concat(["User Data"]); +#elif XP_MACOSX + dirServiceID = "ULibDir"; + subfolders = ["Application Support"].concat(subfoldersOSX); +#else + dirServiceID = "Home"; + subfolders = [".config"].concat(subfoldersUnix); +#endif + 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; +} + +/** + * Converts an array of chrome bookmark objects into one our own places code + * understands. + * + * @param items + * bookmark items to be inserted on this parent + * @param errorAccumulator + * function that gets called with any errors thrown so we don't drop them on the floor. + */ +function convertBookmarks(items, errorAccumulator) { + let itemsToInsert = []; + for (let item of items) { + try { + if (item.type == "url") { + if (item.url.trim().startsWith("chrome:")) { + // Skip invalid chrome URIs. Creating an actual URI always reports + // messages to the console, so we avoid doing that. + continue; + } + itemsToInsert.push({url: item.url, title: item.name}); + } else if (item.type == "folder") { + let folderItem = {type: PlacesUtils.bookmarks.TYPE_FOLDER, title: item.name}; + folderItem.children = convertBookmarks(item.children, errorAccumulator); + itemsToInsert.push(folderItem); + } + } catch (ex) { + Cu.reportError(ex); + errorAccumulator(ex); + } + } + return itemsToInsert; +} + +function ChromeProfileMigrator() { + let 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), + ]; +#ifdef XP_WIN + possibleResources.push(GetWindowsPasswordsResource(profileFolder)); +#endif + 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(aCallback) { + return Task.spawn(function* () { + let gotErrors = false; + let errorGatherer = function() { gotErrors = true }; + // Parse Chrome bookmark file that is JSON format + let bookmarkJSON = yield OS.File.read(bookmarksFile.path, {encoding: "UTF-8"}); + let roots = JSON.parse(bookmarkJSON).roots; + + // Importing bookmark bar items + if (roots.bookmark_bar.children && + roots.bookmark_bar.children.length > 0) { + // Toolbar + let parentGuid = PlacesUtils.bookmarks.toolbarGuid; + let bookmarks = convertBookmarks(roots.bookmark_bar.children, errorGatherer); + if (!MigrationUtils.isStartupMigration) { + parentGuid = + yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid); + } + yield MigrationUtils.insertManyBookmarksWrapper(bookmarks, parentGuid); + } + + // Importing bookmark menu items + if (roots.other.children && + roots.other.children.length > 0) { + // Bookmark menu + let parentGuid = PlacesUtils.bookmarks.menuGuid; + let bookmarks = convertBookmarks(roots.other.children, errorGatherer); + if (!MigrationUtils.isStartupMigration) { + parentGuid + = yield MigrationUtils.createImportedBookmarksFolder("Chrome", parentGuid); + } + yield MigrationUtils.insertManyBookmarksWrapper(bookmarks, parentGuid); + } + if (gotErrors) { + throw new Error("The migration included errors."); + } + }).then(() => aCallback(true), + () => aCallback(false)); + } + }; +} + +function GetHistoryResource(aProfileFolder) { + let historyFile = aProfileFolder.clone(); + historyFile.append("History"); + if (!historyFile.exists()) + return null; + + return { + type: MigrationUtils.resourceTypes.HISTORY, + + migrate(aCallback) { + 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, { + ignoreErrors: true, + ignoreResults: true, + handleCompletion(updatedCount) { + if (updatedCount > 0) { + resolve(); + } else { + reject(new Error("Couldn't add visits")); + } + } + }); + }); + } + }).then(() => { aCallback(true) }, + ex => { + Cu.reportError(ex); + aCallback(false); + }); + } + }; +} + +function GetCookiesResource(aProfileFolder) { + let cookiesFile = aProfileFolder.clone(); + cookiesFile.append("Cookies"); + if (!cookiesFile.exists()) + return null; + + return { + type: MigrationUtils.resourceTypes.COOKIES, + + 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 !defined(XP_WIN) || !defined(XP_MACOSX) +componentsArray.push(CanaryProfileMigrator); +#endif + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(componentsArray); diff --git a/application/basilisk/components/migration/ESEDBReader.jsm b/application/basilisk/components/migration/ESEDBReader.jsm new file mode 100644 index 000000000..6192c8667 --- /dev/null +++ b/application/basilisk/components/migration/ESEDBReader.jsm @@ -0,0 +1,608 @@ +/* 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); +}); + +XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); + +// 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 -1032 /* JET_errFileAccessDenied */: + 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(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 + "\\"); + }, + + async dbLocked(dbFile) { + let options = {winShare: OS.Constants.Win.FILE_SHARE_READ}; + let locked = true; + await OS.File.open(dbFile.path, {read: true}, options).then(fileHandle => { + locked = false; + // Return the close promise so we wait for the file to be closed again. + // Otherwise the file might still be kept open by this handle by the time + // that we try to use the ESE APIs to access it. + return fileHandle.close(); + }, () => { + Cu.reportError("ESE DB at " + dbFile.path + " is locked."); + }); + return locked; + }, + + closeDB(db) { + db.decrementReferenceCounter(); + }, + + COLUMN_TYPES, +}; + diff --git a/application/basilisk/components/migration/EdgeProfileMigrator.js b/application/basilisk/components/migration/EdgeProfileMigrator.js new file mode 100644 index 000000000..5020fa2a7 --- /dev/null +++ b/application/basilisk/components/migration/EdgeProfileMigrator.js @@ -0,0 +1,425 @@ +/* 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/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"); + +Cu.importGlobalProperties(["URL"]); + +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()) { + expectedLocation.normalize(); + 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(aCallback) { + let typedURLs = this._typedURLs; + let places = []; + for (let [urlString, time] of typedURLs) { + let uri; + try { + uri = Services.io.newURI(urlString); + 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, { + ignoreErrors: true, + ignoreResults: true, + handleCompletion(updatedCount) { + aCallback(updatedCount > 0); + } + }); + }, +}; + +function EdgeReadingListMigrator(dbOverride) { + this.dbOverride = dbOverride; +} + +EdgeReadingListMigrator.prototype = { + type: MigrationUtils.resourceTypes.BOOKMARKS, + + get db() { return this.dbOverride || gEdgeDatabase }, + + get exists() { + return !!this.db; + }, + + migrate(callback) { + this._migrateReadingList(PlacesUtils.bookmarks.menuGuid).then( + () => callback(true), + ex => { + Cu.reportError(ex); + callback(false); + } + ); + }, + + _migrateReadingList: Task.async(function*(parentGuid) { + if (yield ESEDBReader.dbLocked(this.db)) { + throw new Error("Edge seems to be running - its database is locked."); + } + 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, this.db); + if (!readingListItems.length) { + return; + } + + let destFolderGuid = yield this._ensureReadingListFolder(parentGuid); + let bookmarks = []; + for (let item of readingListItems) { + let dateAdded = item.AddedDate || new Date(); + // Avoid including broken URLs: + try { + new URL(item.URL); + } catch (ex) { + continue; + } + bookmarks.push({ url: item.URL, title: item.Title, dateAdded }); + } + yield MigrationUtils.insertManyBookmarksWrapper(bookmarks, destFolderGuid); + }), + + _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().then( + () => callback(true), + ex => { + Cu.reportError(ex); + callback(false); + } + ); + }, + + _migrateBookmarks: Task.async(function*() { + if (yield ESEDBReader.dbLocked(this.db)) { + throw new Error("Edge seems to be running - its database is locked."); + } + let {toplevelBMs, toolbarBMs} = this._fetchBookmarksFromDB(); + if (toplevelBMs.length) { + let parentGuid = PlacesUtils.bookmarks.menuGuid; + if (!MigrationUtils.isStartupMigration) { + parentGuid = yield MigrationUtils.createImportedBookmarksFolder("Edge", parentGuid); + } + yield MigrationUtils.insertManyBookmarksWrapper(toplevelBMs, parentGuid); + } + if (toolbarBMs.length) { + let parentGuid = PlacesUtils.bookmarks.toolbarGuid; + if (!MigrationUtils.isStartupMigration) { + parentGuid = yield MigrationUtils.createImportedBookmarksFolder("Edge", parentGuid); + } + yield MigrationUtils.insertManyBookmarksWrapper(toolbarBMs, parentGuid); + } + }), + + _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); + let toplevelBMs = [], toolbarBMs = []; + for (let bookmark of bookmarks) { + let bmToInsert; + // Ignore invalid URLs: + if (!bookmark.IsFolder) { + try { + new URL(bookmark.URL); + } catch (ex) { + Cu.reportError(`Ignoring ${bookmark.URL} when importing from Edge because of exception: ${ex}`); + continue; + } + bmToInsert = { + dateAdded: bookmark.DateUpdated || new Date(), + title: bookmark.Title, + url: bookmark.URL, + }; + } else /* bookmark.IsFolder */ { + // Ignore the favorites bar bookmark itself. + if (bookmark.Title == "_Favorites_Bar_") { + continue; + } + if (!bookmark._childrenRef) { + bookmark._childrenRef = []; + } + bmToInsert = { + title: bookmark.Title, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + dateAdded: bookmark.DateUpdated || new Date(), + children: bookmark._childrenRef, + }; + } + + if (!folderMap.has(bookmark.ParentId)) { + toplevelBMs.push(bmToInsert); + } else { + let parent = folderMap.get(bookmark.ParentId); + if (parent.Title == "_Favorites_Bar_") { + toolbarBMs.push(bmToInsert); + continue; + } + if (!parent._childrenRef) { + parent._childrenRef = []; + } + parent._childrenRef.push(bmToInsert); + } + } + return {toplevelBMs, toolbarBMs}; + }, +}; + +function EdgeProfileMigrator() { + this.wrappedJSObject = this; +} + +EdgeProfileMigrator.prototype = Object.create(MigratorPrototype); + +EdgeProfileMigrator.prototype.getBookmarksMigratorForTesting = function(dbOverride) { + return new EdgeBookmarksMigrator(dbOverride); +}; + +EdgeProfileMigrator.prototype.getReadingListMigratorForTesting = function(dbOverride) { + return new EdgeReadingListMigrator(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 = Services.vc.compare(Services.sysinfo.getProperty("version"), "10") >= 0; + 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/application/basilisk/components/migration/FirefoxProfileMigrator.js b/application/basilisk/components/migration/FirefoxProfileMigrator.js new file mode 100644 index 000000000..d62218784 --- /dev/null +++ b/application/basilisk/components/migration/FirefoxProfileMigrator.js @@ -0,0 +1,252 @@ +/* -*- 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 Basilisk 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"); + +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() { + 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(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(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/application/basilisk/components/migration/IEProfileMigrator.js b/application/basilisk/components/migration/IEProfileMigrator.js new file mode 100644 index 000000000..cb7a8943b --- /dev/null +++ b/application/basilisk/components/migration/IEProfileMigrator.js @@ -0,0 +1,411 @@ +/* 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/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, + title, + visits: [{ transitionType, + visitDate: lastVisitTime }] + } + ); + } + + // Check whether there is any history to import. + if (places.length == 0) { + aCallback(true); + return; + } + + MigrationUtils.insertVisitsWrapper(places, { + ignoreErrors: true, + ignoreResults: true, + handleCompletion(updatedCount) { + aCallback(updatedCount > 0); + } + }); + } +}; + +// 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 (Services.vc.compare(Services.sysinfo.getProperty("version"), "6.2") >= 0) { + 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, + 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 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(), + ]; + // Only support the form password migrator for Windows XP to 7. + if (Services.vc.compare(Services.sysinfo.getProperty("version"), "6.1") >= 0) { + 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/application/basilisk/components/migration/MSMigrationUtils.jsm b/application/basilisk/components/migration/MSMigrationUtils.jsm new file mode 100644 index 000000000..70d04a124 --- /dev/null +++ b/application/basilisk/components/migration/MSMigrationUtils.jsm @@ -0,0 +1,882 @@ +/* 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/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) { + let bookmarks = yield this._getBookmarksInFolder(aSourceFolder); + if (bookmarks.length) { + yield MigrationUtils.insertManyBookmarksWrapper(bookmarks, aDestFolderGuid); + } + }), + + _getBookmarksInFolder: Task.async(function* (aSourceFolder) { + // 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 rv = []; + 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 isBookmarksFolder = entry.leafName == this._toolbarFolderName && + entry.parent.equals(this._favoritesFolder); + if (isBookmarksFolder && entry.isReadable()) { + // Import to the bookmarks toolbar. + let folderGuid = PlacesUtils.bookmarks.toolbarGuid; + if (!MigrationUtils.isStartupMigration) { + folderGuid = + yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid); + } + yield this._migrateFolder(entry, folderGuid); + } else if (entry.isReadable()) { + let childBookmarks = yield this._getBookmarksInFolder(entry); + rv.push({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: entry.leafName, + children: childBookmarks, + }); + } + } 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); + rv.push({url: uri, title: matches[1]}); + } + } + } catch (ex) { + Components.utils.reportError("Unable to import " + this.importedAppLabel + " favorite (" + entry.leafName + "): " + ex); + } + } + return rv; + }), + +}; + +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); + + 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); + 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 (Services.vc.compare(Services.sysinfo.getProperty("version"), "6.2") >= 0) { + // 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); + } 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, + 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/application/basilisk/components/migration/MigrationUtils.jsm b/application/basilisk/components/migration/MigrationUtils.jsm new file mode 100644 index 000000000..fae959852 --- /dev/null +++ b/application/basilisk/components/migration/MigrationUtils.jsm @@ -0,0 +1,1135 @@ +/* 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/. */ + +#filter substitution + +"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/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() { +#ifdef XP_WIN + return [ + "firefox", "edge", "ie", "chrome", "chromium", "360se", + "canary" + ]; +#elif XP_MACOSX + return ["firefox", "safari", "chrome", "chromium", "canary"]; +#elif XP_UNIX + return ["firefox", "chrome", "chromium"]; +#else + return []; +#endif +}); + +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", + "Basilisk": "firefox", + "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); + } + +#ifdef XP_WIN + // "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" && Services.vc.compare(Services.sysinfo.getProperty("version"), "6.2") >= 0) { + // 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."); + } + } + } + } +#endif + + 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"; +#ifdef XP_MACOSX + if (!this.isStartupMigration) { + let win = Services.wm.getMostRecentWindow("Browser:MigrationWizard"); + if (win) { + win.focus(); + return; + } + // On mac, the migration wiazrd should only be modal in the case of + // startup-migration. + features = "centerscreen,chrome,resizable=no"; + } +#endif + + // 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 == "@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; + }); + }, + + insertManyBookmarksWrapper(bookmarks, parent) { + let insertionPromise = PlacesUtils.bookmarks.insertTree({guid: parent, children: bookmarks}); + return insertionPromise.then(insertedItems => { + this._importQuantities.bookmarks += insertedItems.length; + if (gKeepUndoData) { + let bmData = gUndoData.get("bookmarks"); + for (let bm of insertedItems) { + let {parentGuid, guid, lastModified, type} = bm; + bmData.push({parentGuid, guid, lastModified, type}); + } + } + }, ex => Cu.reportError(ex)); + }, + + insertVisitsWrapper(places, options) { + this._importQuantities.history += places.length; + if (gKeepUndoData) { + this._updateHistoryUndo(places); + } + return PlacesUtils.asyncHistory.updatePlaces(places, options, true); + }, + + 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, last; + if (visitCount > 1) { + let visitDates = place.visits.map(v => v.visitDate); + first = Math.min.apply(Math, visitDates); + last = Math.max.apply(Math, visitDates); + } else { + first = last = place.visits[0].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/application/basilisk/components/migration/ProfileMigrator.js b/application/basilisk/components/migration/ProfileMigrator.js new file mode 100644 index 000000000..f67823bae --- /dev/null +++ b/application/basilisk/components/migration/ProfileMigrator.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource:///modules/MigrationUtils.jsm"); + +function ProfileMigrator() { +} + +ProfileMigrator.prototype = { + migrate: MigrationUtils.startupMigration.bind(MigrationUtils), + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIProfileMigrator]), + classDescription: "Profile Migrator", + contractID: "@mozilla.org/toolkit/profile-migrator;1", + classID: Components.ID("6F8BB968-C14F-4D6F-9733-6C6737B35DCE") +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ProfileMigrator]); diff --git a/application/basilisk/components/migration/SafariProfileMigrator.js b/application/basilisk/components/migration/SafariProfileMigrator.js new file mode 100644 index 000000000..cc2bd1d8f --- /dev/null +++ b/application/basilisk/components/migration/SafariProfileMigrator.js @@ -0,0 +1,420 @@ +/* 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/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"); + +Cu.importGlobalProperties(["URL"]); + +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(entries, parentGuid) { + let convertedEntries = this._convertEntries(entries); + return MigrationUtils.insertManyBookmarksWrapper(convertedEntries, parentGuid); + }, + + _convertEntries(entries) { + return entries.map(function(entry) { + let type = entry.get("WebBookmarkType"); + if (type == "WebBookmarkTypeList" && entry.has("Children")) { + return { + title: entry.get("Title"), + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: this._convertEntries(entry.get("Children")), + }; + } + if (type == "WebBookmarkTypeLeaf" && entry.has("URLString")) { + // Check we understand this URL before adding it: + let url = entry.get("URLString"); + try { + new URL(url); + } catch (ex) { + Cu.reportError(`Ignoring ${url} when importing from Safari because of exception: ${ex}`); + return null; + } + let title; + if (entry.has("URIDictionary")) + title = entry.get("URIDictionary").get("title"); + return { url, title }; + } + return null; + }, this).filter(e => !!e); + }, +}; + +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 }] }); + } catch (ex) { + // Safari's History file may contain malformed URIs which + // will be ignored. + Cu.reportError(ex); + } + } + } + if (places.length > 0) { + MigrationUtils.insertVisitsWrapper(places, { + ignoreErrors: true, + ignoreResults: true, + handleCompletion(updatedCount) { + aCallback(updatedCount > 0); + } + }); + } 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 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); + } + } + }, 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 SearchStrings(prefs)); + } + + 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/application/basilisk/components/migration/content/aboutWelcomeBack.xhtml b/application/basilisk/components/migration/content/aboutWelcomeBack.xhtml new file mode 100644 index 000000000..d9fdb6c2c --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/content/extra-migration-strings.properties b/application/basilisk/components/migration/content/extra-migration-strings.properties new file mode 100644 index 000000000..208906b31 --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/content/migration.js b/application/basilisk/components/migration/content/migration.js new file mode 100644 index 000000000..c5d485f12 --- /dev/null +++ b/application/basilisk/components/migration/content/migration.js @@ -0,0 +1,524 @@ +/* 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() { + 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() { + 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() { + // 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() { + 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() { + // 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() { + var profiles = document.getElementById("profiles"); + this._selectedProfile = this._migrator.sourceProfiles.find( + profile => profile.id == profiles.selectedItem.id + ) || null; + }, + + onSelectProfilePageAdvanced() { + 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() { + 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() { + this._wiz.canAdvance = true; + this.onImportItemsPageAdvanced(); + }, + + onImportItemsPageAdvanced() { + 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() { + 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() { + // 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() { + // 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() { + 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() { + 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(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(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() { + 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/application/basilisk/components/migration/content/migration.xul b/application/basilisk/components/migration/content/migration.xul new file mode 100644 index 000000000..cf096144e --- /dev/null +++ b/application/basilisk/components/migration/content/migration.xul @@ -0,0 +1,105 @@ +<?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();"> + <description id="importAll" control="importSourceGroup">&importFrom.label;</description> + <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/application/basilisk/components/migration/jar.mn b/application/basilisk/components/migration/jar.mn new file mode 100644 index 000000000..110788bc4 --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/moz.build b/application/basilisk/components/migration/moz.build new file mode 100644 index 000000000..be91a7290 --- /dev/null +++ b/application/basilisk/components/migration/moz.build @@ -0,0 +1,58 @@ +# -*- 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/. + +JAR_MANIFESTS += ['jar.mn'] + +XPIDL_SOURCES += [ + 'nsIBrowserProfileMigrator.idl', +] + +XPIDL_MODULE = 'migration' + +EXTRA_COMPONENTS += [ + 'FirefoxProfileMigrator.js', + 'ProfileMigrator.js', +] + +EXTRA_PP_COMPONENTS += [ + 'BrowserProfileMigrators.manifest', + 'ChromeProfileMigrator.js', +] + +EXTRA_JS_MODULES += [ + 'AutoMigrate.jsm', +] + +EXTRA_PP_JS_MODULES += [ + '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_PP_COMPONENTS += [ + 'SafariProfileMigrator.js', + ] + DEFINES['HAS_SAFARI_MIGRATOR'] = True + +DEFINES['MOZ_APP_NAME'] = CONFIG['MOZ_APP_NAME'] + +FINAL_LIBRARY = 'browsercomps' diff --git a/application/basilisk/components/migration/nsIBrowserProfileMigrator.idl b/application/basilisk/components/migration/nsIBrowserProfileMigrator.idl new file mode 100644 index 000000000..a251c3683 --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/nsIEHistoryEnumerator.cpp b/application/basilisk/components/migration/nsIEHistoryEnumerator.cpp new file mode 100644 index 000000000..116e9a860 --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/nsIEHistoryEnumerator.h b/application/basilisk/components/migration/nsIEHistoryEnumerator.h new file mode 100644 index 000000000..1572a8dd5 --- /dev/null +++ b/application/basilisk/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/application/basilisk/components/migration/nsWindowsMigrationUtils.h b/application/basilisk/components/migration/nsWindowsMigrationUtils.h new file mode 100644 index 000000000..0288d93d3 --- /dev/null +++ b/application/basilisk/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 + |