[PM-8027] Working through logic heuristics that will help us determine login form fields
This commit is contained in:
parent
d1b6b40b5d
commit
7786e6c567
|
@ -1,5 +1,6 @@
|
||||||
import AutofillPageDetails from "../models/autofill-page-details";
|
import AutofillPageDetails from "../models/autofill-page-details";
|
||||||
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
|
import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service";
|
||||||
|
import { AutofillFieldQualificationService } from "../services/autofill-field-qualification.service";
|
||||||
import CollectAutofillContentService from "../services/collect-autofill-content.service";
|
import CollectAutofillContentService from "../services/collect-autofill-content.service";
|
||||||
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
import DomElementVisibilityService from "../services/dom-element-visibility.service";
|
||||||
import InsertAutofillContentService from "../services/insert-autofill-content.service";
|
import InsertAutofillContentService from "../services/insert-autofill-content.service";
|
||||||
|
@ -12,6 +13,7 @@ import {
|
||||||
} from "./abstractions/autofill-init";
|
} from "./abstractions/autofill-init";
|
||||||
|
|
||||||
class AutofillInit implements AutofillInitInterface {
|
class AutofillInit implements AutofillInitInterface {
|
||||||
|
private readonly autofillFieldQualificationService: AutofillFieldQualificationService;
|
||||||
private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined;
|
private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined;
|
||||||
private readonly domElementVisibilityService: DomElementVisibilityService;
|
private readonly domElementVisibilityService: DomElementVisibilityService;
|
||||||
private readonly collectAutofillContentService: CollectAutofillContentService;
|
private readonly collectAutofillContentService: CollectAutofillContentService;
|
||||||
|
@ -38,6 +40,7 @@ class AutofillInit implements AutofillInitInterface {
|
||||||
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
|
* @param autofillOverlayContentService - The autofill overlay content service, potentially undefined.
|
||||||
*/
|
*/
|
||||||
constructor(autofillOverlayContentService?: AutofillOverlayContentService) {
|
constructor(autofillOverlayContentService?: AutofillOverlayContentService) {
|
||||||
|
this.autofillFieldQualificationService = new AutofillFieldQualificationService();
|
||||||
this.autofillOverlayContentService = autofillOverlayContentService;
|
this.autofillOverlayContentService = autofillOverlayContentService;
|
||||||
this.domElementVisibilityService = new DomElementVisibilityService();
|
this.domElementVisibilityService = new DomElementVisibilityService();
|
||||||
this.collectAutofillContentService = new CollectAutofillContentService(
|
this.collectAutofillContentService = new CollectAutofillContentService(
|
||||||
|
@ -98,6 +101,17 @@ class AutofillInit implements AutofillInitInterface {
|
||||||
): Promise<AutofillPageDetails | void> {
|
): Promise<AutofillPageDetails | void> {
|
||||||
const pageDetails: AutofillPageDetails =
|
const pageDetails: AutofillPageDetails =
|
||||||
await this.collectAutofillContentService.getPageDetails();
|
await this.collectAutofillContentService.getPageDetails();
|
||||||
|
|
||||||
|
// console.log(pageDetails);
|
||||||
|
// pageDetails.fields.forEach((field) => {
|
||||||
|
// const isLoginField = this.autofillFieldQualificationService.isFieldForLoginForm(
|
||||||
|
// field,
|
||||||
|
// pageDetails,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// console.log(isLoginField, field);
|
||||||
|
// });
|
||||||
|
|
||||||
if (sendDetailsInResponse) {
|
if (sendDetailsInResponse) {
|
||||||
return pageDetails;
|
return pageDetails;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,163 @@ export class AutofillFieldQualificationService {
|
||||||
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
|
private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(",");
|
||||||
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
|
private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(",");
|
||||||
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
private autofillFieldKeywordsMap: WeakMap<AutofillField, string> = new WeakMap();
|
||||||
|
private invalidAutocompleteValuesSet = new Set(["off", "false"]);
|
||||||
|
|
||||||
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
isFieldForLoginForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean {
|
||||||
// Check if the field
|
// TODO: Determine whether it makes sense to even incorporate this.
|
||||||
return false;
|
if (!field.viewable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExistingPasswordField = this.isExistingPasswordField(field);
|
||||||
|
if (isExistingPasswordField) {
|
||||||
|
return this.isPasswordFieldForLoginForm(field, pageDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUsernameField = this.isUsernameField(field);
|
||||||
|
if (!isUsernameField) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.isUsernameFieldForLoginForm(field, pageDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
isUsernameField(field: AutofillField): boolean {
|
private isPasswordFieldForLoginForm(
|
||||||
|
field: AutofillField,
|
||||||
|
pageDetails: AutofillPageDetails,
|
||||||
|
): boolean {
|
||||||
|
// Check if the autocomplete attribute is set to "current-password", if so treat this as a password field
|
||||||
|
if (field.autoCompleteType === "current-password") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field has a form parent
|
||||||
|
const parentForm = pageDetails.forms[field.form];
|
||||||
|
const usernameFieldsInPageDetails = pageDetails.fields.filter(this.isUsernameField);
|
||||||
|
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isExistingPasswordField);
|
||||||
|
// If no form parent is found, check if a username field exists and no other password fields are found in the page details, if so treat this as a password field
|
||||||
|
if (
|
||||||
|
!parentForm &&
|
||||||
|
usernameFieldsInPageDetails.length === 1 &&
|
||||||
|
passwordFieldsInPageDetails.length === 1
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no form parent is found and the autocomplete attribute is set to "off" or "false", this is not a password field
|
||||||
|
if (!parentForm && this.invalidAutocompleteValuesSet.has(field.autoCompleteType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a form parent and if the form has username field and no other password fields exist, if so treat this as a password field
|
||||||
|
if (
|
||||||
|
parentForm &&
|
||||||
|
usernameFieldsInPageDetails.length === 1 &&
|
||||||
|
passwordFieldsInPageDetails.length === 1
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a form parent and there are multiple visible password fields in the form, this is not a username field
|
||||||
|
const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
|
||||||
|
(field) => field.viewable,
|
||||||
|
);
|
||||||
|
if (parentForm && visiblePasswordFieldsInPageDetails.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a form parent a no username field exists and the field has an autocomplete attribute set to "off" or "false", this is not a password field
|
||||||
|
if (
|
||||||
|
parentForm &&
|
||||||
|
usernameFieldsInPageDetails.length === 0 &&
|
||||||
|
this.invalidAutocompleteValuesSet.has(field.autoCompleteType)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isUsernameFieldForLoginForm(
|
||||||
|
field: AutofillField,
|
||||||
|
pageDetails: AutofillPageDetails,
|
||||||
|
): boolean {
|
||||||
|
// console.log(field);
|
||||||
|
|
||||||
|
// Check if the autocomplete attribute is set to "username", if so treat this as a username field
|
||||||
|
if (field.autoCompleteType === "username") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field has a form parent
|
||||||
|
const parentForm = pageDetails.forms[field.form];
|
||||||
|
const passwordFieldsInPageDetails = pageDetails.fields.filter(this.isExistingPasswordField);
|
||||||
|
// console.log(passwordFieldsInPageDetails);
|
||||||
|
|
||||||
|
// If no form parent is found, check if a single password field is found in the page details, if so treat this as a username field
|
||||||
|
if (!parentForm && passwordFieldsInPageDetails.length === 1) {
|
||||||
|
// TODO: We should consider checking the distance between the username and password fields in the DOM to determine if they are close enough to be considered a pair
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no form parent is found and the autocomplete attribute is set to "off" or "false", this is not a username field
|
||||||
|
if (!parentForm && this.invalidAutocompleteValuesSet.has(field.autoCompleteType)) {
|
||||||
|
// console.log("invalid autocomplete value");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a form parent and if the form has a single password field, if so treat this as a username field
|
||||||
|
if (
|
||||||
|
parentForm &&
|
||||||
|
passwordFieldsInPageDetails.length === 1 &&
|
||||||
|
parentForm === pageDetails.forms[passwordFieldsInPageDetails[0].form] &&
|
||||||
|
field.elementNumber < passwordFieldsInPageDetails[0].elementNumber
|
||||||
|
) {
|
||||||
|
// console.log("shared form");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a form parent and the form has a single password that is before the username, this is not a username field
|
||||||
|
if (
|
||||||
|
parentForm &&
|
||||||
|
passwordFieldsInPageDetails.length === 1 &&
|
||||||
|
(parentForm !== pageDetails.forms[passwordFieldsInPageDetails[0].form] ||
|
||||||
|
field.elementNumber >= passwordFieldsInPageDetails[0].elementNumber)
|
||||||
|
) {
|
||||||
|
// console.log("username field is below password field");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a form parent and there are multiple visible password fields in the form, this is not a username field
|
||||||
|
const visiblePasswordFieldsInPageDetails = passwordFieldsInPageDetails.filter(
|
||||||
|
(field) => field.viewable,
|
||||||
|
);
|
||||||
|
if (parentForm && visiblePasswordFieldsInPageDetails.length > 1) {
|
||||||
|
// console.log("multiple password fields");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field has a form parent and the form has no password fields and has an autocomplete attribute set to "off" or "false", this is not a username field
|
||||||
|
if (
|
||||||
|
parentForm &&
|
||||||
|
passwordFieldsInPageDetails.length === 0 &&
|
||||||
|
this.invalidAutocompleteValuesSet.has(field.autoCompleteType)
|
||||||
|
) {
|
||||||
|
// console.log("no password fields");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherFieldsInForm = pageDetails.fields.filter((f) => f.form === field.form);
|
||||||
|
// If the parent form has no password fields and the form has multiple fields, this is not a username field
|
||||||
|
if (parentForm && passwordFieldsInPageDetails.length === 0 && otherFieldsInForm.length > 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("no previous conditions met");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUsernameField = (field: AutofillField): boolean => {
|
||||||
if (
|
if (
|
||||||
!this.usernameFieldTypes.has(field.type) ||
|
!this.usernameFieldTypes.has(field.type) ||
|
||||||
this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)
|
this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)
|
||||||
|
@ -25,17 +175,17 @@ export class AutofillFieldQualificationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames);
|
return this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames);
|
||||||
}
|
};
|
||||||
|
|
||||||
isExistingPasswordField(field: AutofillField): boolean {
|
isExistingPasswordField = (field: AutofillField): boolean => {
|
||||||
if (field.autoComplete === "new-password") {
|
if (field.autoCompleteType === "new-password") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.isPasswordField(field);
|
return this.isPasswordField(field);
|
||||||
}
|
};
|
||||||
|
|
||||||
isPasswordField(field: AutofillField): boolean {
|
isPasswordField = (field: AutofillField): boolean => {
|
||||||
const isInputPasswordType = field.type === "password";
|
const isInputPasswordType = field.type === "password";
|
||||||
if (
|
if (
|
||||||
!isInputPasswordType ||
|
!isInputPasswordType ||
|
||||||
|
@ -46,7 +196,7 @@ export class AutofillFieldQualificationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return isInputPasswordType || this.isLikePasswordField(field);
|
return isInputPasswordType || this.isLikePasswordField(field);
|
||||||
}
|
};
|
||||||
|
|
||||||
private isLikePasswordField(field: AutofillField): boolean {
|
private isLikePasswordField(field: AutofillField): boolean {
|
||||||
if (field.type !== "text") {
|
if (field.type !== "text") {
|
||||||
|
|
Loading…
Reference in New Issue