Bug 1271775 - allow bypassing the initial migration dialog, r?jaws draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Wed, 01 Jun 2016 19:00:53 +0100
changeset 376750 783455de2b132f715571a833231dbbf2a0f06ab2
parent 376592 d31c4e9e910d1ea3cad06449943c17675c51392a
child 523227 55905bb6e0be05f256eb3ead608db16013a1551b
push id20659
push usergijskruitbosch@gmail.com
push dateWed, 08 Jun 2016 16:48:27 +0000
reviewersjaws
bugs1271775
milestone50.0a1
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
@@ -1403,12 +1403,14 @@ pref("toolkit.pageThumbs.minHeight", 190
 
 // Enable speech synthesis
 pref("media.webspeech.synth.enabled", true);
 
 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 [
@@ -90,17 +92,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.
    *
@@ -691,18 +693,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,
@@ -716,16 +731,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
@@ -22,16 +22,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() {
+      dump("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
@@ -4447,16 +4447,25 @@
     "bug_numbers": [1275114],
     "alert_emails": ["gijs@mozilla.com"],
     "expires_in_version": "53",
     "kind": "enumerated",
     "n_values": 15,
     "releaseChannelCollection": "opt-out",
     "description": "The browser that was the default on the initial profile migration. The values correspond to the internal browser ID (see MigrationUtils.jsm)"
   },
+  "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."
+  },
   "FX_STARTUP_EXTERNAL_CONTENT_HANDLER": {
     "bug_numbers": [1276027],
     "alert_emails": ["jaws@mozilla.com"],
     "expires_in_version": "53",
     "kind": "count",
     "description": "Count how often the browser is opened as an external app handler. This is generally used when the browser is set as the default browser."
   },
   "INPUT_EVENT_RESPONSE_MS": {