summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/LoginManagerChild.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/LoginManagerChild.sys.mjs338
1 files changed, 258 insertions, 80 deletions
diff --git a/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs b/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs
index 66f45416f8..5433083ede 100644
--- a/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs
+++ b/toolkit/components/passwordmgr/LoginManagerChild.sys.mjs
@@ -51,10 +51,12 @@ ChromeUtils.defineESModuleGetters(lazy, {
FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
+ LoginAutoCompleteResult: "resource://gre/modules/LoginAutoComplete.sys.mjs",
LoginFormFactory: "resource://gre/modules/LoginFormFactory.sys.mjs",
LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
LoginRecipesContent: "resource://gre/modules/LoginRecipes.sys.mjs",
LoginManagerTelemetry: "resource://gre/modules/LoginManagerTelemetry.sys.mjs",
+ NewPasswordModel: "resource://gre/modules/NewPasswordModel.sys.mjs",
});
XPCOMUtils.defineLazyServiceGetter(
@@ -157,40 +159,6 @@ const observer = {
loginManagerChild()._onNavigation(window.document);
},
- // nsIObserver
- observe(subject, topic, _data) {
- switch (topic) {
- case "autocomplete-did-enter-text": {
- let input = subject.QueryInterface(Ci.nsIAutoCompleteInput);
- let { selectedIndex } = input.popup;
- if (selectedIndex < 0 || selectedIndex >= input.controller.matchCount) {
- break;
- }
-
- let { focusedInput } = lazy.gFormFillService;
- if (focusedInput.nodePrincipal.isNullPrincipal) {
- // If we have a null principal then prevent any more password manager code from running and
- // incorrectly using the document `location`.
- return;
- }
-
- let window = focusedInput.ownerGlobal;
- let loginManagerChild = LoginManagerChild.forWindow(window);
-
- let style = input.controller.getStyleAt(selectedIndex);
- if (style == "login" || style == "loginWithOrigin") {
- let details = JSON.parse(
- input.controller.getCommentAt(selectedIndex)
- );
- loginManagerChild.onFieldAutoComplete(focusedInput, details.guid);
- } else if (style == "generatedPassword") {
- loginManagerChild._filledWithGeneratedPassword(focusedInput);
- }
- break;
- }
- }
- },
-
// nsIDOMEventListener
handleEvent(aEvent) {
if (!aEvent.isTrusted) {
@@ -435,9 +403,6 @@ const observer = {
},
};
-// Add this observer once for the process.
-Services.obs.addObserver(observer, "autocomplete-did-enter-text");
-
/**
* Form scenario defines what can be done with form.
*/
@@ -812,7 +777,9 @@ export class LoginFormState {
// The login manager is responsible for fields with the "webauthn" credential type.
let acCredentialType = focusedField.getAutocompleteInfo()?.credentialType;
if (acCredentialType == "webauthn") {
- lazy.gFormFillService.markAsLoginManagerField(focusedField);
+ const actor =
+ focusedField.ownerGlobal.windowGlobalChild.getActor("LoginManager");
+ actor.markAsAutoCompletableField(focusedField);
}
lazy.log("Opening the autocomplete popup.");
@@ -1525,6 +1492,16 @@ export class LoginManagerChild extends JSWindowActorChild {
this.notifyObserversOfFormProcessed(msg.data.formid);
break;
}
+ case "PasswordManager:fillFields": {
+ const login = lazy.LoginHelper.vanillaObjectToLogin(msg.data);
+ this.fillFields(login);
+ break;
+ }
+ case "PasswordManager:fillGeneratedPassword": {
+ const { focusedInput } = lazy.gFormFillService;
+ this.filledWithGeneratedPassword(focusedInput);
+ break;
+ }
}
return undefined;
@@ -2174,7 +2151,7 @@ export class LoginManagerChild extends JSWindowActorChild {
/**
* A username or password was autocompleted into a field.
*/
- onFieldAutoComplete(acInputField, loginGUID) {
+ onFieldAutoComplete(acInputField, login) {
if (!lazy.LoginHelper.enabled) {
return;
}
@@ -2189,7 +2166,7 @@ export class LoginManagerChild extends JSWindowActorChild {
}
if (lazy.LoginHelper.isUsernameFieldType(acInputField)) {
- this.onUsernameAutocompleted(acInputField, loginGUID);
+ this.onUsernameAutocompleted(acInputField, [login]);
} else if (acInputField.hasBeenTypePassword) {
// Ensure the field gets re-masked and edits don't overwrite the generated
// password in case a generated password was filled into it previously.
@@ -2203,7 +2180,7 @@ export class LoginManagerChild extends JSWindowActorChild {
* A username field was filled or tabbed away from so try fill in the
* associated password in the password field.
*/
- onUsernameAutocompleted(acInputField, loginGUID = null) {
+ async onUsernameAutocompleted(acInputField, loginsFound = null) {
lazy.log(`Autocompleting input field with name: ${acInputField.name}`);
let acForm = lazy.LoginFormFactory.createFromField(acInputField);
@@ -2219,43 +2196,49 @@ export class LoginManagerChild extends JSWindowActorChild {
const docState = this.stateForDocument(acInputField.ownerDocument);
let { usernameField, newPasswordField: passwordField } =
docState._getFormFields(acForm, false, recipes);
- if (usernameField == acInputField) {
- // Fill the form when a password field is present.
- if (passwordField) {
- this._getLoginDataFromParent(acForm, {
- guid: loginGUID,
- showPrimaryPassword: false,
- })
- .then(({ form, loginsFound, recipes }) => {
- if (!loginGUID) {
- // not an explicit autocomplete menu selection, filter for exact matches only
- loginsFound = this._filterForExactFormOriginLogins(
- loginsFound,
- acForm
- );
- // filter the list for exact matches with the username
- // NOTE: this could be an empty string which is a valid username
- let searchString = usernameField.value.toLowerCase();
- loginsFound = loginsFound.filter(
- l => l.username.toLowerCase() == searchString
- );
- }
-
- this._fillForm(form, loginsFound, recipes, {
- autofillForm: true,
- clobberPassword: true,
- userTriggered: true,
- });
- })
- .catch(console.error);
- // Use `loginGUID !== null` to distinguish whether this is called when the
- // field is filled or tabbed away from. For the latter, don't highlight the field.
- } else if (loginGUID !== null) {
+ // Ignore the event, it's for some input we don't care about.
+ if (usernameField != acInputField) {
+ return;
+ }
+
+ if (!passwordField) {
+ // Use `loginsFound !== null` to distinguish whether this is called when the
+ // field is filled or tabbed away from. For the latter, don't highlight the field.
+ if (loginsFound !== null) {
LoginFormState._highlightFilledField(usernameField);
}
- } else {
- // Ignore the event, it's for some input we don't care about.
+ return;
+ }
+
+ // Fill the form when a password field is present.
+ if (!loginsFound) {
+ const loginData = await this._getLoginDataFromParent(acForm, {
+ showPrimaryPassword: false,
+ }).catch(console.error);
+
+ if (!loginData?.loginsFound.length) {
+ return;
+ }
+
+ // not an explicit autocomplete menu selection, filter for exact matches only
+ loginsFound = this._filterForExactFormOriginLogins(
+ loginData.loginsFound,
+ acForm
+ );
+ // filter the list for exact matches with the username
+ // NOTE: this could be an empty string which is a valid username
+ const searchString = usernameField.value.toLowerCase();
+ loginsFound = loginsFound.filter(
+ l => l.username.toLowerCase() == searchString
+ );
+ recipes = loginData.recipes;
}
+
+ this._fillForm(acForm, loginsFound, recipes, {
+ autofillForm: true,
+ clobberPassword: true,
+ userTriggered: true,
+ });
}
/**
@@ -2399,7 +2382,7 @@ export class LoginManagerChild extends JSWindowActorChild {
};
if (fields.usernameField) {
- lazy.gFormFillService.markAsLoginManagerField(fields.usernameField);
+ this.markAsAutoCompletableField(fields.usernameField);
}
// It's possible the field triggering this message isn't one of those found by _getFormFields' heuristics
@@ -2639,7 +2622,7 @@ export class LoginManagerChild extends JSWindowActorChild {
* field is handled accordingly.
* @param {HTMLInputElement} passwordField
*/
- _filledWithGeneratedPassword(passwordField) {
+ filledWithGeneratedPassword(passwordField) {
LoginFormState._highlightFilledField(passwordField);
this._passwordEditedOrGenerated(passwordField, {
triggeredByFillingGenerated: true,
@@ -2821,7 +2804,7 @@ export class LoginManagerChild extends JSWindowActorChild {
if (scenario) {
docState.setScenario(form.rootElement, scenario);
- lazy.gFormFillService.markAsLoginManagerField(usernameField);
+ this.markAsAutoCompletableField(usernameField);
}
}
@@ -2868,7 +2851,7 @@ export class LoginManagerChild extends JSWindowActorChild {
// We would also need this attached to show the insecure login
// warning, regardless of saved login.
if (usernameField) {
- lazy.gFormFillService.markAsLoginManagerField(usernameField);
+ this.markAsAutoCompletableField(usernameField);
usernameField.addEventListener("keydown", observer);
}
@@ -3116,7 +3099,7 @@ export class LoginManagerChild extends JSWindowActorChild {
}
if (style === "generatedPassword") {
- this._filledWithGeneratedPassword(passwordField);
+ this.filledWithGeneratedPassword(passwordField);
}
lazy.log("_fillForm succeeded");
@@ -3173,4 +3156,199 @@ export class LoginManagerChild extends JSWindowActorChild {
const docState = this.stateForDocument(inputElement.ownerDocument);
return docState.getScenario(inputElement);
}
+
+ #interestedInputs = [];
+
+ markAsAutoCompletableField(input) {
+ this.#interestedInputs.push(input);
+ this.manager
+ .getActor("AutoComplete")
+ ?.markAsAutoCompletableField(input, this);
+ }
+
+ get actorName() {
+ return "LoginManager";
+ }
+
+ /**
+ * Get the search options when searching for autocomplete entries in the parent
+ *
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @returns {object} the search options for the input
+ */
+ getAutoCompleteSearchOption(input, searchString) {
+ const form = lazy.LoginFormFactory.createFromField(input);
+ const formOrigin = lazy.LoginHelper.getLoginOrigin(
+ input.ownerDocument.documentURI
+ );
+ const actionOrigin = lazy.LoginHelper.getFormActionOrigin(form);
+ const autocompleteInfo = input.getAutocompleteInfo();
+ const hasBeenTypePassword = input.hasBeenTypePassword;
+
+ let forcePasswordGeneration = false;
+ let isProbablyANewPasswordField = false;
+ if (hasBeenTypePassword) {
+ forcePasswordGeneration = this.isPasswordGenerationForcedOn(input);
+ // Run the Fathom model only if the password field does not have the
+ // autocomplete="new-password" attribute.
+ isProbablyANewPasswordField =
+ autocompleteInfo.fieldName == "new-password" ||
+ this.isProbablyANewPasswordField(input);
+ }
+
+ const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
+ ? "SignUpFormScenario"
+ : "";
+
+ const r = {
+ formOrigin,
+ actionOrigin,
+ searchString,
+ forcePasswordGeneration,
+ hasBeenTypePassword,
+ isProbablyANewPasswordField,
+ scenarioName,
+ inputMaxLength: input.maxLength,
+ isWebAuthn: this.#isWebAuthnCredentials(autocompleteInfo),
+ };
+ return r;
+ }
+
+ #searchStartTimeMS = null;
+
+ /**
+ * Ask the provider whether it might have autocomplete entry to show
+ * for the given input.
+ *
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @returns {boolean} true if we shold search for autocomplete entries
+ */
+ shouldSearchForAutoComplete(input, searchString) {
+ this.#searchStartTimeMS = Services.telemetry.msSystemNow();
+
+ // 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.
+ if (input.nodePrincipal.isNullPrincipal) {
+ return false;
+ }
+
+ // Return empty result on password fields with password already filled,
+ // unless password generation was forced.
+ if (
+ input.hasBeenTypePassword &&
+ searchString &&
+ !this.isPasswordGenerationForcedOn(input)
+ ) {
+ return false;
+ }
+
+ if (!lazy.LoginHelper.enabled) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Convert the search result to autocomplete results
+ *
+ * @param {string} searchString - The string to search for
+ * @param {HTMLInputElement} input - The input element to search for autocompelte entries
+ * @param {Array<object>} records - autocomplete records
+ * @returns {AutocompleteResult}
+ */
+ searchResultToAutoCompleteResult(searchString, input, records) {
+ if (
+ input.nodePrincipal.schemeIs("about") ||
+ input.nodePrincipal.isSystemPrincipal
+ ) {
+ // Don't show autocomplete results for about: pages.
+ return null;
+ }
+
+ let {
+ generatedPassword,
+ autocompleteItems,
+ importable,
+ logins,
+ willAutoSaveGeneratedPassword,
+ } = records ?? {};
+ logins ||= [];
+
+ const formOrigin = lazy.LoginHelper.getLoginOrigin(
+ input.ownerDocument.documentURI
+ );
+
+ const isNullPrincipal = input.nodePrincipal.isNullPrincipal;
+ const form = lazy.LoginFormFactory.createFromField(input);
+ const isSecure =
+ !isNullPrincipal && lazy.InsecurePasswordUtils.isFormSecure(form);
+
+ const telemetryEventData = {
+ acFieldName: input.getAutocompleteInfo().fieldName,
+ //hadPrevious: !!aPreviousResult,
+ hadPrevious: false,
+ typeWasPassword: input.hasBeenTypePassword,
+ fieldType: input.type,
+ searchStartTimeMS: this.#searchStartTimeMS,
+ stringLength: searchString.length,
+ };
+
+ const acResult = new lazy.LoginAutoCompleteResult(
+ searchString,
+ lazy.LoginHelper.vanillaObjectsToLogins(logins),
+ autocompleteItems,
+ formOrigin,
+ {
+ generatedPassword,
+ willAutoSaveGeneratedPassword,
+ importable,
+ actor: this,
+ isSecure,
+ hasBeenTypePassword: input.hasBeenTypePassword,
+ hostname: input.ownerDocument.documentURIObject.host,
+ telemetryEventData,
+ }
+ );
+ return acResult;
+ }
+
+ isLoginManagerField(input) {
+ return input.hasBeenTypePassword || this.#interestedInputs.includes(input);
+ }
+
+ #cachedNewPasswordScore = new WeakMap();
+
+ isProbablyANewPasswordField(inputElement) {
+ const threshold = lazy.LoginHelper.generationConfidenceThreshold;
+ if (threshold == -1) {
+ // Fathom is disabled
+ return false;
+ }
+
+ let score = this.#cachedNewPasswordScore.get(inputElement);
+ if (score) {
+ return score >= threshold;
+ }
+
+ const { rules, type } = lazy.NewPasswordModel;
+ const results = rules.against(inputElement);
+ score = results.get(inputElement).scoreFor(type);
+ this.#cachedNewPasswordScore.set(inputElement, score);
+ return score >= threshold;
+ }
+
+ /**
+ * @param {string} autocompleteInfo
+ * @returns whether the non-autofill credential type (https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#non-autofill-credential-type)
+ * of the input field is "webauthn"
+ */
+ #isWebAuthnCredentials(autocompleteInfo) {
+ return autocompleteInfo.credentialType == "webauthn";
+ }
+
+ fillFields(login) {
+ let { focusedInput } = lazy.gFormFillService;
+ this.onFieldAutoComplete(focusedInput, login);
+ }
}