Bug 1271775 - allow bypassing the initial migration dialog, r=jaws FUNNELCAKE86_BRANCH
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Wed, 01 Jun 2016 19:00:53 +0100
branchFUNNELCAKE86_BRANCH
changeset 326419 eeb0cd98062aaf46ca6925c6ba5f70c223b5fdc2
parent 326414 7f5abf95991bda0bc2b8e0d774a8866b726b312b
child 326420 945b06d097611ad4649d6228d1b00fade3ff1251
push id1143
push usernthomas@mozilla.com
push dateTue, 05 Jul 2016 23:40:05 +0000
treeherdermozilla-release@0cc1138c8b72 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1271775
milestone47.0.1
Bug 1271775 - allow bypassing the initial migration dialog, r=jaws MozReview-Commit-ID: LkhHl7ipGEb
browser/app/profile/firefox.js
browser/components/migration/AutoMigrate.jsm
browser/components/migration/MigrationUtils.jsm
browser/components/migration/moz.build
browser/components/migration/tests/unit/test_automigration.js
browser/components/migration/tests/unit/xpcshell.ini
toolkit/components/telemetry/Histograms.json
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1480,12 +1480,14 @@ pref("toolkit.pageThumbs.minHeight", 190
 // Enable speech synthesis, only Nightly for now
 pref("media.webspeech.synth.enabled", true);
 #endif
 
 pref("browser.esedbreader.loglevel", "Error");
 
 pref("browser.laterrun.enabled", false);
 
+pref("browser.migration.automigrate", false);
+
 // Enable browser frames for use on desktop.  Only exposed to chrome callers.
 pref("dom.mozBrowserFramesEnabled", true);
 
 pref("extensions.pocket.enabled", true);
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/AutoMigrate.jsm
@@ -0,0 +1,113 @@
+/* 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;
+
+Cu.import("resource:///modules/MigrationUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const AutoMigrate = {
+  get resourceTypesToUse() {
+    let {BOOKMARKS, HISTORY, FORMDATA, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
+    return BOOKMARKS | HISTORY | FORMDATA | PASSWORDS;
+  },
+
+  /**
+   * 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.getKeyedHistogramById("FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_SUCCEEDED");
+    histogram.add("initialized");
+    let migrator = this.pickMigrator(migratorKey);
+    histogram.add("got-browser");
+
+    profileToMigrate = this.pickProfile(migrator, profileToMigrate);
+    histogram.add("got-profile");
+
+    let resourceTypes = migrator.getMigrateData(profileToMigrate, profileStartup);
+    if (!(resourceTypes & this.resourceTypesToUse)) {
+      throw new Error("No usable resources were found for the selected browser!");
+    }
+    histogram.add("got-data");
+
+    let sawErrors = false;
+    let migrationObserver = function(subject, topic, data) {
+      if (topic == "Migration:ItemError") {
+        sawErrors = true;
+      } else if (topic == "Migration:Ended") {
+        histogram.add(sawErrors ? "finished-with-errors" : "finished");
+        Services.obs.removeObserver(migrationObserver, "Migration:Ended");
+        Services.obs.removeObserver(migrationObserver, "Migration:ItemError");
+      }
+    };
+
+    Services.obs.addObserver(migrationObserver, "Migration:Ended", false);
+    Services.obs.addObserver(migrationObserver, "Migration:ItemError", false);
+    migrator.migrate(this.resourceTypesToUse, profileStartup, profileToMigrate);
+    histogram.add("migrate-called-without-exceptions");
+  },
+
+  /**
+   * Pick and return a migrator to use for automatically migrating.
+   *
+   * @param {String} migratorKey   optional, a migrator key to prefer/pick.
+   * @returns                      the migrator to use for migrating.
+   */
+  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.");
+    }
+    return migrator;
+  },
+
+  /**
+   * 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 id of 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.id;
+    }
+    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].id : null;
+  },
+};
+
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -16,16 +16,18 @@ Cu.import("resource://gre/modules/Task.j
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
                                   "resource://gre/modules/BookmarkHTMLUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AutoMigrate",
+                                  "resource:///modules/AutoMigrate.jsm");
 
 var gMigrators = null;
 var gProfileStartup = null;
 var gMigrationBundle = null;
 
 XPCOMUtils.defineLazyGetter(this, "gAvailableMigratorKeys", function() {
   if (AppConstants.platform == "win") {
     return [
@@ -124,17 +126,17 @@ this.MigratorPrototype = {
   /**
    * 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, retunring any of the migration types (see
+   * - 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.
    *
@@ -688,18 +690,31 @@ this.MigrationUtils = Object.freeze({
       // 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 == AppConstants.MOZ_APP_NAME;
+
+    if (!isRefresh &&
+        Services.prefs.getBoolPref("browser.migration.automigrate")) {
+      try {
+        return AutoMigrate.migrate(aProfileStartup, aMigratorKey, aProfileToMigrate);
+      } catch (ex) {
+        // If automigration failed, continue and show the dialog.
+        Cu.reportError(ex);
+      }
+    }
+
     let migrationEntryPoint = this.MIGRATION_ENTRYPOINT_FIRSTRUN;
-    if (migrator && skipSourcePage && migratorKey == AppConstants.MOZ_APP_NAME) {
+    if (isRefresh) {
       migrationEntryPoint = this.MIGRATION_ENTRYPOINT_FXREFRESH;
     }
 
     let params = [
       migrationEntryPoint,
       migratorKey,
       migrator,
       aProfileStartup,
@@ -712,16 +727,18 @@ this.MigrationUtils = Object.freeze({
    * 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,
--- a/browser/components/migration/moz.build
+++ b/browser/components/migration/moz.build
@@ -20,16 +20,17 @@ EXTRA_COMPONENTS += [
     'ProfileMigrator.js',
 ]
 
 EXTRA_PP_COMPONENTS += [
     'BrowserProfileMigrators.manifest',
 ]
 
 EXTRA_JS_MODULES += [
+    'AutoMigrate.jsm',
     'MigrationUtils.jsm',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     SOURCES += [
         'nsIEHistoryEnumerator.cpp',
     ]
     EXTRA_COMPONENTS += [
new file mode 100644
--- /dev/null
+++ b/browser/components/migration/tests/unit/test_automigration.js
@@ -0,0 +1,119 @@
+Cu.import("resource:///modules/MigrationUtils.jsm");
+let AutoMigrateBackstage = Cu.import("resource:///modules/AutoMigrate.jsm");
+
+let gShimmedMigratorKeyPicker = null;
+let gShimmedMigrator = null;
+
+// This is really a proxy on MigrationUtils, but if we specify that directly,
+// we get in trouble because the object itself is frozen, and Proxies can't
+// return a different value to an object when directly proxying a frozen
+// object.
+AutoMigrateBackstage.MigrationUtils = new Proxy({}, {
+  get(target, name) {
+    if (name == "getMigratorKeyForDefaultBrowser" && gShimmedMigratorKeyPicker) {
+      return gShimmedMigratorKeyPicker;
+    }
+    if (name == "getMigrator" && gShimmedMigrator) {
+      return function() { return gShimmedMigrator };
+    }
+    return MigrationUtils[name];
+  },
+});
+
+do_register_cleanup(function() {
+  AutoMigrateBackstage.MigrationUtils = MigrationUtils;
+});
+
+/**
+ * Test automatically picking a browser to migrate from
+ */
+add_task(function* checkMigratorPicking() {
+  Assert.throws(() => AutoMigrate.pickMigrator("firefox"),
+                /Can't automatically migrate from Firefox/,
+                "Should throw when explicitly picking Firefox.");
+
+  Assert.throws(() => AutoMigrate.pickMigrator("gobbledygook"),
+                /migrator object is not available/,
+                "Should throw when passing unknown migrator key");
+  gShimmedMigratorKeyPicker = function() {
+    return "firefox";
+  };
+  Assert.throws(() => AutoMigrate.pickMigrator(),
+                /Can't automatically migrate from Firefox/,
+                "Should throw when implicitly picking Firefox.");
+  gShimmedMigratorKeyPicker = function() {
+    return "gobbledygook";
+  };
+  Assert.throws(() => AutoMigrate.pickMigrator(),
+                /migrator object is not available/,
+                "Should throw when an unknown migrator is the default");
+  gShimmedMigratorKeyPicker = function() {
+    return "";
+  };
+  Assert.throws(() => AutoMigrate.pickMigrator(),
+                /Could not determine default browser key/,
+                "Should throw when an unknown migrator is the default");
+});
+
+
+/**
+ * Test automatically picking a profile to migrate from
+ */
+add_task(function* checkProfilePicking() {
+  let fakeMigrator = {sourceProfiles: [{id: "a"}, {id: "b"}]};
+  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
+                /Don't know how to pick a profile when more/,
+                "Should throw when there are multiple profiles.");
+  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
+                /Profile specified was not found/,
+                "Should throw when the profile supplied doesn't exist.");
+  let profileToMigrate = AutoMigrate.pickProfile(fakeMigrator, "b");
+  Assert.equal(profileToMigrate, "b", "Should return profile supplied");
+
+  fakeMigrator.sourceProfiles = null;
+  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator, "c"),
+                /Profile specified but only a default profile found./,
+                "Should throw when the profile supplied doesn't exist.");
+  profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
+  Assert.equal(profileToMigrate, null, "Should return default profile when that's the only one.");
+
+  fakeMigrator.sourceProfiles = [];
+  Assert.throws(() => AutoMigrate.pickProfile(fakeMigrator),
+                /No profile data found/,
+                "Should throw when no profile data is present.");
+
+  fakeMigrator.sourceProfiles = [{id: "a"}];
+  profileToMigrate = AutoMigrate.pickProfile(fakeMigrator);
+  Assert.equal(profileToMigrate, "a", "Should return the only profile if only one is present.");
+});
+
+/**
+ * Test the complete automatic process including browser and profile selection,
+ * and actual migration (which implies startup)
+ */
+add_task(function* checkIntegration() {
+  gShimmedMigrator = {
+    get sourceProfiles() {
+      do_print("Read sourceProfiles");
+      return null;
+    },
+    getMigrateData(profileToMigrate) {
+      this._getMigrateDataArgs = profileToMigrate;
+      return Ci.nsIBrowserProfileMigrator.BOOKMARKS;
+    },
+    migrate(types, startup, profileToMigrate) {
+      this._migrateArgs = [types, startup, profileToMigrate];
+    },
+  };
+  gShimmedMigratorKeyPicker = function() {
+    return "gobbledygook";
+  };
+  AutoMigrate.migrate("startup");
+  Assert.strictEqual(gShimmedMigrator._getMigrateDataArgs, null,
+                     "getMigrateData called with 'null' as a profile");
+
+  let {BOOKMARKS, HISTORY, FORMDATA, PASSWORDS} = Ci.nsIBrowserProfileMigrator;
+  let expectedTypes = BOOKMARKS | HISTORY | FORMDATA | PASSWORDS;
+  Assert.deepEqual(gShimmedMigrator._migrateArgs, [expectedTypes, "startup", null],
+                   "getMigrateData called with 'null' as a profile");
+});
--- a/browser/components/migration/tests/unit/xpcshell.ini
+++ b/browser/components/migration/tests/unit/xpcshell.ini
@@ -2,16 +2,17 @@
 head = head_migration.js
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   Library/**
   AppData/**
 
+[test_automigration.js]
 [test_Chrome_cookies.js]
 skip-if = os != "mac" # Relies on ULibDir
 [test_Chrome_passwords.js]
 skip-if = os != "win"
 [test_Edge_availability.js]
 [test_Edge_db_migration.js]
 skip-if = os != "win" || os_version == "5.1" || os_version == "5.2" # Relies on post-XP bits of ESEDB
 [test_fx_telemetry.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -4230,16 +4230,25 @@
   },
   "FX_MIGRATION_HOMEPAGE_IMPORTED": {
     "expires_in_version": "49",
     "kind": "boolean",
     "keyed": true,
     "releaseChannelCollection": "opt-out",
     "description": "Whether the homepage was imported during browser migration. Only available on release builds during firstrun."
   },
+  "FX_STARTUP_MIGRATION_AUTOMATED_IMPORT_SUCCEEDED": {
+    "bug_numbers": [1271775],
+    "alert_emails": ["gijs@mozilla.com"],
+    "expires_in_version": "53",
+    "kind": "count",
+    "keyed": true,
+    "releaseChannelCollection": "opt-out",
+    "description": "Where automatic migration was attempted, indicates to what degree we succeeded."
+  },
   "INPUT_EVENT_RESPONSE_MS": {
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
     "bug_numbers": [1235908],
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 10000,
     "n_buckets": 50,
     "description": "Time (ms) from the Input event being created to the end of it being handled"