mirror of
https://github.com/bitwarden/browser.git
synced 2025-03-10 13:09:37 +01:00
[PM-9342] Inline menu does not show on username field for a form that has a password field with an invalid autocomplete value (#9860)
* [PM-9342] Inline menu does not show on username field for a form that has a password field with an invalid autocomplete value * [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details * [PM-9342] Incorporating logic to handle multiple autocomplete values within a captured set of page details * [PM-9342] Changing logic for how we identify new password fields to reflect a more assertive qualification * [PM-9342] Adding feedback from code review
This commit is contained in:
parent
a613d9c268
commit
4e3fb99b9f
@ -34,10 +34,23 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("has a keyword value that indicates the field is for a create account form", () => {
|
||||
const field = mock<AutofillField>({
|
||||
type: "password",
|
||||
placeholder: "create account password",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("has a type that is an excluded type", () => {
|
||||
AutoFillConstants.ExcludedAutofillLoginTypes.forEach((excludedType) => {
|
||||
const field = mock<AutofillField>({
|
||||
type: excludedType,
|
||||
autoCompleteType: "",
|
||||
});
|
||||
|
||||
expect(
|
||||
@ -53,6 +66,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlID: index === 0 ? attribute : "",
|
||||
htmlName: index === 1 ? attribute : "",
|
||||
placeholder: index > 1 ? attribute : "",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
|
||||
expect(
|
||||
@ -67,6 +81,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlID: "not-password",
|
||||
htmlName: "not-password",
|
||||
placeholder: "not-password",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
@ -80,6 +95,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlID: "something-else",
|
||||
htmlName: "something-else",
|
||||
placeholder: "something-else",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
@ -93,6 +109,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlID: "search",
|
||||
htmlName: "something-else",
|
||||
placeholder: "something-else",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
@ -112,12 +129,14 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "user-password",
|
||||
placeholder: "user-password",
|
||||
form: "",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
const secondField = mock<AutofillField>({
|
||||
type: "password",
|
||||
htmlID: "some-other-password",
|
||||
htmlName: "some-other-password",
|
||||
placeholder: "some-other-password",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
pageDetails.fields = [field, secondField];
|
||||
|
||||
@ -133,18 +152,21 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "user-password",
|
||||
placeholder: "user-password",
|
||||
form: "",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
const usernameField = mock<AutofillField>({
|
||||
type: "text",
|
||||
htmlID: "user-username",
|
||||
htmlName: "user-username",
|
||||
placeholder: "user-username",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
const secondUsernameField = mock<AutofillField>({
|
||||
type: "text",
|
||||
htmlID: "some-other-user-username",
|
||||
htmlName: "some-other-user-username",
|
||||
placeholder: "some-other-user-username",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
pageDetails.fields = [field, usernameField, secondUsernameField];
|
||||
|
||||
@ -186,6 +208,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "user-password",
|
||||
placeholder: "user-password",
|
||||
form: "validFormId",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
const secondField = mock<AutofillField>({
|
||||
type: "password",
|
||||
@ -193,6 +216,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "some-other-password",
|
||||
placeholder: "some-other-password",
|
||||
form: "validFormId",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
pageDetails.fields = [field, secondField];
|
||||
|
||||
@ -218,12 +242,35 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("is structured on a page with a single set of username and password fields", () => {
|
||||
const field = mock<AutofillField>({
|
||||
type: "password",
|
||||
htmlID: "user-password",
|
||||
htmlName: "user-password",
|
||||
placeholder: "user-password",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
const usernameField = mock<AutofillField>({
|
||||
type: "text",
|
||||
htmlID: "user-username",
|
||||
htmlName: "user-username",
|
||||
placeholder: "user-username",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
pageDetails.fields = [field, usernameField];
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("has a type of `text` with an attribute that indicates the field is a password field", () => {
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
htmlID: null,
|
||||
htmlName: "user-password",
|
||||
placeholder: "user-password",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
|
||||
expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe(
|
||||
@ -247,6 +294,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlID: "user-username",
|
||||
htmlName: "user-username",
|
||||
placeholder: "user-username",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
pageDetails.fields = [field, usernameField];
|
||||
|
||||
@ -273,6 +321,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "user-password",
|
||||
placeholder: "user-password",
|
||||
form: "validFormId",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
const secondPasswordField = mock<AutofillField>({
|
||||
type: "password",
|
||||
@ -280,6 +329,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "some-other-password",
|
||||
placeholder: "some-other-password",
|
||||
form: "anotherFormId",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
const usernameField = mock<AutofillField>({
|
||||
type: "text",
|
||||
@ -287,6 +337,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "user-username",
|
||||
placeholder: "user-username",
|
||||
form: "validFormId",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
pageDetails.fields = [field, secondPasswordField, usernameField];
|
||||
|
||||
@ -310,6 +361,7 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
htmlName: "some-other-password",
|
||||
placeholder: "some-other-password",
|
||||
form: "anotherFormId",
|
||||
autoCompleteType: "",
|
||||
});
|
||||
pageDetails.fields = [field, secondPasswordField];
|
||||
|
||||
@ -347,21 +399,23 @@ describe("InlineMenuFieldQualificationService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
["new", "change", "neue", "ändern"].forEach((keyword) => {
|
||||
it(`has a keyword of ${keyword} that indicates a 'new or changed' username is being filled`, () => {
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
autoCompleteType: "",
|
||||
htmlID: "user-username",
|
||||
htmlName: "user-username",
|
||||
placeholder: `${keyword} username`,
|
||||
});
|
||||
["new", "change", "neue", "ändern", "register", "create", "registration"].forEach(
|
||||
(keyword) => {
|
||||
it(`has a keyword of ${keyword} that indicates a 'new or changed' username is being filled`, () => {
|
||||
const field = mock<AutofillField>({
|
||||
type: "text",
|
||||
autoCompleteType: "",
|
||||
htmlID: "user-username",
|
||||
htmlName: "user-username",
|
||||
placeholder: `${keyword} username`,
|
||||
});
|
||||
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
expect(
|
||||
inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails),
|
||||
).toBe(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("does not have a parent form element", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -14,9 +14,18 @@ export class InlineMenuFieldQualificationService
|
||||
private usernameAutocompleteValues = new Set(["username", "email"]);
|
||||
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
|
||||
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
|
||||
private currentPasswordAutocompleteValues = new Set(["current-password"]);
|
||||
private newPasswordAutoCompleteValues = new Set(["new-password"]);
|
||||
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
||||
private autocompleteDisabledValues = new Set(["off", "false"]);
|
||||
private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]);
|
||||
private accountCreationFieldKeywords = new Set([
|
||||
"register",
|
||||
"registration",
|
||||
"create",
|
||||
"confirm",
|
||||
...this.newFieldKeywords,
|
||||
]);
|
||||
private inlineMenuFieldQualificationFlagSet = false;
|
||||
|
||||
constructor() {
|
||||
@ -62,7 +71,12 @@ export class InlineMenuFieldQualificationService
|
||||
): boolean {
|
||||
// If the provided field is set with an autocomplete value of "current-password", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a password field for a login form.
|
||||
if (field.autoCompleteType === "current-password") {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.currentPasswordAutocompleteValues,
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -95,7 +109,11 @@ export class InlineMenuFieldQualificationService
|
||||
// If a single username field or less is present on the page, then we can assume that the
|
||||
// provided field is for a login form. This will only be the case if the field does not
|
||||
// explicitly have its autocomplete attribute set to "off" or "false".
|
||||
return !this.autocompleteDisabledValues.has(field.autoCompleteType);
|
||||
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
}
|
||||
|
||||
// If the field has a form parent and there are multiple visible password fields
|
||||
@ -117,7 +135,10 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
// If the field has a form parent and no username field exists and the field has an
|
||||
// autocomplete attribute set to "off" or "false", this is not a password field
|
||||
return !this.autocompleteDisabledValues.has(field.autoCompleteType);
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,7 +153,9 @@ export class InlineMenuFieldQualificationService
|
||||
): boolean {
|
||||
// If the provided field is set with an autocomplete of "username", we should assume that
|
||||
// the page developer intends for this field to be interpreted as a username field.
|
||||
if (this.usernameAutocompleteValues.has(field.autoCompleteType)) {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(field.autoCompleteType, this.usernameAutocompleteValues)
|
||||
) {
|
||||
const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField);
|
||||
return newPasswordFieldsInPageDetails.length === 0;
|
||||
}
|
||||
@ -175,7 +198,10 @@ export class InlineMenuFieldQualificationService
|
||||
// If the page does not contain any password fields, it might be part of a multistep login form.
|
||||
// That will only be the case if the field does not explicitly have its autocomplete attribute
|
||||
// set to "off" or "false".
|
||||
return !this.autocompleteDisabledValues.has(field.autoCompleteType);
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
}
|
||||
|
||||
// If the field is structured within a form, but no password fields are present in the form,
|
||||
@ -183,7 +209,12 @@ export class InlineMenuFieldQualificationService
|
||||
if (passwordFieldsInPageDetails.length === 0) {
|
||||
// If the field's autocomplete is set to a disabled value, we should assume that the field is
|
||||
// not part of a login form.
|
||||
if (this.autocompleteDisabledValues.has(field.autoCompleteType)) {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -212,7 +243,10 @@ export class InlineMenuFieldQualificationService
|
||||
|
||||
// If no visible password fields are found, this field might be part of a multipart form.
|
||||
// Check for an invalid autocompleteType to determine if the field is part of a login form.
|
||||
return !this.autocompleteDisabledValues.has(field.autoCompleteType);
|
||||
return !this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.autocompleteDisabledValues,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -237,7 +271,13 @@ export class InlineMenuFieldQualificationService
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
private isCurrentPasswordField = (field: AutofillField): boolean => {
|
||||
if (field.autoCompleteType === "new-password") {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.newPasswordAutoCompleteValues,
|
||||
) ||
|
||||
this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -250,11 +290,19 @@ export class InlineMenuFieldQualificationService
|
||||
* @param field - The field to validate
|
||||
*/
|
||||
private isNewPasswordField = (field: AutofillField): boolean => {
|
||||
if (field.autoCompleteType === "current-password") {
|
||||
if (
|
||||
this.fieldContainsAutocompleteValues(
|
||||
field.autoCompleteType,
|
||||
this.currentPasswordAutocompleteValues,
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isPasswordField(field);
|
||||
return (
|
||||
this.isPasswordField(field) &&
|
||||
this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords])
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -422,6 +470,31 @@ export class InlineMenuFieldQualificationService
|
||||
return keywordValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Separates the provided field data into space-separated values and checks if any
|
||||
* of the values are present in the provided set of autocomplete values.
|
||||
*
|
||||
* @param fieldAutocompleteValue - The field autocomplete value to validate
|
||||
* @param compareValues - The set of autocomplete values to check against
|
||||
*/
|
||||
private fieldContainsAutocompleteValues(
|
||||
fieldAutocompleteValue: string,
|
||||
compareValues: Set<string>,
|
||||
) {
|
||||
if (!fieldAutocompleteValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const autocompleteValueParts = fieldAutocompleteValue.split(" ");
|
||||
for (let index = 0; index < autocompleteValueParts.length; index++) {
|
||||
if (compareValues.has(autocompleteValueParts[index])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method represents the previous rudimentary approach to qualifying fields for login forms.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user