diff options
Diffstat (limited to 'browser/components/migration/SafariProfileMigrator.js')
-rw-r--r-- | browser/components/migration/SafariProfileMigrator.js | 650 |
1 files changed, 650 insertions, 0 deletions
diff --git a/browser/components/migration/SafariProfileMigrator.js b/browser/components/migration/SafariProfileMigrator.js new file mode 100644 index 000000000..6a2dbfcb1 --- /dev/null +++ b/browser/components/migration/SafariProfileMigrator.js @@ -0,0 +1,650 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); /* globals OS */ +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/MigrationUtils.jsm"); /* globals MigratorPrototype */ + +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PropertyListUtils", + "resource://gre/modules/PropertyListUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); + +function Bookmarks(aBookmarksFile) { + this._file = aBookmarksFile; +} +Bookmarks.prototype = { + type: MigrationUtils.resourceTypes.BOOKMARKS, + + migrate: function B_migrate(aCallback) { + return Task.spawn(function* () { + let dict = yield new Promise(resolve => + PropertyListUtils.read(this._file, resolve) + ); + if (!dict) + throw new Error("Could not read Bookmarks.plist"); + let children = dict.get("Children"); + if (!children) + throw new Error("Invalid Bookmarks.plist format"); + + let collection = dict.get("Title") == "com.apple.ReadingList" ? + this.READING_LIST_COLLECTION : this.ROOT_COLLECTION; + yield this._migrateCollection(children, collection); + }.bind(this)).then(() => aCallback(true), + e => { Cu.reportError(e); aCallback(false) }); + }, + + // Bookmarks collections in Safari. Constants for migrateCollection. + ROOT_COLLECTION: 0, + MENU_COLLECTION: 1, + TOOLBAR_COLLECTION: 2, + READING_LIST_COLLECTION: 3, + + /** + * Recursively migrate a Safari collection of bookmarks. + * + * @param aEntries + * the collection's children + * @param aCollection + * one of the values above. + */ + _migrateCollection: Task.async(function* (aEntries, aCollection) { + // A collection of bookmarks in Safari resembles places roots. In the + // property list files (Bookmarks.plist, ReadingList.plist) they are + // stored as regular bookmarks folders, and thus can only be distinguished + // from by their names and places in the hierarchy. + + let entriesFiltered = []; + if (aCollection == this.ROOT_COLLECTION) { + for (let entry of aEntries) { + let type = entry.get("WebBookmarkType"); + if (type == "WebBookmarkTypeList" && entry.has("Children")) { + let title = entry.get("Title"); + let children = entry.get("Children"); + if (title == "BookmarksBar") + yield this._migrateCollection(children, this.TOOLBAR_COLLECTION); + else if (title == "BookmarksMenu") + yield this._migrateCollection(children, this.MENU_COLLECTION); + else if (title == "com.apple.ReadingList") + yield this._migrateCollection(children, this.READING_LIST_COLLECTION); + else if (entry.get("ShouldOmitFromUI") !== true) + entriesFiltered.push(entry); + } + else if (type == "WebBookmarkTypeLeaf") { + entriesFiltered.push(entry); + } + } + } + else { + entriesFiltered = aEntries; + } + + if (entriesFiltered.length == 0) + return; + + let folderGuid = -1; + switch (aCollection) { + case this.ROOT_COLLECTION: { + // In Safari, it is possible (though quite cumbersome) to move + // bookmarks to the bookmarks root, which is the parent folder of + // all bookmarks "collections". That is somewhat in parallel with + // both the places root and the unfiled-bookmarks root. + // Because the former is only an implementation detail in our UI, + // the unfiled root seems to be the best choice. + folderGuid = PlacesUtils.bookmarks.unfiledGuid; + break; + } + case this.MENU_COLLECTION: { + folderGuid = PlacesUtils.bookmarks.menuGuid; + if (!MigrationUtils.isStartupMigration) { + folderGuid = + yield MigrationUtils.createImportedBookmarksFolder("Safari", folderGuid); + } + break; + } + case this.TOOLBAR_COLLECTION: { + folderGuid = PlacesUtils.bookmarks.toolbarGuid; + if (!MigrationUtils.isStartupMigration) { + folderGuid = + yield MigrationUtils.createImportedBookmarksFolder("Safari", folderGuid); + } + break; + } + case this.READING_LIST_COLLECTION: { + // Reading list items are imported as regular bookmarks. + // They are imported under their own folder, created either under the + // bookmarks menu (in the case of startup migration). + folderGuid = (yield MigrationUtils.insertBookmarkWrapper({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: MigrationUtils.getLocalizedString("importedSafariReadingList"), + })).guid; + break; + } + default: + throw new Error("Unexpected value for aCollection!"); + } + if (folderGuid == -1) + throw new Error("Invalid folder GUID"); + + yield this._migrateEntries(entriesFiltered, folderGuid); + }), + + // migrate the given array of safari bookmarks to the given places + // folder. + _migrateEntries: Task.async(function* (entries, parentGuid) { + for (let entry of entries) { + let type = entry.get("WebBookmarkType"); + if (type == "WebBookmarkTypeList" && entry.has("Children")) { + let title = entry.get("Title"); + let newFolderGuid = (yield MigrationUtils.insertBookmarkWrapper({ + parentGuid, type: PlacesUtils.bookmarks.TYPE_FOLDER, title + })).guid; + + // Empty folders may not have a children array. + if (entry.has("Children")) + yield this._migrateEntries(entry.get("Children"), newFolderGuid, false); + } + else if (type == "WebBookmarkTypeLeaf" && entry.has("URLString")) { + let title; + if (entry.has("URIDictionary")) + title = entry.get("URIDictionary").get("title"); + + try { + yield MigrationUtils.insertBookmarkWrapper({ + parentGuid, url: entry.get("URLString"), title + }); + } catch (ex) { + Cu.reportError("Invalid Safari bookmark: " + ex); + } + } + } + }) +}; + +function History(aHistoryFile) { + this._file = aHistoryFile; +} +History.prototype = { + type: MigrationUtils.resourceTypes.HISTORY, + + // Helper method for converting the visit date property to a PRTime value. + // The visit date is stored as a string, so it's not read as a Date + // object by PropertyListUtils. + _parseCocoaDate: function H___parseCocoaDate(aCocoaDateStr) { + let asDouble = parseFloat(aCocoaDateStr); + if (!isNaN(asDouble)) { + // reference date of NSDate. + let date = new Date("1 January 2001, GMT"); + date.setMilliseconds(asDouble * 1000); + return date * 1000; + } + return 0; + }, + + migrate: function H_migrate(aCallback) { + PropertyListUtils.read(this._file, function migrateHistory(aDict) { + try { + if (!aDict) + throw new Error("Could not read history property list"); + if (!aDict.has("WebHistoryDates")) + throw new Error("Unexpected history-property list format"); + + // Safari's History file contains only top-level urls. It does not + // distinguish between typed urls and linked urls. + let transType = PlacesUtils.history.TRANSITION_LINK; + + let places = []; + let entries = aDict.get("WebHistoryDates"); + for (let entry of entries) { + if (entry.has("lastVisitedDate")) { + let visitDate = this._parseCocoaDate(entry.get("lastVisitedDate")); + try { + places.push({ uri: NetUtil.newURI(entry.get("")), + title: entry.get("title"), + visits: [{ transitionType: transType, + visitDate: visitDate }] }); + } + catch (ex) { + // Safari's History file may contain malformed URIs which + // will be ignored. + Cu.reportError(ex); + } + } + } + if (places.length > 0) { + MigrationUtils.insertVisitsWrapper(places, { + _success: false, + handleResult: function() { + // Importing any entry is considered a successful import. + this._success = true; + }, + handleError: function() {}, + handleCompletion: function() { + aCallback(this._success); + } + }); + } + else { + aCallback(false); + } + } + catch (ex) { + Cu.reportError(ex); + aCallback(false); + } + }.bind(this)); + } +}; + +/** + * Safari's preferences property list is independently used for three purposes: + * (a) importation of preferences + * (b) importation of search strings + * (c) retrieving the home page. + * + * So, rather than reading it three times, it's cached and managed here. + */ +function MainPreferencesPropertyList(aPreferencesFile) { + this._file = aPreferencesFile; + this._callbacks = []; +} +MainPreferencesPropertyList.prototype = { + /** + * @see PropertyListUtils.read + */ + read: function MPPL_read(aCallback) { + if ("_dict" in this) { + aCallback(this._dict); + return; + } + + let alreadyReading = this._callbacks.length > 0; + this._callbacks.push(aCallback); + if (!alreadyReading) { + PropertyListUtils.read(this._file, function readPrefs(aDict) { + this._dict = aDict; + for (let callback of this._callbacks) { + try { + callback(aDict); + } + catch (ex) { + Cu.reportError(ex); + } + } + this._callbacks.splice(0); + }.bind(this)); + } + }, + + // Workaround for nsIBrowserProfileMigrator.sourceHomePageURL until + // it's replaced with an async method. + _readSync: function MPPL__readSync() { + if ("_dict" in this) + return this._dict; + + let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + inputStream.init(this._file, -1, -1, 0); + let binaryStream = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + binaryStream.setInputStream(inputStream); + let bytes = binaryStream.readByteArray(inputStream.available()); + this._dict = PropertyListUtils._readFromArrayBufferSync( + new Uint8Array(bytes).buffer); + return this._dict; + } +}; + +function Preferences(aMainPreferencesPropertyListInstance) { + this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance; +} +Preferences.prototype = { + type: MigrationUtils.resourceTypes.SETTINGS, + + migrate: function MPR_migrate(aCallback) { + this._mainPreferencesPropertyList.read(aDict => { + Task.spawn(function* () { + if (!aDict) + throw new Error("Could not read preferences file"); + + this._dict = aDict; + + let invert = webkitVal => !webkitVal; + this._set("AutoFillPasswords", "signon.rememberSignons"); + this._set("OpenNewTabsInFront", "browser.tabs.loadInBackground", invert); + this._set("WebKitJavaScriptCanOpenWindowsAutomatically", + "dom.disable_open_during_load", invert); + + // layout.spellcheckDefault is a boolean stored as a number. + this._set("WebContinuousSpellCheckingEnabled", + "layout.spellcheckDefault", Number); + + // Auto-load images + // Firefox has an elaborate set of Image preferences. The correlation is: + // Mode: Safari Firefox + // Blocked FALSE 2 + // Allowed TRUE 1 + // Allowed, originating site only -- 3 + this._set("WebKitDisplayImagesKey", "permissions.default.image", + webkitVal => webkitVal ? 1 : 2); + + this._migrateFontSettings(); + yield this._migrateDownloadsFolder(); + }.bind(this)).then(() => aCallback(true), ex => { + Cu.reportError(ex); + aCallback(false); + }).catch(Cu.reportError); + }); + }, + + /** + * Attempts to migrates a preference from Safari. Returns whether the preference + * has been migrated. + * @param aSafariKey + * The dictionary key for the preference of Safari. + * @param aMozPref + * The gecko/firefox preference to which aSafariKey should be migrated + * @param [optional] aConvertFunction(aSafariValue) + * a function that converts the safari-preference value to the + * appropriate value for aMozPref. If it's not passed, then the + * Safari value is set as is. + * If aConvertFunction returns undefined, then aMozPref is not set + * at all. + * @return whether or not aMozPref was set. + */ + _set: function MPR_set(aSafariKey, aMozPref, aConvertFunction) { + if (this._dict.has(aSafariKey)) { + let safariVal = this._dict.get(aSafariKey); + let mozVal = aConvertFunction !== undefined ? + aConvertFunction(safariVal) : safariVal; + switch (typeof mozVal) { + case "string": + Services.prefs.setCharPref(aMozPref, mozVal); + break; + case "number": + Services.prefs.setIntPref(aMozPref, mozVal); + break; + case "boolean": + Services.prefs.setBoolPref(aMozPref, mozVal); + break; + case "undefined": + return false; + default: + throw new Error("Unexpected value type: " + (typeof mozVal)); + } + } + return true; + }, + + // Fonts settings are quite problematic for migration, for a couple of + // reasons: + // (a) Every font preference in Gecko is set for a particular language. + // In Safari, each font preference applies to all languages. + // (b) The current underlying implementation of nsIFontEnumerator cannot + // really tell you anything about a font: no matter what language or type + // you try to enumerate with EnumerateFonts, you get an array of all + // fonts in the systems (This also breaks our fonts dialog). + // (c) In Gecko, each langauge has a distinct serif and sans-serif font + // preference. Safari has only one default font setting. It seems that + // it checks if it's a serif or sans serif font, and when a site + // explicitly asks to use serif/sans-serif font, it uses the default font + // only if it applies to this type. + // (d) The solution of guessing the lang-group out of the default charset (as + // done in the old Safari migrator) can only work when: + // (1) The default charset preference is set. + // (2) It's not a unicode charset. + // For now, we use the language implied by the system locale as the + // lang-group. The only exception is minimal font size, which is an + // accessibility preference in Safari (under the Advanced tab). If it is set, + // we set it for all languages. + // As for the font type of the default font (serif/sans-serif), the default + // type for the given language is used (set in font.default.LANGGROUP). + _migrateFontSettings: function MPR__migrateFontSettings() { + // If "Never use font sizes smaller than [ ] is set", migrate it for all + // languages. + if (this._dict.has("WebKitMinimumFontSize")) { + let minimumSize = this._dict.get("WebKitMinimumFontSize"); + if (typeof minimumSize == "number") { + let prefs = Services.prefs.getChildList("font.minimum-size"); + for (let pref of prefs) { + Services.prefs.setIntPref(pref, minimumSize); + } + } + else { + Cu.reportError("WebKitMinimumFontSize was set to an invalid value: " + + minimumSize); + } + } + + // In theory, the lang group could be "x-unicode". This will result + // in setting the fonts for "Other Languages". + let lang = this._getLocaleLangGroup(); + + let anySet = false; + let fontType = Services.prefs.getCharPref("font.default." + lang); + anySet |= this._set("WebKitFixedFont", "font.name.monospace." + lang); + anySet |= this._set("WebKitDefaultFixedFontSize", "font.size.fixed." + lang); + anySet |= this._set("WebKitStandardFont", + "font.name." + fontType + "." + lang); + anySet |= this._set("WebKitDefaultFontSize", "font.size.variable." + lang); + + // If we set font settings for a particular language, we'll also set the + // fonts dialog to open with the fonts settings for that langauge. + if (anySet) + Services.prefs.setCharPref("font.language.group", lang); + }, + + // Get the language group for the system locale. + _getLocaleLangGroup: function MPR__getLocaleLangGroup() { + let locale = Services.locale.getLocaleComponentForUserAgent(); + + // See nsLanguageAtomService::GetLanguageGroup + let localeLangGroup = "x-unicode"; + let bundle = Services.strings.createBundle( + "resource://gre/res/langGroups.properties"); + try { + localeLangGroup = bundle.GetStringFromName(locale); + } + catch (ex) { + let hyphenAt = locale.indexOf("-"); + if (hyphenAt != -1) { + try { + localeLangGroup = bundle.GetStringFromName(locale.substr(0, hyphenAt)); + } + catch (ex2) { } + } + } + return localeLangGroup; + }, + + _migrateDownloadsFolder: Task.async(function* () { + if (!this._dict.has("DownloadsPath")) + return; + + let downloadsFolder = FileUtils.File(this._dict.get("DownloadsPath")); + + // If the download folder is set to the Desktop or to ~/Downloads, set the + // folderList pref appropriately so that "Desktop"/Downloads is shown with + // pretty name in the preferences dialog. + let folderListVal = 2; + if (downloadsFolder.equals(FileUtils.getDir("Desk", []))) { + folderListVal = 0; + } + else { + let systemDownloadsPath = yield Downloads.getSystemDownloadsDirectory(); + let systemDownloadsFolder = FileUtils.File(systemDownloadsPath); + if (downloadsFolder.equals(systemDownloadsFolder)) + folderListVal = 1; + } + Services.prefs.setIntPref("browser.download.folderList", folderListVal); + Services.prefs.setComplexValue("browser.download.dir", Ci.nsILocalFile, + downloadsFolder); + }), +}; + +function SearchStrings(aMainPreferencesPropertyListInstance) { + this._mainPreferencesPropertyList = aMainPreferencesPropertyListInstance; +} +SearchStrings.prototype = { + type: MigrationUtils.resourceTypes.OTHERDATA, + + migrate: function SS_migrate(aCallback) { + this._mainPreferencesPropertyList.read(MigrationUtils.wrapMigrateFunction( + function migrateSearchStrings(aDict) { + if (!aDict) + throw new Error("Could not get preferences dictionary"); + + if (aDict.has("RecentSearchStrings")) { + let recentSearchStrings = aDict.get("RecentSearchStrings"); + if (recentSearchStrings && recentSearchStrings.length > 0) { + let changes = recentSearchStrings.map((searchString) => ( + {op: "add", + fieldname: "searchbar-history", + value: searchString})); + FormHistory.update(changes); + } + } + }.bind(this), aCallback)); + } +}; + +// On OS X, the cookie-accept policy preference is stored in a separate +// property list. +function WebFoundationCookieBehavior(aWebFoundationFile) { + this._file = aWebFoundationFile; +} +WebFoundationCookieBehavior.prototype = { + type: MigrationUtils.resourceTypes.SETTINGS, + + migrate: function WFPL_migrate(aCallback) { + PropertyListUtils.read(this._file, MigrationUtils.wrapMigrateFunction( + function migrateCookieBehavior(aDict) { + if (!aDict) + throw new Error("Could not read com.apple.WebFoundation.plist"); + + if (aDict.has("NSHTTPAcceptCookies")) { + // Setting Safari Firefox + // Always Accept always 0 + // Accept from Originating current page 1 + // Never Accept never 2 + let acceptCookies = aDict.get("NSHTTPAcceptCookies"); + let cookieValue = 0; + if (acceptCookies == "never") { + cookieValue = 2; + } else if (acceptCookies == "current page") { + cookieValue = 1; + } + Services.prefs.setIntPref("network.cookie.cookieBehavior", + cookieValue); + } + }.bind(this), aCallback)); + } +}; + +function SafariProfileMigrator() { +} + +SafariProfileMigrator.prototype = Object.create(MigratorPrototype); + +SafariProfileMigrator.prototype.getResources = function SM_getResources() { + let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); + if (!profileDir.exists()) + return null; + + let resources = []; + let pushProfileFileResource = function(aFileName, aConstructor) { + let file = profileDir.clone(); + file.append(aFileName); + if (file.exists()) + resources.push(new aConstructor(file)); + }; + + pushProfileFileResource("History.plist", History); + pushProfileFileResource("Bookmarks.plist", Bookmarks); + + // The Reading List feature was introduced at the same time in Windows and + // Mac versions of Safari. Not surprisingly, they are stored in the same + // format in both versions. Surpsingly, only on Windows there is a + // separate property list for it. This code is used on mac too, because + // Apple may fix this at some point. + pushProfileFileResource("ReadingList.plist", Bookmarks); + + let prefs = this.mainPreferencesPropertyList; + if (prefs) { + resources.push(new Preferences(prefs)); + resources.push(new SearchStrings(prefs)); + } + + let wfFile = FileUtils.getFile("UsrPrfs", ["com.apple.WebFoundation.plist"]); + if (wfFile.exists()) + resources.push(new WebFoundationCookieBehavior(wfFile)); + + return resources; +}; + +SafariProfileMigrator.prototype.getLastUsedDate = function SM_getLastUsedDate() { + let profileDir = FileUtils.getDir("ULibDir", ["Safari"], false); + let datePromises = ["Bookmarks.plist", "History.plist"].map(file => { + let path = OS.Path.join(profileDir.path, file); + return OS.File.stat(path).catch(() => null).then(info => { + return info ? info.lastModificationDate : 0; + }); + }); + return Promise.all(datePromises).then(dates => { + return new Date(Math.max.apply(Math, dates)); + }); +}; + +Object.defineProperty(SafariProfileMigrator.prototype, "mainPreferencesPropertyList", { + get: function get_mainPreferencesPropertyList() { + if (this._mainPreferencesPropertyList === undefined) { + let file = FileUtils.getDir("UsrPrfs", [], false); + if (file.exists()) { + file.append("com.apple.Safari.plist"); + if (file.exists()) { + this._mainPreferencesPropertyList = + new MainPreferencesPropertyList(file); + return this._mainPreferencesPropertyList; + } + } + this._mainPreferencesPropertyList = null; + return this._mainPreferencesPropertyList; + } + return this._mainPreferencesPropertyList; + } +}); + +Object.defineProperty(SafariProfileMigrator.prototype, "sourceHomePageURL", { + get: function get_sourceHomePageURL() { + if (this.mainPreferencesPropertyList) { + let dict = this.mainPreferencesPropertyList._readSync(); + if (dict.has("HomePage")) + return dict.get("HomePage"); + } + return ""; + } +}); + +SafariProfileMigrator.prototype.classDescription = "Safari Profile Migrator"; +SafariProfileMigrator.prototype.contractID = "@mozilla.org/profile/migrator;1?app=browser&type=safari"; +SafariProfileMigrator.prototype.classID = Components.ID("{4b609ecf-60b2-4655-9df4-dc149e474da1}"); + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SafariProfileMigrator]); |