diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:35:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:35:29 +0000 |
commit | 59203c63bb777a3bacec32fb8830fba33540e809 (patch) | |
tree | 58298e711c0ff0575818c30485b44a2f21bf28a0 /toolkit/components/passwordmgr/LoginManagerChild.sys.mjs | |
parent | Adding upstream version 126.0.1. (diff) | |
download | firefox-59203c63bb777a3bacec32fb8830fba33540e809.tar.xz firefox-59203c63bb777a3bacec32fb8830fba33540e809.zip |
Adding upstream version 127.0.upstream/127.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/passwordmgr/LoginManagerChild.sys.mjs')
-rw-r--r-- | toolkit/components/passwordmgr/LoginManagerChild.sys.mjs | 338 |
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); + } } |