diff options
Diffstat (limited to 'application/basilisk/components/migration/AutoMigrate.jsm')
-rw-r--r-- | application/basilisk/components/migration/AutoMigrate.jsm | 670 |
1 files changed, 670 insertions, 0 deletions
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(); |