From 077dbf3832a63258ffc41e6a080de893cbb5b7bf Mon Sep 17 00:00:00 2001 From: Eugen Sawin Date: Mon, 25 May 2020 18:45:25 +0000 Subject: [PATCH] Bug 1618058 - [2.c.7] Implement Autocomplete API backend. r=geckoview-reviewers,MattN,aklotz Differential Revision: https://phabricator.services.mozilla.com/D73753 --- mobile/android/app/geckoview-prefs.js | 5 + .../geckoview/GeckoViewAutocomplete.jsm | 180 ++++++++++++++---- toolkit/actors/AutoCompleteChild.jsm | 18 ++ toolkit/actors/AutoCompleteParent.jsm | 40 +++- .../passwordmgr/LoginAutoComplete.jsm | 8 +- .../passwordmgr/LoginManagerChild.jsm | 29 ++- .../passwordmgr/LoginManagerParent.jsm | 11 +- toolkit/components/passwordmgr/moz.build | 2 +- .../satchel/nsFormFillController.cpp | 2 - 9 files changed, 242 insertions(+), 53 deletions(-) diff --git a/mobile/android/app/geckoview-prefs.js b/mobile/android/app/geckoview-prefs.js index c6bb2e609619..fae72dc29cba 100644 --- a/mobile/android/app/geckoview-prefs.js +++ b/mobile/android/app/geckoview-prefs.js @@ -82,3 +82,8 @@ pref("media.eme.require-app-approval", true); // Enable the Process Priority Manager pref("dom.ipc.processPriorityManager.enabled", true); + +pref("signon.debug", false); +pref("signon.showAutoCompleteFooter", true); +pref("security.insecure_field_warning.contextual.enabled", true); +pref("toolkit.autocomplete.delegate", true); diff --git a/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm b/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm index edf609cfb894..f3483726fb82 100644 --- a/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm +++ b/mobile/android/modules/geckoview/GeckoViewAutocomplete.jsm @@ -7,7 +7,6 @@ const EXPORTED_SYMBOLS = [ "GeckoViewAutocomplete", "LoginEntry", - "SelectLabel", "SelectOption", ]; @@ -21,7 +20,7 @@ const { GeckoViewUtils } = ChromeUtils.import( XPCOMUtils.defineLazyModuleGetters(this, { EventDispatcher: "resource://gre/modules/Messaging.jsm", - PromptDelegate: "resource://gre/modules/GeckoViewPrompt.jsm", + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.jsm", }); XPCOMUtils.defineLazyGetter(this, "LoginInfo", () => @@ -33,11 +32,20 @@ XPCOMUtils.defineLazyGetter(this, "LoginInfo", () => ); class LoginEntry { - constructor({ origin, formActionOrigin, httpRealm, username, password, - guid, timeCreated, timeLastUsed, timePasswordChanged, - timesUsed }) { + constructor({ + origin, + formActionOrigin, + httpRealm, + username, + password, + guid, + timeCreated, + timeLastUsed, + timePasswordChanged, + timesUsed, + }) { this.origin = origin ?? null; - this.formActionOrigin = formActionOrigin ?? null; + this.formActionOrigin = formActionOrigin ?? null; this.httpRealm = httpRealm ?? null; this.username = username ?? null; this.password = password ?? null; @@ -98,36 +106,19 @@ class LoginEntry { } } -class SelectLabel { - // Sync with Autocomplete.SelectOption.Label.Type in Autocomplete.java. - static Type = { - NONE: 0, - PRIMARY: 1, - SECONDARY: 2 - }; - - constructor({ type, value }) { - this.type = Object.values(SelectLabel.Type).includes(type) - ? type - : SelectLabel.Type.NONE; - this.value = value ?? null; - } -} - class SelectOption { // Sync with Autocomplete.LoginSelectOption.Hint in Autocomplete.java. static Hint = { NONE: 0, - GENERATED: (1 << 0), - INSECURE_FORM: (1 << 1), + GENERATED: 1 << 0, + INSECURE_FORM: 1 << 1, + DUPLICATE_USERNAME: 1 << 2, + MATCHING_ORIGIN: 1 << 3, }; - constructor({ value, hint, labels }) { + constructor({ value, hint }) { this.value = value ?? null; - this.hint = Object.values(SelectOption.Hint).includes(hint) - ? hint - : SelectOption.Hint.NONE; - this.labels = labels ?? []; + this.hint = hint ?? SelectOption.Hint.NONE; } } @@ -190,32 +181,155 @@ const GeckoViewAutocomplete = { }); }, + _numActiveOnLoginSelect: 0, + /** + * Delegates login entry selection. + * Call this when there are multiple login entry option for a form to delegate + * the selection. + * + * @param aBrowser The browser instance the triggered the selection. + * @param aOptions The list of {SelectOption} depicting viable options. + */ onLoginSelect(aBrowser, aOptions) { debug`onLoginSelect ${aOptions}`; return new Promise((resolve, reject) => { if (!aBrowser || !aOptions) { + debug`onLoginSelect Rejecting - no browser or options provided`; reject(); + return; } - const prompt = new PromptDelegate(aBrowser.ownerGlobal); + const prompt = new GeckoViewPrompter(aBrowser.ownerGlobal); prompt.asyncShowPrompt( { type: "Autocomplete:Select:Login", options: aOptions, }, result => { - if (!result || result.value === undefined) { + if (!result || !result.selection) { reject(); return; } - const loginInfo = LoginEntry.parse(result.value).toLoginInfo(); - resolve(loginInfo); + const option = new SelectOption({ + value: LoginEntry.parse(result.selection.value), + hint: result.selection.hint, + }); + resolve(option); } ); }); }, + + async delegateSelection({ + browsingContext, + options, + inputElementIdentifier, + formOrigin, + }) { + debug`delegateSelection ${options}`; + + if (!options.length) { + return; + } + + let insecureHint = SelectOption.Hint.NONE; + let loginStyle = null; + + const selectOptions = []; + + for (const option of options) { + switch (option.style) { + case "insecureWarning": { + // We depend on the insecure warning to be the first option. + insecureHint = SelectOption.Hint.INSECURE_FORM; + break; + } + case "generatedPassword": { + const comment = JSON.parse(option.comment); + selectOptions.push( + new SelectOption({ + value: new LoginEntry({ + password: comment.generatedPassword, + }), + hint: SelectOption.Hint.GENERATED | insecureHint, + }) + ); + break; + } + case "login": + // Fallthrough. + case "loginWithOrigin": { + loginStyle = option.style; + const comment = JSON.parse(option.comment); + + let hint = SelectOption.Hint.NONE | insecureHint; + if (comment.isDuplicateUsername) { + hint |= SelectOption.Hint.DUPLICATE_USERNAME; + } + if (comment.isOriginMatched) { + hint |= SelectOption.Hint.MATCHING_ORIGIN; + } + + selectOptions.push( + new SelectOption({ + value: LoginEntry.parse(comment.login), + hint, + }) + ); + break; + } + } + } + + if (selectOptions.length < 1) { + debug`Abort delegateSelection - no valid options provided`; + return; + } + + if (this._numActiveOnLoginSelect > 0) { + debug`Abort delegateSelection - there is already one delegation active`; + return; + } + + ++this._numActiveOnLoginSelect; + + const browser = browsingContext.top.embedderElement; + const selectedOption = await this.onLoginSelect( + browser, + selectOptions + ).catch(_ => { + debug`No GV delegate attached`; + }); + + --this._numActiveOnLoginSelect; + + debug`delegateSelection selected option: ${selectedOption}`; + const selectedLogin = selectedOption?.value?.toLoginInfo(); + + if (!selectedLogin) { + debug`Abort delegateSelection - no login entry selected`; + return; + } + + debug`delegateSelection - filling form`; + + const actor = browsingContext.currentWindowGlobal.getActor("LoginManager"); + + await actor.fillForm({ + browser, + inputElementIdentifier, + loginFormOrigin: formOrigin, + login: selectedLogin, + style: + selectedOption.hint & SelectOption.Hint.GENERATED + ? "generatedPassword" + : loginStyle, + }); + + debug`delegateSelection - form filled`; + }, }; const { debug } = GeckoViewUtils.initLogging("GeckoViewAutocomplete"); diff --git a/toolkit/actors/AutoCompleteChild.jsm b/toolkit/actors/AutoCompleteChild.jsm index 5d36a1accee4..113be8786851 100644 --- a/toolkit/actors/AutoCompleteChild.jsm +++ b/toolkit/actors/AutoCompleteChild.jsm @@ -18,6 +18,18 @@ ChromeUtils.defineModuleGetter( "resource://gre/modules/BrowserUtils.jsm" ); +ChromeUtils.defineModuleGetter( + this, + "ContentDOMReference", + "resource://gre/modules/ContentDOMReference.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "LoginHelper", + "resource://gre/modules/LoginHelper.jsm" +); + XPCOMUtils.defineLazyServiceGetter( this, "formFill", @@ -211,11 +223,17 @@ class AutoCompleteChild extends JSWindowActorChild { let window = element.ownerGlobal; let dir = window.getComputedStyle(element).direction; let results = this.getResultsFromController(input); + let formOrigin = LoginHelper.getLoginOrigin( + element.ownerDocument.documentURI + ); + let inputElementIdentifier = ContentDOMReference.get(element); this.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", { results, rect, dir, + inputElementIdentifier, + formOrigin, }); this._input = input; diff --git a/toolkit/actors/AutoCompleteParent.jsm b/toolkit/actors/AutoCompleteParent.jsm index e9c655b2a750..37c0ab2210ca 100644 --- a/toolkit/actors/AutoCompleteParent.jsm +++ b/toolkit/actors/AutoCompleteParent.jsm @@ -8,6 +8,21 @@ var EXPORTED_SYMBOLS = ["AutoCompleteParent"]; const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.jsm", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + this, + "DELEGATE_AUTOCOMPLETE", + "toolkit.autocomplete.delegate", + false +); + // Stores the browser and actor that has the active popup, used by formfill let currentBrowserWeakRef = null; let currentActor = null; @@ -358,7 +373,8 @@ class AutoCompleteParent extends JSWindowActorParent { receiveMessage(message) { let browser = this.browsingContext.top.embedderElement; - if (!browser || !browser.autoCompletePopup) { + + if (!browser || (!DELEGATE_AUTOCOMPLETE && !browser.autoCompletePopup)) { // If there is no browser or popup, just make sure that the popup has been closed. if (this.openedPopup) { this.openedPopup.closePopup(); @@ -379,14 +395,24 @@ class AutoCompleteParent extends JSWindowActorParent { } case "FormAutoComplete:MaybeOpenPopup": { - let { results, rect, dir } = message.data; - this.showPopupWithResults({ + let { + results, rect, dir, - results, - }); - - this.notifyListeners(); + inputElementIdentifier, + formOrigin, + } = message.data; + if (DELEGATE_AUTOCOMPLETE) { + GeckoViewAutocomplete.delegateSelection({ + browsingContext: this.browsingContext, + options: results, + inputElementIdentifier, + formOrigin, + }); + } else { + this.showPopupWithResults({ results, rect, dir }); + this.notifyListeners(); + } break; } diff --git a/toolkit/components/passwordmgr/LoginAutoComplete.jsm b/toolkit/components/passwordmgr/LoginAutoComplete.jsm index 6f48f9a270d5..899615347aa0 100644 --- a/toolkit/components/passwordmgr/LoginAutoComplete.jsm +++ b/toolkit/components/passwordmgr/LoginAutoComplete.jsm @@ -168,10 +168,13 @@ class LoginAutocompleteItem extends AutocompleteItem { this._login = login.QueryInterface(Ci.nsILoginMetaInfo); this._actor = actor; + this._isDuplicateUsername = + login.username && duplicateUsernames.has(login.username); + XPCOMUtils.defineLazyGetter(this, "label", () => { let username = login.username; // If login is empty or duplicated we want to append a modification date to it. - if (!username || duplicateUsernames.has(username)) { + if (!username || this._isDuplicateUsername) { if (!username) { username = getLocalizedString("noUsername"); } @@ -190,6 +193,9 @@ class LoginAutocompleteItem extends AutocompleteItem { XPCOMUtils.defineLazyGetter(this, "comment", () => { return JSON.stringify({ guid: login.guid, + login, + isDuplicateUsername: this._isDuplicateUsername, + isOriginMatched, comment: isOriginMatched && login.httpRealm === null ? getLocalizedString("displaySameOrigin") diff --git a/toolkit/components/passwordmgr/LoginManagerChild.jsm b/toolkit/components/passwordmgr/LoginManagerChild.jsm index 944ff01da08a..92ab338b7247 100644 --- a/toolkit/components/passwordmgr/LoginManagerChild.jsm +++ b/toolkit/components/passwordmgr/LoginManagerChild.jsm @@ -193,13 +193,7 @@ const observer = { ); loginManagerChild.onFieldAutoComplete(focusedInput, details.guid); } else if (style == "generatedPassword") { - loginManagerChild._highlightFilledField(focusedInput); - loginManagerChild._passwordEditedOrGenerated(focusedInput, { - triggeredByFillingGenerated: true, - }); - loginManagerChild._fillConfirmFieldWithGeneratedPassword( - focusedInput - ); + loginManagerChild._filledWithGeneratedPassword(focusedInput); } break; } @@ -518,6 +512,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild { recipes: msg.data.recipes, inputElementIdentifier: msg.data.inputElementIdentifier, originMatches: msg.data.originMatches, + style: msg.data.style, }); break; } @@ -951,6 +946,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild { recipes, inputElementIdentifier, originMatches, + style, }) { if (!inputElementIdentifier) { log("fillForm: No input element specified"); @@ -993,6 +989,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild { clobberUsername, clobberPassword: true, userTriggered: true, + style, }); } @@ -1734,6 +1731,19 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild { this._togglePasswordFieldMasking(passwordField, false); } + /** + * The password field has been filled with a generated password, ensure the + * field is handled accordingly. + * @param {HTMLInputElement} passwordField + */ + _filledWithGeneratedPassword(passwordField) { + this._highlightFilledField(passwordField); + this._passwordEditedOrGenerated(passwordField, { + triggeredByFillingGenerated: true, + }); + this._fillConfirmFieldWithGeneratedPassword(passwordField); + } + /** * Notify the parent that a generated password was filled into a field or * edited so that it can potentially be saved. @@ -1977,6 +1987,7 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild { clobberUsername = false, clobberPassword = false, userTriggered = false, + style = null, } = {} ) { if (ChromeUtils.getClassName(form) === "HTMLFormElement") { @@ -2289,6 +2300,10 @@ this.LoginManagerChild = class LoginManagerChild extends JSWindowActorChild { this._highlightFilledField(passwordField); + if (style && style === "generatedPassword") { + this._filledWithGeneratedPassword(passwordField); + } + log("_fillForm succeeded"); autofillResult = AUTOFILL_RESULT.FILLED; } catch (ex) { diff --git a/toolkit/components/passwordmgr/LoginManagerParent.jsm b/toolkit/components/passwordmgr/LoginManagerParent.jsm index fdb9264fafc2..ab4b36b6161a 100644 --- a/toolkit/components/passwordmgr/LoginManagerParent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm @@ -312,7 +312,13 @@ class LoginManagerParent extends JSWindowActorParent { * Trigger a login form fill and send relevant data (e.g. logins and recipes) * to the child process (LoginManagerChild). */ - async fillForm({ browser, loginFormOrigin, login, inputElementIdentifier }) { + async fillForm({ + browser, + loginFormOrigin, + login, + inputElementIdentifier, + style, + }) { let recipes = []; if (loginFormOrigin) { let formHost; @@ -339,6 +345,7 @@ class LoginManagerParent extends JSWindowActorParent { originMatches, logins: jsLogins, recipes, + style, }); } @@ -517,7 +524,7 @@ class LoginManagerParent extends JSWindowActorParent { Services.logins.getLoginSavingEnabled(formOrigin))) ) { generatedPassword = this.getGeneratedPassword(); - let potentialConflictingLogins = LoginHelper.searchLoginsWithObject({ + let potentialConflictingLogins = await Services.logins.searchLoginsAsync({ origin: formOrigin, formActionOrigin: actionOrigin, httpRealm: null, diff --git a/toolkit/components/passwordmgr/moz.build b/toolkit/components/passwordmgr/moz.build index dce51e642c6b..215f8217de10 100644 --- a/toolkit/components/passwordmgr/moz.build +++ b/toolkit/components/passwordmgr/moz.build @@ -43,6 +43,7 @@ EXTRA_JS_MODULES += [ 'LoginRecipes.jsm', 'NewPasswordModel.jsm', 'OSCrypto.jsm', + 'PasswordGenerator.jsm', 'storage-json.js', ] @@ -54,7 +55,6 @@ else: EXTRA_JS_MODULES += [ 'LoginImport.jsm', 'LoginStore.jsm', - 'PasswordGenerator.jsm', ] if CONFIG['OS_TARGET'] == 'WINNT': diff --git a/toolkit/components/satchel/nsFormFillController.cpp b/toolkit/components/satchel/nsFormFillController.cpp index f4da6976836a..0dd0be8315e6 100644 --- a/toolkit/components/satchel/nsFormFillController.cpp +++ b/toolkit/components/satchel/nsFormFillController.cpp @@ -966,7 +966,6 @@ nsresult nsFormFillController::HandleFocus(HTMLInputElement* aInput) { return NS_OK; } -#ifndef ANDROID // If this focus doesn't follow a right click within our specified // threshold then show the autocomplete popup for all password fields. // This is done to avoid showing both the context menu and the popup @@ -992,7 +991,6 @@ nsresult nsFormFillController::HandleFocus(HTMLInputElement* aInput) { mPasswordPopupAutomaticallyOpened = true; ShowPopup(); } -#endif return NS_OK; }