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
This commit is contained in:
Matthew Noorenberghe 2019-05-03 00:08:12 +00:00
parent fd9be937cf
commit f66a9c3135
9 changed files with 178 additions and 155 deletions

View File

@ -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;
},
};

View File

@ -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"];

View File

@ -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'],

View File

@ -18,6 +18,7 @@ TESTING_JS_MODULES += [
]
XPIDL_SOURCES += [
'nsILoginAutoCompleteSearch.idl',
'nsILoginInfo.idl',
'nsILoginManager.idl',
'nsILoginManagerCrypto.idl',

View File

@ -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();
};

View File

@ -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.

View File

@ -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

View File

@ -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;
}

View File

@ -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<nsIAutoCompleteController> mController;
nsCOMPtr<nsILoginManager> mLoginManager;
nsCOMPtr<nsILoginAutoCompleteSearch> mLoginManagerAC;
nsCOMPtr<nsILoginReputationService> mLoginReputationService;
mozilla::dom::HTMLInputElement* mFocusedInput;