forked from mirrors/gecko-dev
Bug 1893623 - P1. Trigger autofill from the parent process r=credential-management-reviewers,sgalich
Currently, when users autocomplete a field for an address, credit card, or login, Firefox also "autofills" the relevant fields. Here is a quick summary of how we currently manage this process: 1. Users click on an input field, the autocomplete popup is displayed, and Firefox searches for options so users can choose which value to autocomplete. 2. AutoCompleteChild searches for the value to autocomplete based on the type of the input field, along with the entire profile. For example, when we autocomplete a cc-number field, we also send cc-name, cc-exp, etc., to the child process. 3. AutoCompleteController autocompletes the focused input. 4. AutoCompleteController notifies the corresponding module, which then autofills the remaining fields. Currently, step 4 is triggered directly in the child process. This patch moves the logic of step 4 from the child process to the parent process. This change is a prerequisite for supporting autofill across frames and will also enable us not to send the entire profile in step 2. Differential Revision: https://phabricator.services.mozilla.com/D208752
This commit is contained in:
parent
58465b2927
commit
a6fc3d1c7d
13 changed files with 224 additions and 124 deletions
|
|
@ -141,6 +141,7 @@ export class AutoCompleteChild extends JSWindowActorChild {
|
||||||
dir,
|
dir,
|
||||||
inputElementIdentifier,
|
inputElementIdentifier,
|
||||||
formOrigin,
|
formOrigin,
|
||||||
|
actorName: this.lastAutoCompleteProviderName,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._input = input;
|
this._input = input;
|
||||||
|
|
@ -308,7 +309,7 @@ export class AutoCompleteChild extends JSWindowActorChild {
|
||||||
|
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
// Search result could be empty. However, an autocomplete provider might
|
// Search result could be empty. However, an autocomplete provider might
|
||||||
// want to show an autoclmplete popup when there is no search result. For example,
|
// want to show an autocomplete popup when there is no search result. For example,
|
||||||
// <datalist> for FormHisotry, insecure warning for LoginManager.
|
// <datalist> for FormHisotry, insecure warning for LoginManager.
|
||||||
const searchResult = result.find(r => r.actorName == provider.actorName);
|
const searchResult = result.find(r => r.actorName == provider.actorName);
|
||||||
const acResult = provider.searchResultToAutoCompleteResult(
|
const acResult = provider.searchResultToAutoCompleteResult(
|
||||||
|
|
@ -320,6 +321,10 @@ export class AutoCompleteChild extends JSWindowActorChild {
|
||||||
// We have not yet supported showing autocomplete entries from multiple providers,
|
// We have not yet supported showing autocomplete entries from multiple providers,
|
||||||
// Note: The prioty is defined in AutoCompleteParent.
|
// Note: The prioty is defined in AutoCompleteParent.
|
||||||
if (acResult) {
|
if (acResult) {
|
||||||
|
// `lastAutoCompleteProviderName` should be removed once we implement
|
||||||
|
// the mapping of autocomplete entry to provider in the parent process.
|
||||||
|
this.lastAutoCompleteProviderName = provider.actorName;
|
||||||
|
|
||||||
this.lastProfileAutoCompleteResult = acResult;
|
this.lastProfileAutoCompleteResult = acResult;
|
||||||
listener.onSearchCompletion(acResult);
|
listener.onSearchCompletion(acResult);
|
||||||
return;
|
return;
|
||||||
|
|
@ -332,6 +337,12 @@ export class AutoCompleteChild extends JSWindowActorChild {
|
||||||
this.lastProfileAutoCompleteResult = null;
|
this.lastProfileAutoCompleteResult = null;
|
||||||
this.#ongoingSearches.clear();
|
this.#ongoingSearches.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectEntry() {
|
||||||
|
// we don't need to pass the selected index to the parent process because
|
||||||
|
// the selected index is maintained in the parent.
|
||||||
|
this.sendAsyncMessage("AutoComplete:SelectEntry");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoCompleteChild.prototype.QueryInterface = ChromeUtils.generateQI([
|
AutoCompleteChild.prototype.QueryInterface = ChromeUtils.generateQI([
|
||||||
|
|
|
||||||
|
|
@ -143,9 +143,13 @@ var AutoCompleteResultView = {
|
||||||
this.results = [];
|
this.results = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
setResults(actor, results) {
|
setResults(actor, results, providerActorName) {
|
||||||
this.currentActor = actor;
|
this.currentActor = actor;
|
||||||
this.results = results;
|
this.results = results;
|
||||||
|
|
||||||
|
if (providerActorName) {
|
||||||
|
this.providerActor = actor.manager.getActor(providerActorName);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -203,7 +207,7 @@ export class AutoCompleteParent extends JSWindowActorParent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showPopupWithResults({ rect, dir, results }) {
|
showPopupWithResults({ rect, dir, results, actorName }) {
|
||||||
if (!results.length || this.openedPopup) {
|
if (!results.length || this.openedPopup) {
|
||||||
// We shouldn't ever be showing an empty popup, and if we
|
// We shouldn't ever be showing an empty popup, and if we
|
||||||
// already have a popup open, the old one needs to close before
|
// already have a popup open, the old one needs to close before
|
||||||
|
|
@ -237,7 +241,8 @@ export class AutoCompleteParent extends JSWindowActorParent {
|
||||||
);
|
);
|
||||||
this.openedPopup.style.direction = dir;
|
this.openedPopup.style.direction = dir;
|
||||||
|
|
||||||
AutoCompleteResultView.setResults(this, results);
|
AutoCompleteResultView.setResults(this, results, actorName);
|
||||||
|
|
||||||
this.openedPopup.view = AutoCompleteResultView;
|
this.openedPopup.view = AutoCompleteResultView;
|
||||||
this.openedPopup.selectedIndex = -1;
|
this.openedPopup.selectedIndex = -1;
|
||||||
|
|
||||||
|
|
@ -394,6 +399,13 @@ export class AutoCompleteParent extends JSWindowActorParent {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (message.name) {
|
switch (message.name) {
|
||||||
|
case "AutoComplete:SelectEntry": {
|
||||||
|
if (this.openedPopup) {
|
||||||
|
this.autofillProfile(this.openedPopup.selectedIndex);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "AutoComplete:SetSelectedIndex": {
|
case "AutoComplete:SetSelectedIndex": {
|
||||||
let { index } = message.data;
|
let { index } = message.data;
|
||||||
if (this.openedPopup) {
|
if (this.openedPopup) {
|
||||||
|
|
@ -403,8 +415,14 @@ export class AutoCompleteParent extends JSWindowActorParent {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "AutoComplete:MaybeOpenPopup": {
|
case "AutoComplete:MaybeOpenPopup": {
|
||||||
let { results, rect, dir, inputElementIdentifier, formOrigin } =
|
let {
|
||||||
message.data;
|
results,
|
||||||
|
rect,
|
||||||
|
dir,
|
||||||
|
inputElementIdentifier,
|
||||||
|
formOrigin,
|
||||||
|
actorName,
|
||||||
|
} = message.data;
|
||||||
if (lazy.DELEGATE_AUTOCOMPLETE) {
|
if (lazy.DELEGATE_AUTOCOMPLETE) {
|
||||||
lazy.GeckoViewAutocomplete.delegateSelection({
|
lazy.GeckoViewAutocomplete.delegateSelection({
|
||||||
browsingContext: this.browsingContext,
|
browsingContext: this.browsingContext,
|
||||||
|
|
@ -413,7 +431,7 @@ export class AutoCompleteParent extends JSWindowActorParent {
|
||||||
formOrigin,
|
formOrigin,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.showPopupWithResults({ results, rect, dir });
|
this.showPopupWithResults({ results, rect, dir, actorName });
|
||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -437,7 +455,7 @@ export class AutoCompleteParent extends JSWindowActorParent {
|
||||||
case "AutoComplete:StartSearch": {
|
case "AutoComplete:StartSearch": {
|
||||||
const { searchString, data } = message.data;
|
const { searchString, data } = message.data;
|
||||||
const result = await this.#startSearch(searchString, data);
|
const result = await this.#startSearch(searchString, data);
|
||||||
return result;
|
return Promise.resolve(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Returning false to pacify ESLint, but this return value is
|
// Returning false to pacify ESLint, but this return value is
|
||||||
|
|
@ -540,6 +558,36 @@ export class AutoCompleteParent extends JSWindowActorParent {
|
||||||
|
|
||||||
stopSearch() {}
|
stopSearch() {}
|
||||||
|
|
||||||
|
previewAutofillProfile(index) {
|
||||||
|
const actor = AutoCompleteResultView.providerActor;
|
||||||
|
if (!actor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear preview when the selected index is not valid
|
||||||
|
if (index < 0) {
|
||||||
|
actor.previewFields(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = AutoCompleteResultView.results[index];
|
||||||
|
actor.previewFields(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a field is autocompleted, fill relevant fields
|
||||||
|
*/
|
||||||
|
autofillProfile(index) {
|
||||||
|
// Find the provider of this autocomplete
|
||||||
|
const actor = AutoCompleteResultView.providerActor;
|
||||||
|
if (index < 0 || !actor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = AutoCompleteResultView.results[index];
|
||||||
|
actor.autofillFields(result);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to the browser that is requesting the input
|
* Sends a message to the browser that is requesting the input
|
||||||
* that the open popup should be focused.
|
* that the open popup should be focused.
|
||||||
|
|
|
||||||
|
|
@ -1235,6 +1235,8 @@ nsresult nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
|
||||||
SetSearchStringInternal(value);
|
SetSearchStringInternal(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popup->SelectEntry();
|
||||||
|
|
||||||
obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
|
obsSvc->NotifyObservers(input, "autocomplete-did-enter-text", nullptr);
|
||||||
|
|
||||||
input->OnTextEntered(aEvent);
|
input->OnTextEntered(aEvent);
|
||||||
|
|
|
||||||
|
|
@ -92,4 +92,8 @@ interface nsIAutoCompletePopup : nsISupports
|
||||||
*/
|
*/
|
||||||
void stopSearch();
|
void stopSearch();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the autocomplete popup that an autocomplete entry is selected.
|
||||||
|
*/
|
||||||
|
void selectEntry();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -338,13 +338,9 @@ export const ProfileAutocomplete = {
|
||||||
// The observer notification is for autocomplete in a different process.
|
// The observer notification is for autocomplete in a different process.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lazy.FormAutofillContent.autofillPending = true;
|
|
||||||
Services.obs.notifyObservers(null, "autofill-fill-starting");
|
|
||||||
await this._fillFromAutocompleteRow(
|
await this._fillFromAutocompleteRow(
|
||||||
lazy.FormAutofillContent.activeInput
|
lazy.FormAutofillContent.activeInput
|
||||||
);
|
);
|
||||||
Services.obs.notifyObservers(null, "autofill-fill-complete");
|
|
||||||
lazy.FormAutofillContent.autofillPending = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -415,10 +411,7 @@ export const ProfileAutocomplete = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await lazy.FormAutofillContent.activeHandler.autofillFormFields(profile);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_clearProfilePreview() {
|
_clearProfilePreview() {
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,9 @@ const observer = {
|
||||||
* Handles content's interactions for the frame.
|
* Handles content's interactions for the frame.
|
||||||
*/
|
*/
|
||||||
export class FormAutofillChild extends JSWindowActorChild {
|
export class FormAutofillChild extends JSWindowActorChild {
|
||||||
|
// Flag indicating whether the form is waiting to be filled by Autofill.
|
||||||
|
#autofillPending = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
|
@ -109,9 +112,6 @@ export class FormAutofillChild extends JSWindowActorChild {
|
||||||
this._hasDOMContentLoadedHandler = false;
|
this._hasDOMContentLoadedHandler = false;
|
||||||
this._hasPendingTask = false;
|
this._hasPendingTask = false;
|
||||||
|
|
||||||
// Flag indicating whether the form is waiting to be filled by Autofill.
|
|
||||||
this._autofillPending = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
|
* @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
|
||||||
*/
|
*/
|
||||||
|
|
@ -478,14 +478,14 @@ export class FormAutofillChild extends JSWindowActorChild {
|
||||||
this.unregisterProgressListener();
|
this.unregisterProgressListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveMessage(message) {
|
async receiveMessage(message) {
|
||||||
if (!lazy.FormAutofill.isAutofillEnabled) {
|
if (!lazy.FormAutofill.isAutofillEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (message.name) {
|
switch (message.name) {
|
||||||
case "FormAutofill:PreviewProfile": {
|
case "FormAutofill:PreviewProfile": {
|
||||||
this.previewProfile(message.data.selectedIndex);
|
this.previewProfile(message.data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "FormAutofill:ClearForm": {
|
case "FormAutofill:ClearForm": {
|
||||||
|
|
@ -493,7 +493,7 @@ export class FormAutofillChild extends JSWindowActorChild {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "FormAutofill:FillForm": {
|
case "FormAutofill:FillForm": {
|
||||||
this.activeHandler.autofillFormFields(message.data);
|
await this.autofillFields(message.data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -615,7 +615,7 @@ export class FormAutofillChild extends JSWindowActorChild {
|
||||||
this.debug("updateActiveElement: checking for popup-on-focus");
|
this.debug("updateActiveElement: checking for popup-on-focus");
|
||||||
// We know this element just received focus. If it's a credit card field,
|
// We know this element just received focus. If it's a credit card field,
|
||||||
// open its popup.
|
// open its popup.
|
||||||
if (this._autofillPending) {
|
if (this.#autofillPending) {
|
||||||
this.debug("updateActiveElement: skipping check; autofill is imminent");
|
this.debug("updateActiveElement: skipping check; autofill is imminent");
|
||||||
} else if (element.value?.length !== 0) {
|
} else if (element.value?.length !== 0) {
|
||||||
this.debug(
|
this.debug(
|
||||||
|
|
@ -636,11 +636,6 @@ export class FormAutofillChild extends JSWindowActorChild {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set autofillPending(flag) {
|
|
||||||
this.debug("Setting autofillPending to", flag);
|
|
||||||
this._autofillPending = flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearForm() {
|
clearForm() {
|
||||||
let focusedInput =
|
let focusedInput =
|
||||||
this.activeInput ||
|
this.activeInput ||
|
||||||
|
|
@ -670,22 +665,32 @@ export class FormAutofillChild extends JSWindowActorChild {
|
||||||
?.lastProfileAutoCompleteFocusedInput;
|
?.lastProfileAutoCompleteFocusedInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
previewProfile(selectedIndex) {
|
previewProfile(profile) {
|
||||||
if (
|
if (profile && this.activeSection) {
|
||||||
selectedIndex === -1 ||
|
const adaptedProfile = this.activeSection.getAdaptedProfiles([
|
||||||
!this.activeInput ||
|
profile,
|
||||||
this.lastProfileAutoCompleteResult?.getStyleAt(selectedIndex) !=
|
])[0];
|
||||||
"autofill"
|
this.activeSection.previewFormFields(adaptedProfile);
|
||||||
) {
|
|
||||||
lazy.ProfileAutocomplete._clearProfilePreview();
|
|
||||||
} else {
|
} else {
|
||||||
lazy.ProfileAutocomplete._previewSelectedProfile(selectedIndex);
|
this.activeSection.clearPreviewedFormFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async autofillFields(profile) {
|
||||||
|
this.#autofillPending = true;
|
||||||
|
Services.obs.notifyObservers(null, "autofill-fill-starting");
|
||||||
|
try {
|
||||||
|
Services.obs.notifyObservers(null, "autofill-fill-starting");
|
||||||
|
await this.activeHandler.autofillFormFields(profile);
|
||||||
|
Services.obs.notifyObservers(null, "autofill-fill-complete");
|
||||||
|
} finally {
|
||||||
|
this.#autofillPending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPopupClosed() {
|
onPopupClosed() {
|
||||||
this.debug("Popup has closed.");
|
this.debug("Popup has closed.");
|
||||||
lazy.ProfileAutocomplete._clearProfilePreview();
|
this.activeSection?.clearPreviewedFormFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
onPopupOpened() {
|
onPopupOpened() {
|
||||||
|
|
|
||||||
|
|
@ -696,4 +696,25 @@ export class FormAutofillParent extends JSWindowActorParent {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previewFields(result) {
|
||||||
|
try {
|
||||||
|
const profile =
|
||||||
|
result.style == "autofill" ? JSON.parse(result.comment) : null;
|
||||||
|
this.sendAsyncMessage("FormAutofill:PreviewProfile", profile);
|
||||||
|
} catch (e) {
|
||||||
|
lazy.log.debug("Fail to get preview profile: ", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
autofillFields(result) {
|
||||||
|
if (result.style == "autofill") {
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(result.comment);
|
||||||
|
this.sendAsyncMessage("FormAutofill:FillForm", profile);
|
||||||
|
} catch (e) {
|
||||||
|
lazy.log.debug("Fail to get autofill profile.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -413,6 +413,7 @@ export class FormAutofillSection {
|
||||||
profile[`${fieldDetail.fieldName}-formatted`] ||
|
profile[`${fieldDetail.fieldName}-formatted`] ||
|
||||||
profile[fieldDetail.fieldName] ||
|
profile[fieldDetail.fieldName] ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
if (HTMLSelectElement.isInstance(element)) {
|
if (HTMLSelectElement.isInstance(element)) {
|
||||||
// Unlike text input, select element is always previewed even if
|
// Unlike text input, select element is always previewed even if
|
||||||
// the option is already selected.
|
// the option is already selected.
|
||||||
|
|
|
||||||
|
|
@ -159,40 +159,6 @@ const observer = {
|
||||||
loginManagerChild()._onNavigation(window.document);
|
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
|
// nsIDOMEventListener
|
||||||
handleEvent(aEvent) {
|
handleEvent(aEvent) {
|
||||||
if (!aEvent.isTrusted) {
|
if (!aEvent.isTrusted) {
|
||||||
|
|
@ -437,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.
|
* Form scenario defines what can be done with form.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1529,6 +1492,16 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
this.notifyObserversOfFormProcessed(msg.data.formid);
|
this.notifyObserversOfFormProcessed(msg.data.formid);
|
||||||
break;
|
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;
|
return undefined;
|
||||||
|
|
@ -2178,7 +2151,7 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
/**
|
/**
|
||||||
* A username or password was autocompleted into a field.
|
* A username or password was autocompleted into a field.
|
||||||
*/
|
*/
|
||||||
onFieldAutoComplete(acInputField, loginGUID) {
|
onFieldAutoComplete(acInputField, login) {
|
||||||
if (!lazy.LoginHelper.enabled) {
|
if (!lazy.LoginHelper.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2193,7 +2166,7 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lazy.LoginHelper.isUsernameFieldType(acInputField)) {
|
if (lazy.LoginHelper.isUsernameFieldType(acInputField)) {
|
||||||
this.onUsernameAutocompleted(acInputField, loginGUID);
|
this.onUsernameAutocompleted(acInputField, [login]);
|
||||||
} else if (acInputField.hasBeenTypePassword) {
|
} else if (acInputField.hasBeenTypePassword) {
|
||||||
// Ensure the field gets re-masked and edits don't overwrite the generated
|
// Ensure the field gets re-masked and edits don't overwrite the generated
|
||||||
// password in case a generated password was filled into it previously.
|
// password in case a generated password was filled into it previously.
|
||||||
|
|
@ -2207,7 +2180,7 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
* A username field was filled or tabbed away from so try fill in the
|
* A username field was filled or tabbed away from so try fill in the
|
||||||
* associated password in the password field.
|
* associated password in the password field.
|
||||||
*/
|
*/
|
||||||
onUsernameAutocompleted(acInputField, loginGUID = null) {
|
async onUsernameAutocompleted(acInputField, loginsFound = null) {
|
||||||
lazy.log(`Autocompleting input field with name: ${acInputField.name}`);
|
lazy.log(`Autocompleting input field with name: ${acInputField.name}`);
|
||||||
|
|
||||||
let acForm = lazy.LoginFormFactory.createFromField(acInputField);
|
let acForm = lazy.LoginFormFactory.createFromField(acInputField);
|
||||||
|
|
@ -2223,43 +2196,49 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
const docState = this.stateForDocument(acInputField.ownerDocument);
|
const docState = this.stateForDocument(acInputField.ownerDocument);
|
||||||
let { usernameField, newPasswordField: passwordField } =
|
let { usernameField, newPasswordField: passwordField } =
|
||||||
docState._getFormFields(acForm, false, recipes);
|
docState._getFormFields(acForm, false, recipes);
|
||||||
if (usernameField == acInputField) {
|
// Ignore the event, it's for some input we don't care about.
|
||||||
// Fill the form when a password field is present.
|
if (usernameField != acInputField) {
|
||||||
if (passwordField) {
|
return;
|
||||||
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, {
|
if (!passwordField) {
|
||||||
autofillForm: true,
|
// Use `loginsFound !== null` to distinguish whether this is called when the
|
||||||
clobberPassword: true,
|
// field is filled or tabbed away from. For the latter, don't highlight the field.
|
||||||
userTriggered: true,
|
if (loginsFound !== null) {
|
||||||
});
|
|
||||||
})
|
|
||||||
.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) {
|
|
||||||
LoginFormState._highlightFilledField(usernameField);
|
LoginFormState._highlightFilledField(usernameField);
|
||||||
}
|
}
|
||||||
} else {
|
return;
|
||||||
// Ignore the event, it's for some input we don't care about.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2643,7 +2622,7 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
* field is handled accordingly.
|
* field is handled accordingly.
|
||||||
* @param {HTMLInputElement} passwordField
|
* @param {HTMLInputElement} passwordField
|
||||||
*/
|
*/
|
||||||
_filledWithGeneratedPassword(passwordField) {
|
filledWithGeneratedPassword(passwordField) {
|
||||||
LoginFormState._highlightFilledField(passwordField);
|
LoginFormState._highlightFilledField(passwordField);
|
||||||
this._passwordEditedOrGenerated(passwordField, {
|
this._passwordEditedOrGenerated(passwordField, {
|
||||||
triggeredByFillingGenerated: true,
|
triggeredByFillingGenerated: true,
|
||||||
|
|
@ -3120,7 +3099,7 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style === "generatedPassword") {
|
if (style === "generatedPassword") {
|
||||||
this._filledWithGeneratedPassword(passwordField);
|
this.filledWithGeneratedPassword(passwordField);
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy.log("_fillForm succeeded");
|
lazy.log("_fillForm succeeded");
|
||||||
|
|
@ -3367,4 +3346,9 @@ export class LoginManagerChild extends JSWindowActorChild {
|
||||||
#isWebAuthnCredentials(autocompleteInfo) {
|
#isWebAuthnCredentials(autocompleteInfo) {
|
||||||
return autocompleteInfo.credentialType == "webauthn";
|
return autocompleteInfo.credentialType == "webauthn";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillFields(login) {
|
||||||
|
let { focusedInput } = lazy.gFormFillService;
|
||||||
|
this.onFieldAutoComplete(focusedInput, login);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1549,6 +1549,23 @@ export class LoginManagerParent extends JSWindowActorParent {
|
||||||
async searchAutoCompleteEntries(searchString, data) {
|
async searchAutoCompleteEntries(searchString, data) {
|
||||||
return this.doAutocompleteSearch(data.formOrigin, data);
|
return this.doAutocompleteSearch(data.formOrigin, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previewFields(_result) {
|
||||||
|
// Logins do not show previews
|
||||||
|
}
|
||||||
|
|
||||||
|
autofillFields(result) {
|
||||||
|
if (result.style == "login" || result.style == "loginWithOrigin") {
|
||||||
|
try {
|
||||||
|
const profile = JSON.parse(result.comment);
|
||||||
|
this.sendAsyncMessage("PasswordManager:fillFields", profile.login);
|
||||||
|
} catch (e) {
|
||||||
|
lazy.log("Fail to get autofill profile: ", e.message);
|
||||||
|
}
|
||||||
|
} else if (result.style == "generatedPassword") {
|
||||||
|
this.sendAsyncMessage("PasswordManager:fillGeneratedPassword");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LoginManagerParent.SUGGEST_IMPORT_DEBOUNCE_MS = 10000;
|
LoginManagerParent.SUGGEST_IMPORT_DEBOUNCE_MS = 10000;
|
||||||
|
|
|
||||||
|
|
@ -146,21 +146,29 @@ Login Manager test: filling generated passwords into confirm password fields
|
||||||
"resetLoginsAndGeneratedPasswords", () => {
|
"resetLoginsAndGeneratedPasswords", () => {
|
||||||
LoginTestUtils.clearData();
|
LoginTestUtils.clearData();
|
||||||
LoginTestUtils.resetGeneratedPasswordsCache();
|
LoginTestUtils.resetGeneratedPasswordsCache();
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
function resetLoginsAndGeneratedPasswords() {
|
function resetLoginsAndGeneratedPasswords() {
|
||||||
return setupScript.sendAsyncMessage("resetLoginsAndGeneratedPasswords");
|
return setupScript.sendQuery("resetLoginsAndGeneratedPasswords");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function triggerPasswordGeneration(form) {
|
async function triggerPasswordGeneration(form) {
|
||||||
await openPopupOn(form.pword);
|
await openPopupOn(form.pword);
|
||||||
synthesizeKey("KEY_ArrowDown");
|
synthesizeKey("KEY_ArrowDown");
|
||||||
synthesizeKey("KEY_Enter");
|
synthesizeKey("KEY_Enter");
|
||||||
|
|
||||||
|
const storageAddPromise = promiseStorageChanged(["addLogin"]);
|
||||||
await SimpleTest.promiseWaitForCondition(() => !!form.pword.value, "Wait for generated password to get filled");
|
await SimpleTest.promiseWaitForCondition(() => !!form.pword.value, "Wait for generated password to get filled");
|
||||||
|
await storageAddPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
add_setup(async () => {
|
||||||
|
SpecialPowers.pushPrefEnv({"set": [["signon.webauthn.autocomplete", false]]});
|
||||||
|
})
|
||||||
|
|
||||||
add_named_task("autocomplete menu contains option to generate password", async () => {
|
add_named_task("autocomplete menu contains option to generate password", async () => {
|
||||||
await resetLoginsAndGeneratedPasswords();
|
await resetLoginsAndGeneratedPasswords();
|
||||||
const form = setContentForTask(formTemplates.form1);
|
const form = setContentForTask(formTemplates.form1);
|
||||||
|
|
@ -206,7 +214,7 @@ Login Manager test: filling generated passwords into confirm password fields
|
||||||
await resetLoginsAndGeneratedPasswords();
|
await resetLoginsAndGeneratedPasswords();
|
||||||
const form = setContentForTask(formTemplates.form1);
|
const form = setContentForTask(formTemplates.form1);
|
||||||
await triggerPasswordGeneration(form);
|
await triggerPasswordGeneration(form);
|
||||||
is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pword.value == form.pwordNext.value, "Value of the confirm field has been filled with generated password");
|
||||||
});
|
});
|
||||||
|
|
||||||
add_named_task("password field is not masked initially after password generation", async () => {
|
add_named_task("password field is not masked initially after password generation", async () => {
|
||||||
|
|
@ -270,7 +278,7 @@ Login Manager test: filling generated passwords into confirm password fields
|
||||||
form.pword.blur();
|
form.pword.blur();
|
||||||
await messageSentPromise;
|
await messageSentPromise;
|
||||||
|
|
||||||
is(form.pwordNext.value, generatedPassword, "Value of the confirm field still holds the original generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == generatedPassword, "Value of the confirm field still holds the original generated password");
|
||||||
ok(form.pwordNext.matches(":autofill"), "Highlight is still applied to password confirmation field");
|
ok(form.pwordNext.matches(":autofill"), "Highlight is still applied to password confirmation field");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -314,21 +322,21 @@ Login Manager test: filling generated passwords into confirm password fields
|
||||||
form.pwordNext.focus()
|
form.pwordNext.focus()
|
||||||
sendString("edited value");
|
sendString("edited value");
|
||||||
await triggerPasswordGeneration(form);
|
await triggerPasswordGeneration(form);
|
||||||
is(form.pwordNext.value, "edited value", "Value of the confirm field has been filled with generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == "edited value", "Value of the confirm field has been filled with generated password");
|
||||||
});
|
});
|
||||||
|
|
||||||
add_named_task("password confirmation does not get filled with the generated password if its readonly", async () => {
|
add_named_task("password confirmation does not get filled with the generated password if its readonly", async () => {
|
||||||
await resetLoginsAndGeneratedPasswords();
|
await resetLoginsAndGeneratedPasswords();
|
||||||
const form = setContentForTask(formTemplates.form3);
|
const form = setContentForTask(formTemplates.form3);
|
||||||
await triggerPasswordGeneration(form);
|
await triggerPasswordGeneration(form);
|
||||||
is(form.pwordNext.value, "", "Value of the confirm field has been filled with generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == "", "Value of the confirm field has been filled with generated password");
|
||||||
});
|
});
|
||||||
|
|
||||||
add_named_task("password confirmation does not get filled with the generated password if its disabled", async () => {
|
add_named_task("password confirmation does not get filled with the generated password if its disabled", async () => {
|
||||||
await resetLoginsAndGeneratedPasswords();
|
await resetLoginsAndGeneratedPasswords();
|
||||||
const form = setContentForTask(formTemplates.form4);
|
const form = setContentForTask(formTemplates.form4);
|
||||||
await triggerPasswordGeneration(form);
|
await triggerPasswordGeneration(form);
|
||||||
is(form.pwordNext.value, "", "Value of the confirm field has been filled with generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == "", "Value of the confirm field has been filled with generated password");
|
||||||
});
|
});
|
||||||
|
|
||||||
add_named_task("password confirmation matching autocomplete info gets filled with the generated password", async () => {
|
add_named_task("password confirmation matching autocomplete info gets filled with the generated password", async () => {
|
||||||
|
|
@ -336,14 +344,14 @@ Login Manager test: filling generated passwords into confirm password fields
|
||||||
const form = setContentForTask(formTemplates.form5);
|
const form = setContentForTask(formTemplates.form5);
|
||||||
await triggerPasswordGeneration(form);
|
await triggerPasswordGeneration(form);
|
||||||
is(form.pwordBetween.value, "", "Value of the between field has not been filled");
|
is(form.pwordBetween.value, "", "Value of the between field has not been filled");
|
||||||
is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == form.pword.value, "Value of the confirm field has been filled with generated password");
|
||||||
});
|
});
|
||||||
|
|
||||||
add_named_task("password confirmation matching autocomplete info gets ignored if its disabled, even if has autocomplete info", async () => {
|
add_named_task("password confirmation matching autocomplete info gets ignored if its disabled, even if has autocomplete info", async () => {
|
||||||
await resetLoginsAndGeneratedPasswords();
|
await resetLoginsAndGeneratedPasswords();
|
||||||
const form = setContentForTask(formTemplates.form6);
|
const form = setContentForTask(formTemplates.form6);
|
||||||
await triggerPasswordGeneration(form);
|
await triggerPasswordGeneration(form);
|
||||||
is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == form.pword.value, "Value of the confirm field has been filled with generated password");
|
||||||
is(form.pwordAfter.value, "", "Value of the disabled confirmation field has not been filled");
|
is(form.pwordAfter.value, "", "Value of the disabled confirmation field has not been filled");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -365,7 +373,7 @@ Login Manager test: filling generated passwords into confirm password fields
|
||||||
await resetLoginsAndGeneratedPasswords();
|
await resetLoginsAndGeneratedPasswords();
|
||||||
const form = setContentForTask(formTemplates.form9);
|
const form = setContentForTask(formTemplates.form9);
|
||||||
await triggerPasswordGeneration(form);
|
await triggerPasswordGeneration(form);
|
||||||
is(form.pwordNext.value, form.pword.value, "Value of the confirm field has been filled with generated password");
|
await SimpleTest.promiseWaitForCondition(() => form.pwordNext.value == form.pword.value, "Value of the confirm field has been filled with generated password");
|
||||||
});
|
});
|
||||||
|
|
||||||
add_named_task("do not fill third password field after the confirm-password field", async () => {
|
add_named_task("do not fill third password field after the confirm-password field", async () => {
|
||||||
|
|
|
||||||
|
|
@ -205,4 +205,12 @@ export class FormHistoryParent extends JSWindowActorParent {
|
||||||
}
|
}
|
||||||
entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
|
entry.totalScore = Math.round(entry.frecency * Math.max(1, boundaryCalc));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previewFields(_result) {
|
||||||
|
// Not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
autofillFields(_result) {
|
||||||
|
// Not implemented
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -832,9 +832,7 @@
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
let selectedIndex = popup ? popup.selectedIndex : -1;
|
let selectedIndex = popup ? popup.selectedIndex : -1;
|
||||||
actor.manager
|
actor.previewAutofillProfile(selectedIndex);
|
||||||
.getActor("FormAutofill")
|
|
||||||
.sendAsyncMessage("FormAutofill:PreviewProfile", { selectedIndex });
|
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue