Bug 1258921 - Expose logininfo's guid to the content process and use it to delete logins via the autocomplete popup. r=MattN
authorBlake Kaplan <mrbkap@gmail.com>
Fri, 08 Apr 2016 11:21:07 -0700
changeset 331549 04db9fe12e0835f16b9ca46d5b774b64016a143f
parent 331548 4c229ab19c492dfda5d513bfa1cee532cc9b83b4
child 331550 53252c783a434673b8c56728c2273f984883bc53
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs1258921
milestone48.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
Bug 1258921 - Expose logininfo's guid to the content process and use it to delete logins via the autocomplete popup. r=MattN
toolkit/components/passwordmgr/LoginHelper.jsm
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/passwordmgr/nsLoginManager.js
toolkit/components/satchel/AutoCompleteE10S.jsm
--- a/toolkit/components/passwordmgr/LoginHelper.jsm
+++ b/toolkit/components/passwordmgr/LoginHelper.jsm
@@ -396,10 +396,59 @@ this.LoginHelper = {
         }
       }
     }
     // if the new login is an update or is older than an exiting login, don't add it.
     if (foundMatchingLogin) {
       return;
     }
     Services.logins.addLogin(login);
+  },
+
+  /**
+   * Convert an array of nsILoginInfo to vanilla JS objects suitable for
+   * sending over IPC.
+   *
+   * NB: All members of nsILoginInfo and nsILoginMetaInfo are strings.
+   */
+  loginsToVanillaObjects(logins) {
+    return logins.map(this.loginToVanillaObject);
+  },
+
+  /**
+   * Same as above, but for a single login.
+   */
+  loginToVanillaObject(login) {
+    let obj = {};
+    for (let i in login) {
+      if (typeof login[i] !== 'function') {
+        obj[i] = login[i];
+      }
+    }
+
+    login.QueryInterface(Ci.nsILoginMetaInfo);
+    obj.guid = login.guid;
+    return obj;
+  },
+
+  /**
+   * Convert an object received from IPC into an nsILoginInfo (with guid).
+   */
+  vanillaObjectToLogin(login) {
+    var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
+                  createInstance(Ci.nsILoginInfo);
+    formLogin.init(login.hostname, login.formSubmitURL,
+                   login.httpRealm, login.username,
+                   login.password, login.usernameField,
+                   login.passwordField);
+
+    formLogin.QueryInterface(Ci.nsILoginMetaInfo);
+    formLogin.guid = login.guid;
+    return formLogin;
+  },
+
+  /**
+   * As above, but for an array of objects.
+   */
+  vanillaObjectsToLogins(logins) {
+    return logins.map(this.vanillaObjectToLogin);
   }
 };
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -171,56 +171,49 @@ var LoginManagerContent = {
 
     let deferred = Promise.defer();
     requestData.promise = deferred;
     this._requests.set(requestId, requestData);
     return deferred.promise;
   },
 
   receiveMessage: function (msg, window) {
-    // Convert an array of logins in simple JS-object form to an array of
-    // nsILoginInfo objects.
-    function jsLoginsToXPCOM(logins) {
-      return logins.map(login => {
-        var formLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].
-                      createInstance(Ci.nsILoginInfo);
-        formLogin.init(login.hostname, login.formSubmitURL,
-                       login.httpRealm, login.username,
-                       login.password, login.usernameField,
-                       login.passwordField);
-        return formLogin;
-      });
-    }
-
     if (msg.name == "RemoteLogins:fillForm") {
       this.fillForm({
         topDocument: window.document,
         loginFormOrigin: msg.data.loginFormOrigin,
-        loginsFound: jsLoginsToXPCOM(msg.data.logins),
+        loginsFound: LoginHelper.vanillaObjectsToLogins(msg.data.logins),
         recipes: msg.data.recipes,
         inputElement: msg.objects.inputElement,
       });
       return;
     }
 
     let request = this._takeRequest(msg);
     switch (msg.name) {
       case "RemoteLogins:loginsFound": {
-        let loginsFound = jsLoginsToXPCOM(msg.data.logins);
+        let loginsFound = LoginHelper.vanillaObjectsToLogins(msg.data.logins);
         request.promise.resolve({
           form: request.form,
           loginsFound: loginsFound,
           recipes: msg.data.recipes,
         });
         break;
       }
 
       case "RemoteLogins:loginsAutoCompleted": {
-        let loginsFound = jsLoginsToXPCOM(msg.data.logins);
-        request.promise.resolve(loginsFound);
+        let loginsFound =
+          LoginHelper.vanillaObjectsToLogins(msg.data.logins);
+        // If we're in the parent process, don't pass a message manager so our
+        // autocomplete result objects know they can remove the login from the
+        // login manager directly.
+        let messageManager =
+          (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) ?
+            msg.target : undefined;
+        request.promise.resolve({ logins: loginsFound, messageManager });
         break;
       }
     }
   },
 
   /**
    * Get relevant logins and recipes from the parent
    *
@@ -257,21 +250,26 @@ var LoginManagerContent = {
     let formOrigin = LoginUtils._getPasswordOrigin(doc.documentURI);
     let actionOrigin = LoginUtils._getActionOrigin(form);
 
     let messageManager = messageManagerFromWindow(win);
 
     let remote = (Services.appinfo.processType ===
                   Services.appinfo.PROCESS_TYPE_CONTENT);
 
+    let previousResult = aPreviousResult ?
+                           { searchString: aPreviousResult.searchString,
+                             logins: LoginHelper.loginsToVanillaObjects(aPreviousResult.logins) } :
+                           null;
+
     let requestData = {};
     let messageData = { formOrigin: formOrigin,
                         actionOrigin: actionOrigin,
                         searchString: aSearchString,
-                        previousResult: aPreviousResult,
+                        previousResult: previousResult,
                         rect: aRect,
                         remote: remote };
 
     return this._sendRequest(messageManager, requestData,
                              "RemoteLogins:autoCompleteLogins",
                              messageData);
   },
 
@@ -1175,33 +1173,34 @@ var LoginUtils = {
     if (uriString == "")
       uriString = form.baseURI; // ala bug 297761
 
     return this._getPasswordOrigin(uriString, true);
   },
 };
 
 // nsIAutoCompleteResult implementation
-function UserAutoCompleteResult (aSearchString, matchingLogins) {
+function UserAutoCompleteResult (aSearchString, matchingLogins, messageManager) {
   function loginSort(a,b) {
     var userA = a.username.toLowerCase();
     var userB = b.username.toLowerCase();
 
     if (userA < userB)
       return -1;
 
     if (userA > userB)
       return  1;
 
     return 0;
   }
 
   this.searchString = aSearchString;
   this.logins = matchingLogins.sort(loginSort);
   this.matchCount = matchingLogins.length;
+  this._messageManager = messageManager;
 
   if (this.matchCount > 0) {
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
     this.defaultIndex = 0;
   }
 }
 
 UserAutoCompleteResult.prototype = {
@@ -1257,19 +1256,23 @@ UserAutoCompleteResult.prototype = {
 
     var [removedLogin] = this.logins.splice(index, 1);
 
     this.matchCount--;
     if (this.defaultIndex > this.logins.length)
       this.defaultIndex--;
 
     if (removeFromDB) {
-      var pwmgr = Cc["@mozilla.org/login-manager;1"].
-                  getService(Ci.nsILoginManager);
-      pwmgr.removeLogin(removedLogin);
+      if (this._messageManager) {
+        let vanilla = LoginHelper.loginToVanillaObject(removedLogin);
+        this._messageManager.sendAsyncMessage("RemoteLogins:removeLogin",
+                                              { login: vanilla });
+      } else {
+        Services.logins.removeLogin(removedLogin);
+      }
     }
   }
 };
 
 /**
  * A factory to generate FormLike objects that represent a set of login fields
  * which aren't necessarily marked up with a <form> element.
  */
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -42,16 +42,17 @@ var LoginManagerParent = {
 
   init: function() {
     let mm = Cc["@mozilla.org/globalmessagemanager;1"]
                .getService(Ci.nsIMessageListenerManager);
     mm.addMessageListener("RemoteLogins:findLogins", this);
     mm.addMessageListener("RemoteLogins:findRecipes", this);
     mm.addMessageListener("RemoteLogins:onFormSubmit", this);
     mm.addMessageListener("RemoteLogins:autoCompleteLogins", this);
+    mm.addMessageListener("RemoteLogins:removeLogin", this);
     mm.addMessageListener("RemoteLogins:updateLoginFormPresence", this);
 
     XPCOMUtils.defineLazyGetter(this, "recipeParentPromise", () => {
       const { LoginRecipesParent } = Cu.import("resource://gre/modules/LoginRecipes.jsm", {});
       this._recipeManager = new LoginRecipesParent({
         defaults: Services.prefs.getComplexValue("signon.recipes.path", Ci.nsISupportsString).data,
       });
       return this._recipeManager.initializationPromise;
@@ -93,16 +94,22 @@ var LoginManagerParent = {
         this.updateLoginFormPresence(msg.target, data);
         break;
       }
 
       case "RemoteLogins:autoCompleteLogins": {
         this.doAutocompleteSearch(data, msg.target);
         break;
       }
+
+      case "RemoteLogins:removeLogin": {
+        let login = LoginHelper.vanillaObjectToLogin(data.login);
+        AutoCompleteE10S.removeLogin(login);
+        break;
+      }
     }
 
     return undefined;
   },
 
   /**
    * Trigger a login form fill and send relevant data (e.g. logins and recipes)
    * to the child process (LoginManagerContent).
@@ -117,17 +124,17 @@ var LoginManagerParent = {
         recipes = recipeManager.getRecipesForHost(formHost);
       } catch (ex) {
         // Some schemes e.g. chrome aren't supported by URL
       }
     }
 
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
-    let jsLogins = JSON.parse(JSON.stringify([login]));
+    let jsLogins = [LoginHelper.loginToVanillaObject(login)];
 
     let objects = inputElement ? {inputElement} : null;
     browser.messageManager.sendAsyncMessage("RemoteLogins:fillForm", {
       loginFormOrigin,
       logins: jsLogins,
       recipes,
     }, objects);
   }),
@@ -213,17 +220,17 @@ var LoginManagerParent = {
       Services.obs.addObserver(observer, "passwordmgr-crypto-login", false);
       Services.obs.addObserver(observer, "passwordmgr-crypto-loginCanceled", false);
       return;
     }
 
     var logins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
-    var jsLogins = JSON.parse(JSON.stringify(logins));
+    var jsLogins = LoginHelper.loginsToVanillaObjects(logins);
     target.sendAsyncMessage("RemoteLogins:loginsFound", {
       requestId: requestId,
       logins: jsLogins,
       recipes,
     });
 
     const PWMGR_FORM_ACTION_EFFECT =  Services.telemetry.getHistogramById("PWMGR_FORM_ACTION_EFFECT");
     if (logins.length == 0) {
@@ -246,17 +253,17 @@ var LoginManagerParent = {
     let searchStringLower = searchString.toLowerCase();
     let logins;
     if (previousResult &&
         searchStringLower.startsWith(previousResult.searchString.toLowerCase())) {
       log("Using previous autocomplete result");
 
       // We have a list of results for a shorter search string, so just
       // filter them further based on the new search string.
-      logins = previousResult.logins;
+      logins = LoginHelper.vanillaObjectsToLogins(previousResult.logins);
     } else {
       log("Creating new autocomplete search result.");
 
       // Grab the logins from the database.
       logins = Services.logins.findLogins({}, formOrigin, actionOrigin, null);
     }
 
     let matchingLogins = logins.filter(function(fullMatch) {
@@ -274,17 +281,17 @@ var LoginManagerParent = {
     // nsAutoCompleteController).
     if (remote) {
       result = new UserAutoCompleteResult(searchString, matchingLogins);
       AutoCompleteE10S.showPopupWithResults(target.ownerDocument.defaultView, rect, result);
     }
 
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
-    var jsLogins = JSON.parse(JSON.stringify(matchingLogins));
+    var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
     target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
       requestId: requestId,
       logins: jsLogins,
     });
   },
 
   onFormSubmit: function(hostname, formSubmitURL,
                          usernameField, newPasswordField,
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -458,18 +458,18 @@ LoginManager.prototype = {
                          logins: aPreviousResult.wrappedJSObject.logins };
     } else {
       previousResult = null;
     }
 
     let rect = BrowserUtils.getElementBoundingScreenRect(aElement);
     LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult,
                                                  aElement, rect)
-                       .then(function(logins) {
+                       .then(function({ logins, messageManager }) {
                          let results =
-                             new UserAutoCompleteResult(aSearchString, logins);
+                             new UserAutoCompleteResult(aSearchString, logins, messageManager);
                          aCallback.onSearchCompletion(results);
                        })
                        .then(null, Cu.reportError);
   },
 }; // end of LoginManager implementation
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);
--- a/toolkit/components/satchel/AutoCompleteE10S.jsm
+++ b/toolkit/components/satchel/AutoCompleteE10S.jsm
@@ -142,18 +142,32 @@ this.AutoCompleteE10S = {
   // This function is used by the login manager, which uses a single message
   // to fill in the autocomplete results. See
   // "RemoteLogins:autoCompleteLogins".
   showPopupWithResults: function(browserWindow, rect, results) {
     this._initPopup(browserWindow, rect);
     this._showPopup(results);
   },
 
-  removeEntry(index) {
-    this._resultCache.removeValueAt(index, true);
+  removeLogin(login) {
+    Services.logins.removeLogin(login);
+
+    // It's possible to race and have the deleted login no longer be in our
+    // resultCache's logins, so we remove it from the database above and only
+    // deal with our resultCache below.
+    let idx = this._resultCache.logins.findIndex(cur => {
+      return login.guid === cur.QueryInterface(Ci.nsILoginMetaInfo).guid
+    });
+    if (idx !== -1) {
+      this.removeEntry(idx, false);
+    }
+  },
+
+  removeEntry(index, updateDB = true) {
+    this._resultCache.removeValueAt(index, updateDB);
 
     let selectedIndex = this.popup.selectedIndex;
     this.showPopupWithResults(this._popupCache.browserWindow,
                               this._popupCache.rect,
                               this._resultCache);
 
     // If we removed the last result, bump the selected index back once.
     if (selectedIndex >= this._resultCache.matchCount)