From f66a9c3135cd39221e7042ec2321990c4cd375b9 Mon Sep 17 00:00:00 2001 From: Matthew Noorenberghe Date: Fri, 3 May 2019 00:08:12 +0000 Subject: [PATCH] Bug 1334012 - Move Login AutoComplete to its own interface. r=jaws Differential Revision: https://phabricator.services.mozilla.com/D29694 --HG-- rename : toolkit/components/passwordmgr/nsILoginManager.idl => toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl extra : moz-landing-system : lando --- .../passwordmgr/LoginAutoCompleteResult.jsm | 121 +++++++++++++++++- .../components/passwordmgr/LoginManager.jsm | 121 ++---------------- .../components/passwordmgr/components.conf | 6 + toolkit/components/passwordmgr/moz.build | 1 + .../nsILoginAutoCompleteSearch.idl | 31 +++++ .../passwordmgr/nsILoginManager.idl | 24 ---- .../components/satchel/AutoCompletePopup.jsm | 4 - .../satchel/nsFormFillController.cpp | 21 ++- .../components/satchel/nsFormFillController.h | 4 +- 9 files changed, 178 insertions(+), 155 deletions(-) create mode 100644 toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl diff --git a/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm b/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm index 1fa9f91c4fa4..ceba2aa45251 100644 --- a/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm +++ b/toolkit/components/passwordmgr/LoginAutoCompleteResult.jsm @@ -3,18 +3,29 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** - * nsIAutoCompleteResult implementation for saved logins. + * nsIAutoCompleteResult and nsILoginAutoCompleteSearch implementations for saved logins. */ "use strict"; -var EXPORTED_SYMBOLS = ["LoginAutoCompleteResult"]; +var EXPORTED_SYMBOLS = [ + "LoginAutoComplete", + "LoginAutoCompleteResult", +]; const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +ChromeUtils.defineModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "InsecurePasswordUtils", + "resource://gre/modules/InsecurePasswordUtils.jsm"); +ChromeUtils.defineModuleGetter(this, "LoginFormFactory", + "resource://gre/modules/LoginFormFactory.jsm"); ChromeUtils.defineModuleGetter(this, "LoginHelper", "resource://gre/modules/LoginHelper.jsm"); +ChromeUtils.defineModuleGetter(this, "LoginManagerContent", + "resource://gre/modules/LoginManagerContent.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "formFillController", "@mozilla.org/satchel/form-fill-controller;1", @@ -24,6 +35,7 @@ XPCOMUtils.defineLazyGetter(this, "log", () => { return LoginHelper.createLogger("LoginAutoCompleteResult"); }); + // nsIAutoCompleteResult implementation function LoginAutoCompleteResult(aSearchString, matchingLogins, {isSecure, messageManager, isPasswordField, hostname}) { function loginSort(a, b) { @@ -236,3 +248,108 @@ LoginAutoCompleteResult.prototype = { } }, }; + +function LoginAutoComplete() {} +LoginAutoComplete.prototype = { + classID: Components.ID("{2bdac17c-53f1-4896-a521-682ccdeef3a8}"), + QueryInterface: ChromeUtils.generateQI([Ci.nsILoginAutoCompleteSearch]), + + _autoCompleteLookupPromise: null, + + /** + * Yuck. This is called directly by satchel: + * nsFormFillController::StartSearch() + * [toolkit/components/satchel/nsFormFillController.cpp] + * + * We really ought to have a simple way for code to register an + * auto-complete provider, and not have satchel calling pwmgr directly. + * + * @param {string} aSearchString The value typed in the field. + * @param {nsIAutoCompleteResult} aPreviousResult + * @param {HTMLInputElement} aElement + * @param {nsIFormAutoCompleteObserver} aCallback + */ + startSearch(aSearchString, aPreviousResult, aElement, aCallback) { + let {isNullPrincipal} = aElement.nodePrincipal; + // Show the insecure login warning in the passwords field on null principal documents. + let isSecure = !isNullPrincipal; + // Avoid loading InsecurePasswordUtils.jsm in a sandboxed document (e.g. an ad. frame) if we + // already know it has a null principal and will therefore get the insecure autocomplete + // treatment. + // InsecurePasswordUtils doesn't handle the null principal case as not secure because we don't + // want the same treatment: + // * The web console warnings will be confusing (as they're primarily about http:) and not very + // useful if the developer intentionally sandboxed the document. + // * The site identity insecure field warning would require LoginManagerContent being loaded and + // listening to some of the DOM events we're ignoring in null principal documents. For memory + // reasons it's better to not load LMC at all for these sandboxed frames. Also, if the top- + // document is sandboxing a document, it probably doesn't want that sandboxed document to be + // able to affect the identity icon in the address bar by adding a password field. + if (isSecure) { + let form = LoginFormFactory.createFromField(aElement); + isSecure = InsecurePasswordUtils.isFormSecure(form); + } + let isPasswordField = aElement.type == "password"; + let hostname = aElement.ownerDocument.documentURIObject.host; + + let completeSearch = (autoCompleteLookupPromise, { logins, messageManager }) => { + // If the search was canceled before we got our + // results, don't bother reporting them. + if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) { + return; + } + + this._autoCompleteLookupPromise = null; + let results = new LoginAutoCompleteResult(aSearchString, logins, { + messageManager, + isSecure, + isPasswordField, + hostname, + }); + aCallback.onSearchCompletion(results); + }; + + if (isNullPrincipal) { + // Don't search login storage when the field has a null principal as we don't want to fill + // logins for the `location` in this case. + let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); + acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); + return; + } + + if (isPasswordField && aSearchString) { + // Return empty result on password fields with password already filled. + let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); + acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); + return; + } + + if (!LoginHelper.enabled) { + let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); + acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); + return; + } + + log.debug("AutoCompleteSearch invoked. Search is:", aSearchString); + + let previousResult; + if (aPreviousResult) { + previousResult = { + searchString: aPreviousResult.searchString, + logins: aPreviousResult.wrappedJSObject.logins, + }; + } else { + previousResult = null; + } + + let rect = BrowserUtils.getElementBoundingScreenRect(aElement); + let acLookupPromise = this._autoCompleteLookupPromise = + LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult, + aElement, rect); + acLookupPromise.then(completeSearch.bind(this, acLookupPromise)).catch(log.error); + }, + + stopSearch() { + this._autoCompleteLookupPromise = null; + }, +}; diff --git a/toolkit/components/passwordmgr/LoginManager.jsm b/toolkit/components/passwordmgr/LoginManager.jsm index 917f8e5c4e83..60100f84b466 100644 --- a/toolkit/components/passwordmgr/LoginManager.jsm +++ b/toolkit/components/passwordmgr/LoginManager.jsm @@ -9,16 +9,12 @@ const PERMISSION_SAVE_LOGINS = "login-saving"; const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); -ChromeUtils.defineModuleGetter(this, "BrowserUtils", - "resource://gre/modules/BrowserUtils.jsm"); ChromeUtils.defineModuleGetter(this, "LoginHelper", "resource://gre/modules/LoginHelper.jsm"); ChromeUtils.defineModuleGetter(this, "LoginFormFactory", "resource://gre/modules/LoginFormFactory.jsm"); ChromeUtils.defineModuleGetter(this, "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm"); -ChromeUtils.defineModuleGetter(this, "LoginAutoCompleteResult", - "resource://gre/modules/LoginAutoCompleteResult.jsm"); ChromeUtils.defineModuleGetter(this, "InsecurePasswordUtils", "resource://gre/modules/InsecurePasswordUtils.jsm"); @@ -29,6 +25,11 @@ XPCOMUtils.defineLazyGetter(this, "log", () => { const MS_PER_DAY = 24 * 60 * 60 * 1000; +if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) { + throw new Error("LoginManager.jsm should only run in the parent process"); +} + + function LoginManager() { this.init(); } @@ -56,8 +57,6 @@ LoginManager.prototype = { /* ---------- private members ---------- */ - - _storage: null, // Storage component which contains the saved logins @@ -65,24 +64,17 @@ LoginManager.prototype = { * Initialize the Login Manager. Automatically called when service * is created. * - * Note: Service created in /browser/base/content/browser.js, - * delayedStartup() + * Note: Service created in BrowserGlue#_scheduleStartupIdleTasks() */ init() { // Cache references to current |this| in utility objects this._observer._pwmgr = this; - this._autoCompleteLookupPromise = null; - // Form submit observer checks forms for new logins and pw changes. Services.obs.addObserver(this._observer, "xpcom-shutdown"); + Services.obs.addObserver(this._observer, "passwordmgr-storage-replace"); - if (Services.appinfo.processType === - Services.appinfo.PROCESS_TYPE_DEFAULT) { - Services.obs.addObserver(this._observer, "passwordmgr-storage-replace"); - - // Initialize storage so that asynchronous data loading can start. - this._initStorage(); - } + // Initialize storage so that asynchronous data loading can start. + this._initStorage(); Services.obs.addObserver(this._observer, "gather-telemetry"); }, @@ -464,101 +456,6 @@ LoginManager.prototype = { log.debug("Login saving for", origin, "now enabled?", enabled); LoginHelper.notifyStorageChanged(enabled ? "hostSavingEnabled" : "hostSavingDisabled", origin); }, - - /** - * Yuck. This is called directly by satchel: - * nsFormFillController::StartSearch() - * [toolkit/components/satchel/nsFormFillController.cpp] - * - * We really ought to have a simple way for code to register an - * auto-complete provider, and not have satchel calling pwmgr directly. - */ - autoCompleteSearchAsync(aSearchString, aPreviousResult, - aElement, aCallback) { - // aPreviousResult is an nsIAutoCompleteResult, aElement is - // HTMLInputElement - - let {isNullPrincipal} = aElement.nodePrincipal; - // Show the insecure login warning in the passwords field on null principal documents. - let isSecure = !isNullPrincipal; - // Avoid loading InsecurePasswordUtils.jsm in a sandboxed document (e.g. an ad. frame) if we - // already know it has a null principal and will therefore get the insecure autocomplete - // treatment. - // InsecurePasswordUtils doesn't handle the null principal case as not secure because we don't - // want the same treatment: - // * The web console warnings will be confusing (as they're primarily about http:) and not very - // useful if the developer intentionally sandboxed the document. - // * The site identity insecure field warning would require LoginManagerContent being loaded and - // listening to some of the DOM events we're ignoring in null principal documents. For memory - // reasons it's better to not load LMC at all for these sandboxed frames. Also, if the top- - // document is sandboxing a document, it probably doesn't want that sandboxed document to be - // able to affect the identity icon in the address bar by adding a password field. - if (isSecure) { - let form = LoginFormFactory.createFromField(aElement); - isSecure = InsecurePasswordUtils.isFormSecure(form); - } - let isPasswordField = aElement.type == "password"; - let hostname = aElement.ownerDocument.documentURIObject.host; - - let completeSearch = (autoCompleteLookupPromise, { logins, messageManager }) => { - // If the search was canceled before we got our - // results, don't bother reporting them. - if (this._autoCompleteLookupPromise !== autoCompleteLookupPromise) { - return; - } - - this._autoCompleteLookupPromise = null; - let results = new LoginAutoCompleteResult(aSearchString, logins, { - messageManager, - isSecure, - isPasswordField, - hostname, - }); - aCallback.onSearchCompletion(results); - }; - - if (isNullPrincipal) { - // Don't search login storage when the field has a null principal as we don't want to fill - // logins for the `location` in this case. - let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); - acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); - return; - } - - if (isPasswordField && aSearchString) { - // Return empty result on password fields with password already filled. - let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); - acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); - return; - } - - if (!LoginHelper.enabled) { - let acLookupPromise = this._autoCompleteLookupPromise = Promise.resolve({ logins: [] }); - acLookupPromise.then(completeSearch.bind(this, acLookupPromise)); - return; - } - - log.debug("AutoCompleteSearch invoked. Search is:", aSearchString); - - let previousResult; - if (aPreviousResult) { - previousResult = { searchString: aPreviousResult.searchString, - logins: aPreviousResult.wrappedJSObject.logins }; - } else { - previousResult = null; - } - - let rect = BrowserUtils.getElementBoundingScreenRect(aElement); - let acLookupPromise = this._autoCompleteLookupPromise = - LoginManagerContent._autoCompleteSearchAsync(aSearchString, previousResult, - aElement, rect); - acLookupPromise.then(completeSearch.bind(this, acLookupPromise)) - .catch(Cu.reportError); - }, - - stopSearch() { - this._autoCompleteLookupPromise = null; - }, }; // end of LoginManager implementation var EXPORTED_SYMBOLS = ["LoginManager"]; diff --git a/toolkit/components/passwordmgr/components.conf b/toolkit/components/passwordmgr/components.conf index f6c0ddd9a1e9..cf2e29a3cbd8 100644 --- a/toolkit/components/passwordmgr/components.conf +++ b/toolkit/components/passwordmgr/components.conf @@ -17,6 +17,12 @@ Classes = [ 'jsm': 'resource://gre/modules/LoginManagerPrompter.jsm', 'constructor': 'LoginManagerPromptFactory', }, + { + 'cid': '{2bdac17c-53f1-4896-a521-682ccdeef3a8}', + 'contract_ids': ['@mozilla.org/login-manager/autocompletesearch;1'], + 'jsm': 'resource://gre/modules/LoginAutoCompleteResult.jsm', + 'constructor': 'LoginAutoComplete', + }, { 'cid': '{8aa66d77-1bbb-45a6-991e-b8f47751c291}', 'contract_ids': ['@mozilla.org/login-manager/prompter;1'], diff --git a/toolkit/components/passwordmgr/moz.build b/toolkit/components/passwordmgr/moz.build index edb79a84feec..bc4c314d8760 100644 --- a/toolkit/components/passwordmgr/moz.build +++ b/toolkit/components/passwordmgr/moz.build @@ -18,6 +18,7 @@ TESTING_JS_MODULES += [ ] XPIDL_SOURCES += [ + 'nsILoginAutoCompleteSearch.idl', 'nsILoginInfo.idl', 'nsILoginManager.idl', 'nsILoginManagerCrypto.idl', diff --git a/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl b/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl new file mode 100644 index 000000000000..6c0e94b4453f --- /dev/null +++ b/toolkit/components/passwordmgr/nsILoginAutoCompleteSearch.idl @@ -0,0 +1,31 @@ +/* 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/. */ + +#include "nsISupports.idl" + +interface nsIAutoCompleteResult; +interface nsIFormAutoCompleteObserver; + +webidl HTMLInputElement; + +[scriptable, uuid(2bdac17c-53f1-4896-a521-682ccdeef3a8)] +interface nsILoginAutoCompleteSearch : nsISupports { + /** + * Generate results for a login field autocomplete menu. + * + * NOTE: This interface is provided for use only by the FormFillController, + * which calls it directly. This isn't really ideal, it should + * probably be callback registered through the FFC. + * NOTE: This API is different than nsIAutoCompleteSearch. + */ + void startSearch(in AString aSearchString, + in nsIAutoCompleteResult aPreviousResult, + in HTMLInputElement aElement, + in nsIFormAutoCompleteObserver aListener); + + /** + * Stop a previously-started search. + */ + void stopSearch(); +}; diff --git a/toolkit/components/passwordmgr/nsILoginManager.idl b/toolkit/components/passwordmgr/nsILoginManager.idl index babd5a0e505b..b5d3eb82937f 100644 --- a/toolkit/components/passwordmgr/nsILoginManager.idl +++ b/toolkit/components/passwordmgr/nsILoginManager.idl @@ -2,17 +2,11 @@ * 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/. */ - #include "nsISupports.idl" -interface nsIURI; interface nsILoginInfo; -interface nsIAutoCompleteResult; -interface nsIFormAutoCompleteObserver; interface nsIPropertyBag; -webidl HTMLInputElement; - [scriptable, uuid(38c7f6af-7df9-49c7-b558-2776b24e6cc1)] interface nsILoginManager : nsISupports { /** @@ -217,24 +211,6 @@ interface nsILoginManager : nsISupports { unsigned long countLogins(in AString aHostname, in AString aActionURL, in AString aHttpRealm); - - /** - * Generate results for a userfield autocomplete menu. - * - * NOTE: This interface is provided for use only by the FormFillController, - * which calls it directly. This isn't really ideal, it should - * probably be callback registered through the FFC. - */ - void autoCompleteSearchAsync(in AString aSearchString, - in nsIAutoCompleteResult aPreviousResult, - in HTMLInputElement aElement, - in nsIFormAutoCompleteObserver aListener); - - /** - * Stop a previously-started async search. - */ - void stopSearch(); - /** * Search for logins in the login manager. An array is always returned; * if there are no logins the array is empty. diff --git a/toolkit/components/satchel/AutoCompletePopup.jsm b/toolkit/components/satchel/AutoCompletePopup.jsm index fc33a26b08bb..70c83149f95c 100644 --- a/toolkit/components/satchel/AutoCompletePopup.jsm +++ b/toolkit/components/satchel/AutoCompletePopup.jsm @@ -150,10 +150,6 @@ this.AutoCompletePopup = { } }, - // Along with being called internally by the receiveMessage handler, - // this function is also called directly by the login manager, which - // uses a single message to fill in the autocomplete results. See - // "PasswordManager:autoCompleteLogins". showPopupWithResults({ browser, rect, dir, results }) { if (!results.length || this.openedPopup) { // We shouldn't ever be showing an empty popup, and if we diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp index 5073b385c60c..61f85fc22b12 100644 --- a/toolkit/components/satchel/nsFormFillController.cpp +++ b/toolkit/components/satchel/nsFormFillController.cpp @@ -33,7 +33,7 @@ #include "nsIContentViewer.h" #include "nsIContent.h" #include "nsRect.h" -#include "nsILoginManager.h" +#include "nsILoginAutoCompleteSearch.h" #include "nsToolkitCompsCID.h" #include "nsEmbedCID.h" #include "nsContentUtils.h" @@ -65,7 +65,7 @@ static nsIFormAutoComplete* GetFormAutoComplete() { return sInstance; } -NS_IMPL_CYCLE_COLLECTION(nsFormFillController, mController, mLoginManager, +NS_IMPL_CYCLE_COLLECTION(nsFormFillController, mController, mLoginManagerAC, mLoginReputationService, mFocusedPopup, mDocShells, mPopups, mLastListener, mLastFormAutoComplete) @@ -298,8 +298,8 @@ nsFormFillController::MarkAsLoginManagerField(HTMLInputElement* aInput) { } } - if (!mLoginManager) { - mLoginManager = do_GetService("@mozilla.org/login-manager;1"); + if (!mLoginManagerAC) { + mLoginManagerAC = do_GetService("@mozilla.org/login-manager/autocompletesearch;1"); } return NS_OK; @@ -707,19 +707,18 @@ nsFormFillController::StartSearch(const nsAString& aSearchString, // Handle the case where a password field is focused but // MarkAsLoginManagerField wasn't called because password manager is // disabled. - if (!mLoginManager) { - mLoginManager = do_GetService("@mozilla.org/login-manager;1"); + if (!mLoginManagerAC) { + mLoginManagerAC = do_GetService("@mozilla.org/login-manager/autocompletesearch;1"); } - if (NS_WARN_IF(!mLoginManager)) { + if (NS_WARN_IF(!mLoginManagerAC)) { return NS_ERROR_FAILURE; } // XXX aPreviousResult shouldn't ever be a historyResult type, since we're // not letting satchel manage the field? mLastListener = aListener; - rv = mLoginManager->AutoCompleteSearchAsync(aSearchString, aPreviousResult, - mFocusedInput, this); + rv = mLoginManagerAC->StartSearch(aSearchString, aPreviousResult, mFocusedInput, this); NS_ENSURE_SUCCESS(rv, rv); } else { MOZ_LOG(sLogger, LogLevel::Debug, ("StartSearch: non-login field")); @@ -800,8 +799,8 @@ nsFormFillController::StopSearch() { if (mLastFormAutoComplete) { mLastFormAutoComplete->StopAutoCompleteSearch(); mLastFormAutoComplete = nullptr; - } else if (mLoginManager) { - mLoginManager->StopSearch(); + } else if (mLoginManagerAC) { + mLoginManagerAC->StopSearch(); } return NS_OK; } diff --git a/toolkit/components/satchel/nsFormFillController.h b/toolkit/components/satchel/nsFormFillController.h index f835701fe55a..d34e45b9de85 100644 --- a/toolkit/components/satchel/nsFormFillController.h +++ b/toolkit/components/satchel/nsFormFillController.h @@ -16,7 +16,7 @@ #include "nsCOMPtr.h" #include "nsDataHashtable.h" #include "nsIDocShell.h" -#include "nsILoginManager.h" +#include "nsILoginAutoCompleteSearch.h" #include "nsIMutationObserver.h" #include "nsTArray.h" #include "nsCycleCollectionParticipant.h" @@ -110,7 +110,7 @@ class nsFormFillController final : public nsIFormFillController, // members ////////////////////////////////////////// nsCOMPtr mController; - nsCOMPtr mLoginManager; + nsCOMPtr mLoginManagerAC; nsCOMPtr mLoginReputationService; mozilla::dom::HTMLInputElement* mFocusedInput;