Add ability to pre-populate sync credentials from the default Firefox profile in about:accounts (bug 1079835). r=markh
authorPanos Astithas <past@mozilla.com>
Mon, 27 Oct 2014 15:15:12 +0200
changeset 213101 16fe52c3df5f107283e0176096d17606a2682a82
parent 212983 e3194c623ac67f21e17c5e66d3f858e26ac71514
child 213102 5a9f63201e5cdb7ff173f64810376f6a6c1b8850
push id51143
push usercbook@mozilla.com
push dateThu, 30 Oct 2014 14:14:04 +0000
treeherdermozilla-inbound@e80345c5bf6f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1079835
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Add ability to pre-populate sync credentials from the default Firefox profile in about:accounts (bug 1079835). r=markh
browser/app/profile/firefox.js
browser/base/content/aboutaccounts/aboutaccounts.js
browser/base/content/test/general/browser_aboutAccounts.js
browser/base/content/test/general/content_aboutAccounts.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1690,16 +1690,20 @@ pref("identity.fxaccounts.remote.force_a
 // The remote content URL shown for signin in. Must use HTTPS.
 pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v1");
 
 // The URL we take the user to when they opt to "manage" their Firefox Account.
 // Note that this will always need to be in the same TLD as the
 // "identity.fxaccounts.remote.signup.uri" pref.
 pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings");
 
+// Migrate any existing Firefox Account data from the default profile to the
+// Developer Edition profile.
+pref("identity.fxaccounts.migrateToDevEdition", false);
+
 // On GTK, we now default to showing the menubar only when alt is pressed:
 #ifdef MOZ_WIDGET_GTK
 pref("ui.key.menuAccessKeyFocuses", true);
 #endif
 
 // Encrypted media extensions.
 pref("media.eme.enabled", false);
 
--- a/browser/base/content/aboutaccounts/aboutaccounts.js
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -326,18 +326,21 @@ function init() {
       // ideally we would only show this when we know the user is in a
       // "must reauthenticate" state - but we don't.
       // As the email address will be included in the URL returned from
       // promiseAccountsForceSigninURI, just always show it.
       fxAccounts.promiseAccountsForceSigninURI().then(url => {
         show("remote");
         wrapper.init(url, entryPoint);
       });
+    } else if (window.location.href.contains("action=migrateToDevEdition") &&
+               user == null) {
+      migrateToDevEdition(user, entryPoint);
     } else {
-      // No action specified
+      // No action specified, or migration request when we already have a user.
       if (user) {
         show("stage", "manage");
         let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
         document.title = sb.GetStringFromName("manage.pageTitle");
       } else {
         show("stage", "intro");
         // load the remote frame in the background
         wrapper.init(fxAccounts.getAccountsSignUpURI(), entryPoint);
@@ -367,16 +370,58 @@ function show(id, childId) {
         elt.style.display = 'block';
       } else {
         elt.style.display = 'none';
       }
     }
   }
 }
 
+// Migrate sync data from the default profile to the dev-edition profile.
+function migrateToDevEdition(user, entryPoint) {
+  let migrateSyncCreds = false;
+  try {
+    migrateSyncCreds = Services.prefs.getBoolPref("identity.fxaccounts.migrateToDevEdition");
+  } catch (e) {}
+  if (migrateSyncCreds) {
+    Cu.import("resource://gre/modules/osfile.jsm");
+    let fxAccountsStorage = OS.Path.join(window.getDefaultProfilePath(), fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
+    return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => {
+      let accountData = JSON.parse(text).accountData;
+      return fxAccounts.setSignedInUser(accountData);
+    }).then(() => {
+      return fxAccounts.promiseAccountsForceSigninURI().then(url => {
+        show("remote");
+        wrapper.init(url, entryPoint);
+      });
+    }).then(null, error => {
+      log("Failed to migrate FX Account: " + error);
+      show("stage", "intro");
+      // load the remote frame in the background
+      wrapper.init(fxAccounts.getAccountsSignUpURI(), entryPoint);
+    }).then(() => {
+      // Reset the pref after migration.
+      Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
+    });
+  } else {
+    show("stage", "intro");
+    // load the remote frame in the background
+    wrapper.init(fxAccounts.getAccountsSignUpURI(), entryPoint);
+  }
+}
+
+// Helper function that returns the path of the default profile on disk. Will be
+// overridden in tests.
+function getDefaultProfilePath() {
+  let defaultProfile = Cc["@mozilla.org/toolkit/profile-service;1"]
+                        .getService(Ci.nsIToolkitProfileService)
+                        .defaultProfile;
+  return defaultProfile.rootDir.path;
+}
+
 document.addEventListener("DOMContentLoaded", function onload() {
   document.removeEventListener("DOMContentLoaded", onload, true);
   init();
   var buttonGetStarted = document.getElementById('buttonGetStarted');
   buttonGetStarted.addEventListener('click', getStarted);
 
   var oldsync = document.getElementById('oldsync');
   oldsync.addEventListener('click', handleOldSync);
--- a/browser/base/content/test/general/browser_aboutAccounts.js
+++ b/browser/base/content/test/general/browser_aboutAccounts.js
@@ -10,16 +10,18 @@
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: window.location is null");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
   "resource://gre/modules/FxAccounts.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+  "resource://gre/modules/FileUtils.jsm");
 
 const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
 // Preference helpers.
 let changedPrefs = new Set();
 
 function setPref(name, value) {
   changedPrefs.add(name);
   Services.prefs.setCharPref(name, value);
@@ -176,16 +178,117 @@ let gTests = [
     yield setSignedInUser();
     let [tab, url] = yield promiseNewTabWithIframeLoadEvent("about:accounts?action=reauth");
     // The current user will be appended to the url
     let expected = expected_url + "&email=foo%40example.com";
     is(url, expected, "action=reauth got the expected URL");
   },
 },
 {
+  desc: "Test action=migrateToDevEdition (success)",
+  teardown: function* () {
+    gBrowser.removeCurrentTab();
+    yield signOut();
+  },
+  run: function* ()
+  {
+    let fxAccountsCommon = {};
+    Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
+    const pref = "identity.fxaccounts.migrateToDevEdition";
+    changedPrefs.add(pref);
+    Services.prefs.setBoolPref(pref, true);
+
+    // Create the signedInUser.json file that will be used as the source of
+    // migrated user data.
+    let signedInUser = {
+      version: 1,
+      accountData: {
+        email: "foo@example.com",
+        uid: "1234@lcip.org",
+        sessionToken: "dead",
+        verified: true
+      }
+    };
+    // We use a sub-dir of the real profile dir as the "pretend" profile dir
+    // for this test.
+    let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    let mockDir = profD.clone();
+    mockDir.append("about-accounts-mock-profd");
+    mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    let fxAccountsStorage = OS.Path.join(mockDir.path, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
+    yield OS.File.writeAtomic(fxAccountsStorage, JSON.stringify(signedInUser));
+    info("Wrote file " + fxAccountsStorage);
+
+    // this is a little subtle - we load about:blank so we get a tab, then
+    // we send a message which does both (a) load the URL we want and (b) mocks
+    // the default profile path used by about:accounts.
+    let tab = yield promiseNewTabLoadEvent("about:blank?");
+    let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+    let mm = tab.linkedBrowser.messageManager;
+    mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+      url: "about:accounts?action=migrateToDevEdition",
+      profilePath: mockDir.path,
+    });
+
+    let response = yield readyPromise;
+    // We are expecting the iframe to be on the "force reauth" URL
+    let expected = yield fxAccounts.promiseAccountsForceSigninURI();
+    is(response.data.url, expected);
+
+    let userData = yield fxAccounts.getSignedInUser();
+    SimpleTest.isDeeply(userData, signedInUser.accountData, "All account data were migrated");
+    // The migration pref will have been switched off by now.
+    is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+
+    yield OS.File.remove(fxAccountsStorage);
+    yield OS.File.removeEmptyDir(mockDir.path);
+  },
+},
+{
+  desc: "Test action=migrateToDevEdition (no user to migrate)",
+  teardown: function* () {
+    gBrowser.removeCurrentTab();
+    yield signOut();
+  },
+  run: function* ()
+  {
+    const pref = "identity.fxaccounts.migrateToDevEdition";
+    changedPrefs.add(pref);
+    Services.prefs.setBoolPref(pref, true);
+
+    let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+    let mockDir = profD.clone();
+    mockDir.append("about-accounts-mock-profd");
+    mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    // but leave it empty, so we don't think a user is logged in.
+
+    let tab = yield promiseNewTabLoadEvent("about:blank?");
+    let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
+
+    let mm = tab.linkedBrowser.messageManager;
+    mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
+      url: "about:accounts?action=migrateToDevEdition",
+      profilePath: mockDir.path,
+    });
+
+    let response = yield readyPromise;
+    // We are expecting the iframe to be on the "signup" URL
+    let expected = fxAccounts.getAccountsSignUpURI();
+    is(response.data.url, expected);
+
+    // and expect no signed in user.
+    let userData = yield fxAccounts.getSignedInUser();
+    is(userData, null);
+    // The migration pref should have still been switched off.
+    is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
+    yield OS.File.removeEmptyDir(mockDir.path);
+  },
+},
+{
   desc: "Test observers about:accounts",
   teardown: function() {
     gBrowser.removeCurrentTab();
   },
   run: function* () {
     setPref("identity.fxaccounts.remote.signup.uri", "https://example.com/");
     yield setSignedInUser();
     let tab = yield promiseNewTabLoadEvent("about:accounts");
@@ -318,10 +421,11 @@ function setSignedInUser(data) {
       kB: "cafe",
       verified: true
     }
   }
  return fxAccounts.setSignedInUser(data);
 }
 
 function signOut() {
-  return fxAccounts.signOut();
+  // we always want a "localOnly" signout here...
+  return fxAccounts.signOut(true);
 }
--- a/browser/base/content/test/general/content_aboutAccounts.js
+++ b/browser/base/content/test/general/content_aboutAccounts.js
@@ -1,26 +1,32 @@
 /* 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/. */
 
 // This file is loaded as a "content script" for browser_aboutAccounts tests
 "use strict";
 
+const {interfaces: Ci, utils: Cu} = Components;
+
 addEventListener("load", function load(event) {
   if (event.target != content.document) {
     return;
   }
 //  content.document.removeEventListener("load", load, true);
   sendAsyncMessage("test:document:load");
 }, true);
 
 addEventListener("DOMContentLoaded", function domContentLoaded(event) {
   removeEventListener("DOMContentLoaded", domContentLoaded, true);
   let iframe = content.document.getElementById("remote");
+  if (!iframe) {
+    // at least one test initially loads about:blank - in that case, we are done.
+    return;
+  }
   iframe.addEventListener("load", function iframeLoaded(event) {
     if (iframe.contentWindow.location.href == "about:blank" ||
         event.target != iframe) {
       return;
     }
     iframe.removeEventListener("load", iframeLoaded, true);
     sendAsyncMessage("test:iframe:load", {url: iframe.getAttribute("src")});
   }, true);
@@ -41,8 +47,28 @@ addMessageListener("test:check-visibilit
         result[id] = "strange: " + displayStyle; // tests should fail!
       }
     } else {
       result[id] = "doesn't exist: " + id;
     }
   }
   sendAsyncMessage("test:check-visibilities-response", result);
 });
+
+addMessageListener("test:load-with-mocked-profile-path", function (message) {
+  addEventListener("DOMContentLoaded", function domContentLoaded(event) {
+    removeEventListener("DOMContentLoaded", domContentLoaded, true);
+    content.getDefaultProfilePath = () => message.data.profilePath;
+    // now wait for the iframe to load.
+    let iframe = content.document.getElementById("remote");
+    iframe.addEventListener("load", function iframeLoaded(event) {
+      if (iframe.contentWindow.location.href == "about:blank" ||
+          event.target != iframe) {
+        return;
+      }
+      iframe.removeEventListener("load", iframeLoaded, true);
+      sendAsyncMessage("test:load-with-mocked-profile-path-response",
+                       {url: iframe.getAttribute("src")});
+    }, true);
+  });
+  let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+  webNav.loadURI(message.data.url, webNav.LOAD_FLAGS_NONE, null, null, null);
+}, true);