diff options
Diffstat (limited to 'browser/components/migration/IEProfileMigrator.js')
-rw-r--r-- | browser/components/migration/IEProfileMigrator.js | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/browser/components/migration/IEProfileMigrator.js b/browser/components/migration/IEProfileMigrator.js new file mode 100644 index 000000000..ac055686c --- /dev/null +++ b/browser/components/migration/IEProfileMigrator.js @@ -0,0 +1,542 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +const kLoginsKey = "Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2"; +const kMainKey = "Software\\Microsoft\\Internet Explorer\\Main"; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */ +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */ +Cu.import("resource:///modules/MSMigrationUtils.jsm"); + + +XPCOMUtils.defineLazyModuleGetter(this, "ctypes", + "resource://gre/modules/ctypes.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OSCrypto", + "resource://gre/modules/OSCrypto.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry", + "resource://gre/modules/WindowsRegistry.jsm"); + +Cu.importGlobalProperties(["URL"]); + +// Resources + +function History() { +} + +History.prototype = { + type: MigrationUtils.resourceTypes.HISTORY, + + get exists() { + return true; + }, + + migrate: function H_migrate(aCallback) { + let places = []; + let typedURLs = MSMigrationUtils.getTypedURLs("Software\\Microsoft\\Internet Explorer"); + let historyEnumerator = Cc["@mozilla.org/profile/migrator/iehistoryenumerator;1"]. + createInstance(Ci.nsISimpleEnumerator); + while (historyEnumerator.hasMoreElements()) { + let entry = historyEnumerator.getNext().QueryInterface(Ci.nsIPropertyBag2); + let uri = entry.get("uri").QueryInterface(Ci.nsIURI); + // MSIE stores some types of URLs in its history that we don't handle, + // like HTMLHelp and others. Since we don't properly map handling for + // all of them we just avoid importing them. + if (["http", "https", "ftp", "file"].indexOf(uri.scheme) == -1) { + continue; + } + + let title = entry.get("title"); + // Embed visits have no title and don't need to be imported. + if (title.length == 0) { + continue; + } + + // The typed urls are already fixed-up, so we can use them for comparison. + let transitionType = typedURLs.has(uri.spec) ? + Ci.nsINavHistoryService.TRANSITION_TYPED : + Ci.nsINavHistoryService.TRANSITION_LINK; + // use the current date if we have no visits for this entry. + // Note that the entry will have a time in microseconds (PRTime), + // and Date.now() returns milliseconds. Places expects PRTime, + // so we multiply the Date.now return value to make up the difference. + let lastVisitTime = entry.get("time") || (Date.now() * 1000); + + places.push( + { uri: uri, + title: title, + visits: [{ transitionType: transitionType, + visitDate: lastVisitTime }] + } + ); + } + + // Check whether there is any history to import. + if (places.length == 0) { + aCallback(true); + return; + } + + MigrationUtils.insertVisitsWrapper(places, { + _success: false, + handleResult: function() { + // Importing any entry is considered a successful import. + this._success = true; + }, + handleError: function() {}, + handleCompletion: function() { + aCallback(this._success); + } + }); + } +}; + +// IE form password migrator supporting windows from XP until 7 and IE from 7 until 11 +function IE7FormPasswords() { + // used to distinguish between this migrator and other passwords migrators in tests. + this.name = "IE7FormPasswords"; +} + +IE7FormPasswords.prototype = { + type: MigrationUtils.resourceTypes.PASSWORDS, + + get exists() { + // work only on windows until 7 + if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { + return false; + } + + try { + let nsIWindowsRegKey = Ci.nsIWindowsRegKey; + let key = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(nsIWindowsRegKey); + key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kLoginsKey, + nsIWindowsRegKey.ACCESS_READ); + let count = key.valueCount; + key.close(); + return count > 0; + } catch (e) { + return false; + } + }, + + migrate(aCallback) { + let historyEnumerator = Cc["@mozilla.org/profile/migrator/iehistoryenumerator;1"]. + createInstance(Ci.nsISimpleEnumerator); + let uris = []; // the uris of the websites that are going to be migrated + while (historyEnumerator.hasMoreElements()) { + let entry = historyEnumerator.getNext().QueryInterface(Ci.nsIPropertyBag2); + let uri = entry.get("uri").QueryInterface(Ci.nsIURI); + // MSIE stores some types of URLs in its history that we don't handle, like HTMLHelp + // and others. Since we are not going to import the logins that are performed in these URLs + // we can just skip them. + if (["http", "https", "ftp"].indexOf(uri.scheme) == -1) { + continue; + } + + uris.push(uri); + } + this._migrateURIs(uris); + aCallback(true); + }, + + /** + * Migrate the logins that were saved for the uris arguments. + * @param {nsIURI[]} uris - the uris that are going to be migrated. + */ + _migrateURIs(uris) { + this.ctypesKernelHelpers = new MSMigrationUtils.CtypesKernelHelpers(); + this._crypto = new OSCrypto(); + let nsIWindowsRegKey = Ci.nsIWindowsRegKey; + let key = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(nsIWindowsRegKey); + key.open(nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, kLoginsKey, + nsIWindowsRegKey.ACCESS_READ); + + let urlsSet = new Set(); // set of the already processed urls. + // number of the successfully decrypted registry values + let successfullyDecryptedValues = 0; + /* The logins are stored in the registry, where the key is a hashed URL and its + * value contains the encrypted details for all logins for that URL. + * + * First iterate through IE history, hashing each URL and looking for a match. If + * found, decrypt the value, using the URL as a salt. Finally add any found logins + * to the Firefox password manager. + */ + + for (let uri of uris) { + try { + // remove the query and the ref parts of the URL + let urlObject = new URL(uri.spec); + let url = urlObject.origin + urlObject.pathname; + // if the current url is already processed, it should be skipped + if (urlsSet.has(url)) { + continue; + } + urlsSet.add(url); + // hash value of the current uri + let hashStr = this._crypto.getIELoginHash(url); + if (!key.hasValue(hashStr)) { + continue; + } + let value = key.readBinaryValue(hashStr); + // if no value was found, the uri is skipped + if (value == null) { + continue; + } + let data; + try { + // the url is used as salt to decrypt the registry value + data = this._crypto.decryptData(value, url, true); + } catch (e) { + continue; + } + // extract the login details from the decrypted data + let ieLogins = this._extractDetails(data, uri); + // if at least a credential was found in the current data, successfullyDecryptedValues should + // be incremented by one + if (ieLogins.length) { + successfullyDecryptedValues++; + } + this._addLogins(ieLogins); + } catch (e) { + Cu.reportError("Error while importing logins for " + uri.spec + ": " + e); + } + } + // if the number of the imported values is less than the number of values in the key, it means + // that not all the values were imported and an error should be reported + if (successfullyDecryptedValues < key.valueCount) { + Cu.reportError("We failed to decrypt and import some logins. " + + "This is likely because we didn't find the URLs where these " + + "passwords were submitted in the IE history and which are needed to be used " + + "as keys in the decryption."); + } + + key.close(); + this._crypto.finalize(); + this.ctypesKernelHelpers.finalize(); + }, + + _crypto: null, + + /** + * Add the logins to the password manager. + * @param {Object[]} logins - array of the login details. + */ + _addLogins(ieLogins) { + for (let ieLogin of ieLogins) { + try { + // create a new login + let login = { + username: ieLogin.username, + password: ieLogin.password, + hostname: ieLogin.url, + timeCreated: ieLogin.creation, + }; + MigrationUtils.insertLoginWrapper(login); + } catch (e) { + Cu.reportError(e); + } + } + }, + + /** + * Extract the details of one or more logins from the raw decrypted data. + * @param {string} data - the decrypted data containing raw information. + * @param {nsURI} uri - the nsURI of page where the login has occur. + * @returns {Object[]} array of objects where each of them contains the username, password, URL, + * and creation time representing all the logins found in the data arguments. + */ + _extractDetails(data, uri) { + // the structure of the header of the IE7 decrypted data for all the logins sharing the same URL + let loginData = new ctypes.StructType("loginData", [ + // Bytes 0-3 are not needed and not documented + {"unknown1": ctypes.uint32_t}, + // Bytes 4-7 are the header size + {"headerSize": ctypes.uint32_t}, + // Bytes 8-11 are the data size + {"dataSize": ctypes.uint32_t}, + // Bytes 12-19 are not needed and not documented + {"unknown2": ctypes.uint32_t}, + {"unknown3": ctypes.uint32_t}, + // Bytes 20-23 are the data count: each username and password is considered as a data + {"dataMax": ctypes.uint32_t}, + // Bytes 24-35 are not needed and not documented + {"unknown4": ctypes.uint32_t}, + {"unknown5": ctypes.uint32_t}, + {"unknown6": ctypes.uint32_t} + ]); + + // the structure of a IE7 decrypted login item + let loginItem = new ctypes.StructType("loginItem", [ + // Bytes 0-3 are the offset of the username + {"usernameOffset": ctypes.uint32_t}, + // Bytes 4-11 are the date + {"loDateTime": ctypes.uint32_t}, + {"hiDateTime": ctypes.uint32_t}, + // Bytes 12-15 are not needed and not documented + {"foo": ctypes.uint32_t}, + // Bytes 16-19 are the offset of the password + {"passwordOffset": ctypes.uint32_t}, + // Bytes 20-31 are not needed and not documented + {"unknown1": ctypes.uint32_t}, + {"unknown2": ctypes.uint32_t}, + {"unknown3": ctypes.uint32_t} + ]); + + let url = uri.prePath; + let results = []; + let arr = this._crypto.stringToArray(data); + // convert data to ctypes.unsigned_char.array(arr.length) + let cdata = ctypes.unsigned_char.array(arr.length)(arr); + // Bytes 0-35 contain the loginData data structure for all the logins sharing the same URL + let currentLoginData = ctypes.cast(cdata, loginData); + let headerSize = currentLoginData.headerSize; + let currentInfoIndex = loginData.size; + // pointer to the current login item + let currentLoginItemPointer = ctypes.cast(cdata.addressOfElement(currentInfoIndex), + loginItem.ptr); + // currentLoginData.dataMax is the data count: each username and password is considered as + // a data. So, the number of logins is the number of data dived by 2 + let numLogins = currentLoginData.dataMax / 2; + for (let n = 0; n < numLogins; n++) { + // Bytes 0-31 starting from currentInfoIndex contain the loginItem data structure for the + // current login + let currentLoginItem = currentLoginItemPointer.contents; + let creation = this.ctypesKernelHelpers. + fileTimeToSecondsSinceEpoch(currentLoginItem.hiDateTime, + currentLoginItem.loDateTime) * 1000; + let currentResult = { + creation: creation, + url: url, + }; + // The username is UTF-16 and null-terminated. + currentResult.username = + ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.usernameOffset), + ctypes.char16_t.ptr).readString(); + // The password is UTF-16 and null-terminated. + currentResult.password = + ctypes.cast(cdata.addressOfElement(headerSize + 12 + currentLoginItem.passwordOffset), + ctypes.char16_t.ptr).readString(); + results.push(currentResult); + // move to the next login item + currentLoginItemPointer = currentLoginItemPointer.increment(); + } + return results; + }, +}; + +function Settings() { +} + +Settings.prototype = { + type: MigrationUtils.resourceTypes.SETTINGS, + + get exists() { + return true; + }, + + migrate: function S_migrate(aCallback) { + // Converts from yes/no to a boolean. + let yesNoToBoolean = v => v == "yes"; + + // Converts source format like "en-us,ar-kw;q=0.7,ar-om;q=0.3" into + // destination format like "en-us, ar-kw, ar-om". + // Final string is sorted by quality (q=) param. + function parseAcceptLanguageList(v) { + return v.match(/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/gi) + .sort(function(a, b) { + let qA = parseFloat(a.split(";q=")[1]) || 1.0; + let qB = parseFloat(b.split(";q=")[1]) || 1.0; + return qB - qA; + }) + .map(a => a.split(";")[0]); + } + + // For reference on some of the available IE Registry settings: + // * http://msdn.microsoft.com/en-us/library/cc980058%28v=prot.13%29.aspx + // * http://msdn.microsoft.com/en-us/library/cc980059%28v=prot.13%29.aspx + + // Note that only settings exposed in our UI should be migrated. + + this._set("Software\\Microsoft\\Internet Explorer\\International", + "AcceptLanguage", + "intl.accept_languages", + parseAcceptLanguageList); + // TODO (bug 745853): For now, only x-western font is translated. + this._set("Software\\Microsoft\\Internet Explorer\\International\\Scripts\\3", + "IEFixedFontName", + "font.name.monospace.x-western"); + this._set(kMainKey, + "Use FormSuggest", + "browser.formfill.enable", + yesNoToBoolean); + this._set(kMainKey, + "FormSuggest Passwords", + "signon.rememberSignons", + yesNoToBoolean); + this._set(kMainKey, + "Anchor Underline", + "browser.underline_anchors", + yesNoToBoolean); + this._set(kMainKey, + "Display Inline Images", + "permissions.default.image", + v => yesNoToBoolean(v) ? 1 : 2); + this._set(kMainKey, + "Move System Caret", + "accessibility.browsewithcaret", + yesNoToBoolean); + this._set("Software\\Microsoft\\Internet Explorer\\Settings", + "Always Use My Colors", + "browser.display.document_color_use", + v => (!v ? 0 : 2)); + this._set("Software\\Microsoft\\Internet Explorer\\Settings", + "Always Use My Font Face", + "browser.display.use_document_fonts", + v => !v); + this._set(kMainKey, + "SmoothScroll", + "general.smoothScroll", + Boolean); + this._set("Software\\Microsoft\\Internet Explorer\\TabbedBrowsing\\", + "WarnOnClose", + "browser.tabs.warnOnClose", + Boolean); + this._set("Software\\Microsoft\\Internet Explorer\\TabbedBrowsing\\", + "OpenInForeground", + "browser.tabs.loadInBackground", + v => !v); + + aCallback(true); + }, + + /** + * Reads a setting from the Registry and stores the converted result into + * the appropriate Firefox preference. + * + * @param aPath + * Registry path under HKCU. + * @param aKey + * Name of the key. + * @param aPref + * Firefox preference. + * @param [optional] aTransformFn + * Conversion function from the Registry format to the pref format. + */ + _set: function S__set(aPath, aKey, aPref, aTransformFn) { + let value = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + aPath, aKey); + // Don't import settings that have never been flipped. + if (value === undefined) + return; + + if (aTransformFn) + value = aTransformFn(value); + + switch (typeof value) { + case "string": + Services.prefs.setCharPref(aPref, value); + break; + case "number": + Services.prefs.setIntPref(aPref, value); + break; + case "boolean": + Services.prefs.setBoolPref(aPref, value); + break; + default: + throw new Error("Unexpected value type: " + (typeof value)); + } + } +}; + +function IEProfileMigrator() +{ + this.wrappedJSObject = this; // export this to be able to use it in the unittest. +} + +IEProfileMigrator.prototype = Object.create(MigratorPrototype); + +IEProfileMigrator.prototype.getResources = function IE_getResources() { + let resources = [ + MSMigrationUtils.getBookmarksMigrator(), + new History(), + MSMigrationUtils.getCookiesMigrator(), + new Settings(), + ]; + // Only support the form password migrator for Windows XP to 7. + if (AppConstants.isPlatformAndVersionAtMost("win", "6.1")) { + resources.push(new IE7FormPasswords()); + } + let windowsVaultFormPasswordsMigrator = + MSMigrationUtils.getWindowsVaultFormPasswordsMigrator(); + windowsVaultFormPasswordsMigrator.name = "IEVaultFormPasswords"; + resources.push(windowsVaultFormPasswordsMigrator); + return resources.filter(r => r.exists); +}; + +IEProfileMigrator.prototype.getLastUsedDate = function IE_getLastUsedDate() { + let datePromises = ["Favs", "CookD"].map(dirId => { + let {path} = Services.dirsvc.get(dirId, Ci.nsIFile); + return OS.File.stat(path).catch(() => null).then(info => { + return info ? info.lastModificationDate : 0; + }); + }); + datePromises.push(new Promise(resolve => { + let typedURLs = new Map(); + try { + typedURLs = MSMigrationUtils.getTypedURLs("Software\\Microsoft\\Internet Explorer"); + } catch (ex) {} + let dates = [0, ... typedURLs.values()]; + resolve(Math.max.apply(Math, dates)); + })); + return Promise.all(datePromises).then(dates => { + return new Date(Math.max.apply(Math, dates)); + }); +}; + +Object.defineProperty(IEProfileMigrator.prototype, "sourceHomePageURL", { + get: function IE_get_sourceHomePageURL() { + let defaultStartPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + kMainKey, "Default_Page_URL"); + let startPage = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + kMainKey, "Start Page"); + // If the user didn't customize the Start Page, he is still on the default + // page, that may be considered the equivalent of our about:home. There's + // no reason to retain it, since it is heavily targeted to IE. + let homepage = startPage != defaultStartPage ? startPage : ""; + + // IE7+ supports secondary home pages located in a REG_MULTI_SZ key. These + // are in addition to the Start Page, and no empty entries are possible, + // thus a Start Page is always defined if any of these exists, though it + // may be the default one. + let secondaryPages = WindowsRegistry.readRegKey(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + kMainKey, "Secondary Start Pages"); + if (secondaryPages) { + if (homepage) + secondaryPages.unshift(homepage); + homepage = secondaryPages.join("|"); + } + + return homepage; + } +}); + +IEProfileMigrator.prototype.classDescription = "IE Profile Migrator"; +IEProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=ie"; +IEProfileMigrator.prototype.classID = Components.ID("{3d2532e3-4932-4774-b7ba-968f5899d3a4}"); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([IEProfileMigrator]); |