1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["AutoMigrate"]; 8 9const kAutoMigrateEnabledPref = "browser.migrate.automigrate.enabled"; 10const kUndoUIEnabledPref = "browser.migrate.automigrate.ui.enabled"; 11 12const kInPageUIEnabledPref = "browser.migrate.automigrate.inpage.ui.enabled"; 13 14const kAutoMigrateBrowserPref = "browser.migrate.automigrate.browser"; 15const kAutoMigrateImportedItemIds = "browser.migrate.automigrate.imported-items"; 16 17const kAutoMigrateLastUndoPromptDateMsPref = "browser.migrate.automigrate.lastUndoPromptDateMs"; 18const kAutoMigrateDaysToOfferUndoPref = "browser.migrate.automigrate.daysToOfferUndo"; 19 20const kAutoMigrateUndoSurveyPref = "browser.migrate.automigrate.undo-survey"; 21const kAutoMigrateUndoSurveyLocalePref = "browser.migrate.automigrate.undo-survey-locales"; 22 23const kNotificationId = "automigration-undo"; 24 25ChromeUtils.import("resource:///modules/MigrationUtils.jsm"); 26ChromeUtils.import("resource://gre/modules/Preferences.jsm"); 27ChromeUtils.import("resource://gre/modules/Services.jsm"); 28ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); 29 30ChromeUtils.defineModuleGetter(this, "AsyncShutdown", 31 "resource://gre/modules/AsyncShutdown.jsm"); 32ChromeUtils.defineModuleGetter(this, "LoginHelper", 33 "resource://gre/modules/LoginHelper.jsm"); 34ChromeUtils.defineModuleGetter(this, "NewTabUtils", 35 "resource://gre/modules/NewTabUtils.jsm"); 36ChromeUtils.defineModuleGetter(this, "OS", 37 "resource://gre/modules/osfile.jsm"); 38ChromeUtils.defineModuleGetter(this, "PlacesUtils", 39 "resource://gre/modules/PlacesUtils.jsm"); 40ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch", 41 "resource://gre/modules/TelemetryStopwatch.jsm"); 42 43XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() { 44 const kBrandBundle = "chrome://branding/locale/brand.properties"; 45 return Services.strings.createBundle(kBrandBundle); 46}); 47 48Cu.importGlobalProperties(["URL"]); 49 50XPCOMUtils.defineLazyGetter(this, "kUndoStateFullPath", function() { 51 return OS.Path.join(OS.Constants.Path.profileDir, "initialMigrationMetadata.jsonlz4"); 52}); 53 54const AutoMigrate = { 55 get resourceTypesToUse() { 56 let {BOOKMARKS, HISTORY, PASSWORDS} = Ci.nsIBrowserProfileMigrator; 57 return BOOKMARKS | HISTORY | PASSWORDS; 58 }, 59 60 _checkIfEnabled() { 61 let pref = Preferences.get(kAutoMigrateEnabledPref, false); 62 // User-set values should take precedence: 63 if (Services.prefs.prefHasUserValue(kAutoMigrateEnabledPref)) { 64 return pref; 65 } 66 // If we're using the default value, make sure the distribution.ini 67 // value is taken into account even early on startup. 68 try { 69 let distributionFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile); 70 distributionFile.append("distribution.ini"); 71 let parser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]. 72 getService(Ci.nsIINIParserFactory). 73 createINIParser(distributionFile); 74 return JSON.parse(parser.getString("Preferences", kAutoMigrateEnabledPref)); 75 } catch (ex) { /* ignore exceptions (file doesn't exist, invalid value, etc.) */ } 76 77 return pref; 78 }, 79 80 init() { 81 this.enabled = this._checkIfEnabled(); 82 }, 83 84 /** 85 * Automatically pick a migrator and resources to migrate, 86 * then migrate those and start up. 87 * 88 * @throws if automatically deciding on migrators/data 89 * failed for some reason. 90 */ 91 async migrate(profileStartup, migratorKey, profileToMigrate) { 92 let histogram = Services.telemetry.getHistogramById( 93 "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_PROCESS_SUCCESS"); 94 histogram.add(0); 95 let {migrator, pickedKey} = await this.pickMigrator(migratorKey); 96 histogram.add(5); 97 98 profileToMigrate = await this.pickProfile(migrator, profileToMigrate); 99 histogram.add(10); 100 101 let resourceTypes = await migrator.getMigrateData(profileToMigrate, profileStartup); 102 if (!(resourceTypes & this.resourceTypesToUse)) { 103 throw new Error("No usable resources were found for the selected browser!"); 104 } 105 histogram.add(15); 106 107 let sawErrors = false; 108 let migrationObserver = (subject, topic) => { 109 if (topic == "Migration:ItemError") { 110 sawErrors = true; 111 } else if (topic == "Migration:Ended") { 112 histogram.add(25); 113 if (sawErrors) { 114 histogram.add(26); 115 } 116 Services.obs.removeObserver(migrationObserver, "Migration:Ended"); 117 Services.obs.removeObserver(migrationObserver, "Migration:ItemError"); 118 Services.prefs.setCharPref(kAutoMigrateBrowserPref, pickedKey); 119 // Save the undo history and block shutdown on that save completing. 120 AsyncShutdown.profileBeforeChange.addBlocker( 121 "AutoMigrate Undo saving", this.saveUndoState(), () => { 122 return {state: this._saveUndoStateTrackerForShutdown}; 123 }); 124 } 125 }; 126 127 MigrationUtils.initializeUndoData(); 128 Services.obs.addObserver(migrationObserver, "Migration:Ended"); 129 Services.obs.addObserver(migrationObserver, "Migration:ItemError"); 130 await migrator.migrate(this.resourceTypesToUse, profileStartup, profileToMigrate); 131 histogram.add(20); 132 }, 133 134 /** 135 * Pick and return a migrator to use for automatically migrating. 136 * 137 * @param {String} migratorKey optional, a migrator key to prefer/pick. 138 * @returns {Object} an object with the migrator to use for migrating, as 139 * well as the key we eventually ended up using to obtain it. 140 */ 141 async pickMigrator(migratorKey) { 142 if (!migratorKey) { 143 let defaultKey = MigrationUtils.getMigratorKeyForDefaultBrowser(); 144 if (!defaultKey) { 145 throw new Error("Could not determine default browser key to migrate from"); 146 } 147 migratorKey = defaultKey; 148 } 149 if (migratorKey == "firefox") { 150 throw new Error("Can't automatically migrate from Firefox."); 151 } 152 153 let migrator = await MigrationUtils.getMigrator(migratorKey); 154 if (!migrator) { 155 throw new Error("Migrator specified or a default was found, but the migrator object is not available (or has no data)."); 156 } 157 return {migrator, pickedKey: migratorKey}; 158 }, 159 160 /** 161 * Pick a source profile (from the original browser) to use. 162 * 163 * @param {Migrator} migrator the migrator object to use 164 * @param {String} suggestedId the id of the profile to migrate, if pre-specified, or null 165 * @returns the profile to migrate, or null if migrating 166 * from the default profile. 167 */ 168 async pickProfile(migrator, suggestedId) { 169 let profiles = await migrator.getSourceProfiles(); 170 if (profiles && !profiles.length) { 171 throw new Error("No profile data found to migrate."); 172 } 173 if (suggestedId) { 174 if (!profiles) { 175 throw new Error("Profile specified but only a default profile found."); 176 } 177 let suggestedProfile = profiles.find(profile => profile.id == suggestedId); 178 if (!suggestedProfile) { 179 throw new Error("Profile specified was not found."); 180 } 181 return suggestedProfile; 182 } 183 if (profiles && profiles.length > 1) { 184 throw new Error("Don't know how to pick a profile when more than 1 profile is present."); 185 } 186 return profiles ? profiles[0] : null; 187 }, 188 189 _pendingUndoTasks: false, 190 async canUndo() { 191 if (this._savingPromise) { 192 await this._savingPromise; 193 } 194 if (this._pendingUndoTasks) { 195 return false; 196 } 197 let fileExists = false; 198 try { 199 fileExists = await OS.File.exists(kUndoStateFullPath); 200 } catch (ex) { 201 Cu.reportError(ex); 202 } 203 return fileExists; 204 }, 205 206 async undo() { 207 let browserId = Preferences.get(kAutoMigrateBrowserPref, "unknown"); 208 TelemetryStopwatch.startKeyed("FX_STARTUP_MIGRATION_UNDO_TOTAL_MS", browserId); 209 let histogram = Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_UNDO"); 210 histogram.add(0); 211 if (!(await this.canUndo())) { 212 histogram.add(5); 213 throw new Error("Can't undo!"); 214 } 215 216 this._pendingUndoTasks = true; 217 this._removeNotificationBars(); 218 histogram.add(10); 219 220 let readPromise = OS.File.read(kUndoStateFullPath, { 221 encoding: "utf-8", 222 compression: "lz4", 223 }); 224 let stateData = this._dejsonifyUndoState(await readPromise); 225 histogram.add(12); 226 227 this._errorMap = {bookmarks: 0, visits: 0, logins: 0}; 228 let reportErrorTelemetry = (type) => { 229 let histogramId = `FX_STARTUP_MIGRATION_UNDO_${type.toUpperCase()}_ERRORCOUNT`; 230 Services.telemetry.getKeyedHistogramById(histogramId).add(browserId, this._errorMap[type]); 231 }; 232 233 let startTelemetryStopwatch = resourceType => { 234 let histogramId = `FX_STARTUP_MIGRATION_UNDO_${resourceType.toUpperCase()}_MS`; 235 TelemetryStopwatch.startKeyed(histogramId, browserId); 236 }; 237 let stopTelemetryStopwatch = resourceType => { 238 let histogramId = `FX_STARTUP_MIGRATION_UNDO_${resourceType.toUpperCase()}_MS`; 239 TelemetryStopwatch.finishKeyed(histogramId, browserId); 240 }; 241 startTelemetryStopwatch("bookmarks"); 242 await this._removeUnchangedBookmarks(stateData.get("bookmarks")).catch(ex => { 243 Cu.reportError("Uncaught exception when removing unchanged bookmarks!"); 244 Cu.reportError(ex); 245 }); 246 stopTelemetryStopwatch("bookmarks"); 247 reportErrorTelemetry("bookmarks"); 248 histogram.add(15); 249 250 startTelemetryStopwatch("visits"); 251 await this._removeSomeVisits(stateData.get("visits")).catch(ex => { 252 Cu.reportError("Uncaught exception when removing history visits!"); 253 Cu.reportError(ex); 254 }); 255 stopTelemetryStopwatch("visits"); 256 reportErrorTelemetry("visits"); 257 histogram.add(20); 258 259 startTelemetryStopwatch("logins"); 260 await this._removeUnchangedLogins(stateData.get("logins")).catch(ex => { 261 Cu.reportError("Uncaught exception when removing unchanged logins!"); 262 Cu.reportError(ex); 263 }); 264 stopTelemetryStopwatch("logins"); 265 reportErrorTelemetry("logins"); 266 histogram.add(25); 267 268 // This is async, but no need to wait for it. 269 NewTabUtils.links.populateCache(() => { 270 NewTabUtils.allPages.update(); 271 }, true); 272 273 this._purgeUndoState(this.UNDO_REMOVED_REASON_UNDO_USED); 274 histogram.add(30); 275 TelemetryStopwatch.finishKeyed("FX_STARTUP_MIGRATION_UNDO_TOTAL_MS", browserId); 276 }, 277 278 _removeNotificationBars() { 279 let browserWindows = Services.wm.getEnumerator("navigator:browser"); 280 while (browserWindows.hasMoreElements()) { 281 let win = browserWindows.getNext(); 282 if (!win.closed) { 283 for (let browser of win.gBrowser.browsers) { 284 let nb = win.gBrowser.getNotificationBox(browser); 285 let notification = nb.getNotificationWithValue(kNotificationId); 286 if (notification) { 287 nb.removeNotification(notification); 288 } 289 } 290 } 291 } 292 }, 293 294 _purgeUndoState(reason) { 295 // We don't wait for the off-main-thread removal to complete. OS.File will 296 // ensure it happens before shutdown. 297 OS.File.remove(kUndoStateFullPath, {ignoreAbsent: true}).then(() => { 298 this._pendingUndoTasks = false; 299 }); 300 301 let migrationBrowser = Preferences.get(kAutoMigrateBrowserPref, "unknown"); 302 Services.prefs.clearUserPref(kAutoMigrateBrowserPref); 303 304 let histogram = 305 Services.telemetry.getKeyedHistogramById("FX_STARTUP_MIGRATION_UNDO_REASON"); 306 histogram.add(migrationBrowser, reason); 307 }, 308 309 getBrowserUsedForMigration() { 310 let browserId = Services.prefs.getCharPref(kAutoMigrateBrowserPref); 311 if (browserId) { 312 return MigrationUtils.getBrowserName(browserId); 313 } 314 return null; 315 }, 316 317 /** 318 * Decide if we need to show [the user] a prompt indicating we automatically 319 * imported their data. 320 * @param target (xul:browser) 321 * The browser in which we should show the notification. 322 * @returns {Boolean} return true when need to show the prompt. 323 */ 324 async shouldShowMigratePrompt(target) { 325 if (!(await this.canUndo())) { 326 return false; 327 } 328 329 // The tab might have navigated since we requested the undo state: 330 let canUndoFromThisPage = ["about:home", "about:newtab"].includes(target.currentURI.spec); 331 if (!canUndoFromThisPage || 332 !Preferences.get(kUndoUIEnabledPref, false)) { 333 return false; 334 } 335 336 // At this stage we're committed to show the prompt - unless we shouldn't, 337 // in which case we remove the undo prefs (which will cause canUndo() to 338 // return false from now on.): 339 if (this.isMigratePromptExpired()) { 340 this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_EXPIRED); 341 this._removeNotificationBars(); 342 return false; 343 } 344 345 let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 0); 346 Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_UNDO_OFFERED").add(4 - remainingDays); 347 348 return true; 349 }, 350 351 /** 352 * Return the message that denotes the user data is migrated from the other browser. 353 * @returns {String} imported message with the brand and the browser name 354 */ 355 getUndoMigrationMessage() { 356 let browserName = this.getBrowserUsedForMigration(); 357 if (!browserName) { 358 browserName = MigrationUtils.getLocalizedString("automigration.undo.unknownbrowser"); 359 } 360 const kMessageId = "automigration.undo.message2." + 361 Preferences.get(kAutoMigrateImportedItemIds, "all"); 362 const kBrandShortName = gBrandBundle.GetStringFromName("brandShortName"); 363 return MigrationUtils.getLocalizedString(kMessageId, 364 [kBrandShortName, browserName]); 365 }, 366 367 /** 368 * Show the user a notification bar indicating we automatically imported 369 * their data and offering them the possibility of removing it. 370 * @param target (xul:browser) 371 * The browser in which we should show the notification. 372 */ 373 showUndoNotificationBar(target) { 374 let isInPage = Preferences.get(kInPageUIEnabledPref, false); 375 let win = target.ownerGlobal; 376 let notificationBox = win.gBrowser.getNotificationBox(target); 377 if (isInPage || !notificationBox || notificationBox.getNotificationWithValue(kNotificationId)) { 378 return; 379 } 380 let message = this.getUndoMigrationMessage(); 381 let buttons = [ 382 { 383 label: MigrationUtils.getLocalizedString("automigration.undo.keep2.label"), 384 accessKey: MigrationUtils.getLocalizedString("automigration.undo.keep2.accesskey"), 385 callback: () => { 386 this.keepAutoMigration(); 387 this._removeNotificationBars(); 388 }, 389 }, 390 { 391 label: MigrationUtils.getLocalizedString("automigration.undo.dontkeep2.label"), 392 accessKey: MigrationUtils.getLocalizedString("automigration.undo.dontkeep2.accesskey"), 393 callback: () => { 394 this.undoAutoMigration(win); 395 }, 396 }, 397 ]; 398 notificationBox.appendNotification( 399 message, kNotificationId, null, notificationBox.PRIORITY_INFO_HIGH, buttons 400 ); 401 }, 402 403 404 /** 405 * Return true if we have shown the prompt to user several days. 406 * (defined in kAutoMigrateDaysToOfferUndoPref) 407 */ 408 isMigratePromptExpired() { 409 let today = new Date(); 410 // Round down to midnight: 411 today = new Date(today.getFullYear(), today.getMonth(), today.getDate()); 412 // We store the unix timestamp corresponding to midnight on the last day 413 // on which we prompted. Fetch that and compare it to today's date. 414 // (NB: stored as a string because int prefs are too small for unix 415 // timestamps.) 416 let previousPromptDateMsStr = Preferences.get(kAutoMigrateLastUndoPromptDateMsPref, "0"); 417 let previousPromptDate = new Date(parseInt(previousPromptDateMsStr, 10)); 418 if (previousPromptDate < today) { 419 let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 4) - 1; 420 Preferences.set(kAutoMigrateDaysToOfferUndoPref, remainingDays); 421 Preferences.set(kAutoMigrateLastUndoPromptDateMsPref, today.valueOf().toString()); 422 if (remainingDays <= 0) { 423 return true; 424 } 425 } 426 return false; 427 }, 428 429 UNDO_REMOVED_REASON_UNDO_USED: 0, 430 UNDO_REMOVED_REASON_SYNC_SIGNIN: 1, 431 UNDO_REMOVED_REASON_PASSWORD_CHANGE: 2, 432 UNDO_REMOVED_REASON_BOOKMARK_CHANGE: 3, 433 UNDO_REMOVED_REASON_OFFER_EXPIRED: 4, 434 UNDO_REMOVED_REASON_OFFER_REJECTED: 5, 435 436 _jsonifyUndoState(state) { 437 if (!state) { 438 return "null"; 439 } 440 // Deal with date serialization. 441 let bookmarks = state.get("bookmarks"); 442 for (let bm of bookmarks) { 443 bm.lastModified = bm.lastModified.getTime(); 444 } 445 let serializableState = { 446 bookmarks, 447 logins: state.get("logins"), 448 visits: state.get("visits"), 449 }; 450 return JSON.stringify(serializableState); 451 }, 452 453 _dejsonifyUndoState(state) { 454 state = JSON.parse(state); 455 if (!state) { 456 return new Map(); 457 } 458 for (let bm of state.bookmarks) { 459 bm.lastModified = new Date(bm.lastModified); 460 } 461 return new Map([ 462 ["bookmarks", state.bookmarks], 463 ["logins", state.logins], 464 ["visits", state.visits], 465 ]); 466 }, 467 468 /** 469 * Store the items we've saved into a pref. We use this to be able to show 470 * a detailed message to the user indicating what we've imported. 471 * @param state (Map) 472 * The 'undo' state for the import, which contains info about 473 * how many items of each kind we've (tried to) import. 474 */ 475 _setImportedItemPrefFromState(state) { 476 let itemsWithData = []; 477 if (state) { 478 for (let itemType of state.keys()) { 479 if (state.get(itemType).length) { 480 itemsWithData.push(itemType); 481 } 482 } 483 } 484 if (itemsWithData.length == 3) { 485 itemsWithData = "all"; 486 } else { 487 itemsWithData = itemsWithData.sort().join("."); 488 } 489 if (itemsWithData) { 490 Preferences.set(kAutoMigrateImportedItemIds, itemsWithData); 491 } 492 }, 493 494 /** 495 * Used for the shutdown blocker's information field. 496 */ 497 _saveUndoStateTrackerForShutdown: "not running", 498 /** 499 * Store the information required for using 'undo' of the automatic 500 * migration in the user's profile. 501 */ 502 async saveUndoState() { 503 let resolveSavingPromise; 504 this._saveUndoStateTrackerForShutdown = "processing undo history"; 505 this._savingPromise = new Promise(resolve => { resolveSavingPromise = resolve; }); 506 let state = await MigrationUtils.stopAndRetrieveUndoData(); 507 508 if (!state || ![...state.values()].some(ary => ary.length > 0)) { 509 // If we didn't import anything, abort now. 510 resolveSavingPromise(); 511 return Promise.resolve(); 512 } 513 514 this._saveUndoStateTrackerForShutdown = "saving imported item list"; 515 this._setImportedItemPrefFromState(state); 516 517 this._saveUndoStateTrackerForShutdown = "writing undo history"; 518 this._undoSavePromise = OS.File.writeAtomic( 519 kUndoStateFullPath, this._jsonifyUndoState(state), { 520 encoding: "utf-8", 521 compression: "lz4", 522 tmpPath: kUndoStateFullPath + ".tmp", 523 }); 524 this._undoSavePromise.then( 525 rv => { 526 resolveSavingPromise(rv); 527 delete this._savingPromise; 528 }, 529 e => { 530 Cu.reportError("Could not write undo state for automatic migration."); 531 throw e; 532 }); 533 return this._undoSavePromise; 534 }, 535 536 async _removeUnchangedBookmarks(bookmarks) { 537 if (!bookmarks.length) { 538 return; 539 } 540 541 let guidToLMMap = new Map(bookmarks.map(b => [b.guid, b.lastModified])); 542 let bookmarksFromDB = []; 543 let bmPromises = Array.from(guidToLMMap.keys()).map(guid => { 544 // Ignore bookmarks where the promise doesn't resolve (ie that are missing) 545 // Also check that the bookmark fetch returns isn't null before adding it. 546 try { 547 return PlacesUtils.bookmarks.fetch(guid).then(bm => bm && bookmarksFromDB.push(bm), () => {}); 548 } catch (ex) { 549 // Ignore immediate exceptions, too. 550 } 551 return Promise.resolve(); 552 }); 553 // We can't use the result of Promise.all because that would include nulls 554 // for bookmarks that no longer exist (which we're catching above). 555 await Promise.all(bmPromises); 556 let unchangedBookmarks = bookmarksFromDB.filter(bm => { 557 return bm.lastModified.getTime() == guidToLMMap.get(bm.guid).getTime(); 558 }); 559 560 // We need to remove items without children first, followed by their 561 // parents, etc. In order to do this, find out how many ancestors each item 562 // has that also appear in our list of things to remove, and sort the items 563 // by those numbers. This ensures that children are always removed before 564 // their parents. 565 function determineAncestorCount(bm) { 566 if (bm._ancestorCount) { 567 return bm._ancestorCount; 568 } 569 let myCount = 0; 570 let parentBM = unchangedBookmarks.find(item => item.guid == bm.parentGuid); 571 if (parentBM) { 572 myCount = determineAncestorCount(parentBM) + 1; 573 } 574 bm._ancestorCount = myCount; 575 return myCount; 576 } 577 unchangedBookmarks.forEach(determineAncestorCount); 578 unchangedBookmarks.sort((a, b) => b._ancestorCount - a._ancestorCount); 579 for (let {guid} of unchangedBookmarks) { 580 // Can't just use a .catch() because Bookmarks.remove() can throw (rather 581 // than returning rejected promises). 582 try { 583 await PlacesUtils.bookmarks.remove(guid, {preventRemovalOfNonEmptyFolders: true}); 584 } catch (err) { 585 if (err && err.message != "Cannot remove a non-empty folder.") { 586 this._errorMap.bookmarks++; 587 Cu.reportError(err); 588 } 589 } 590 } 591 }, 592 593 async _removeUnchangedLogins(logins) { 594 for (let login of logins) { 595 let foundLogins = LoginHelper.searchLoginsWithObject({guid: login.guid}); 596 if (foundLogins.length) { 597 let foundLogin = foundLogins[0]; 598 foundLogin.QueryInterface(Ci.nsILoginMetaInfo); 599 if (foundLogin.timePasswordChanged == login.timePasswordChanged) { 600 try { 601 Services.logins.removeLogin(foundLogin); 602 } catch (ex) { 603 Cu.reportError("Failed to remove a login for " + foundLogins.hostname); 604 Cu.reportError(ex); 605 this._errorMap.logins++; 606 } 607 } 608 } 609 } 610 }, 611 612 async _removeSomeVisits(visits) { 613 for (let urlVisits of visits) { 614 let urlObj; 615 try { 616 urlObj = new URL(urlVisits.url); 617 } catch (ex) { 618 continue; 619 } 620 let visitData = { 621 url: urlObj, 622 beginDate: PlacesUtils.toDate(urlVisits.first), 623 endDate: PlacesUtils.toDate(urlVisits.last), 624 limit: urlVisits.visitCount, 625 }; 626 try { 627 await PlacesUtils.history.removeVisitsByFilter(visitData); 628 } catch (ex) { 629 this._errorMap.visits++; 630 try { 631 visitData.url = visitData.url.href; 632 } catch (ignoredEx) {} 633 Cu.reportError("Failed to remove a visit: " + JSON.stringify(visitData)); 634 Cu.reportError(ex); 635 } 636 } 637 }, 638 639 /** 640 * Maybe open a new tab with a survey. The tab will only be opened if all of 641 * the following are true: 642 * - the 'browser.migrate.automigrate.undo-survey' pref is not empty. 643 * It should contain the URL of the survey to open. 644 * - the 'browser.migrate.automigrate.undo-survey-locales' pref, a 645 * comma-separated list of language codes, contains the language code 646 * that is currently in use for the 'global' chrome pacakge (ie the 647 * locale in which the user is currently using Firefox). 648 * The URL will be passed through nsIURLFormatter to allow including 649 * build ids etc. The special additional formatting variable 650 * "%IMPORTEDBROWSER" is also replaced with the name of the browser 651 * from which we imported data. 652 * 653 * @param {Window} chromeWindow A reference to the window in which to open a link. 654 */ 655 _maybeOpenUndoSurveyTab(chromeWindow) { 656 let canDoSurveyInLocale = false; 657 try { 658 let surveyLocales = Preferences.get(kAutoMigrateUndoSurveyLocalePref, ""); 659 surveyLocales = surveyLocales.split(",").map(str => str.trim()); 660 // Strip out any empty elements, so an empty pref doesn't 661 // lead to a an array with 1 empty string in it. 662 surveyLocales = new Set(surveyLocales.filter(str => !!str)); 663 canDoSurveyInLocale = 664 surveyLocales.has(Services.locale.getAppLocaleAsLangTag()); 665 } catch (ex) { 666 /* ignore exceptions and just don't do the survey. */ 667 } 668 669 let migrationBrowser = this.getBrowserUsedForMigration(); 670 let rawURL = Preferences.get(kAutoMigrateUndoSurveyPref, ""); 671 if (!canDoSurveyInLocale || !migrationBrowser || !rawURL) { 672 return; 673 } 674 675 let url = Services.urlFormatter.formatURL(rawURL); 676 url = url.replace("%IMPORTEDBROWSER%", encodeURIComponent(migrationBrowser)); 677 chromeWindow.openUILinkIn(url, "tab"); 678 }, 679 680 QueryInterface: XPCOMUtils.generateQI( 681 [Ci.nsIObserver, Ci.nsINavBookmarkObserver, Ci.nsISupportsWeakReference] 682 ), 683 684 /** 685 * Undo action called by the UndoNotification or by the newtab 686 * @param chromeWindow A reference to the window in which to open a link. 687 */ 688 undoAutoMigration(chromeWindow) { 689 this._maybeOpenUndoSurveyTab(chromeWindow); 690 this.undo(); 691 }, 692 693 /** 694 * Keep the automigration result and not prompt anymore 695 */ 696 keepAutoMigration() { 697 this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_REJECTED); 698 }, 699}; 700 701AutoMigrate.init(); 702