Refactor formdetector to separate localization params (#541)

This commit is contained in:
Leendert de Borst
2025-02-20 16:42:42 +01:00
parent a6529d67fa
commit fdbf3db6bb
5 changed files with 162 additions and 32 deletions

View File

@@ -48,10 +48,19 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
document.getElementsByName(elementIdentifier)[0];
if (target instanceof HTMLInputElement) {
openAutofillPopup(target, true); // Pass true to force open
// Inject icon
injectIcon(target);
// Force open the popup
openAutofillPopup(target, true);
sendResponse({ success: true });
} else {
sendResponse({ success: false, error: 'Target element is not an input field' });
}
} else {
sendResponse({ success: false, error: 'No element identifier provided' });
}
}
// Must return true if response is sent asynchronously
return true;
});

View File

@@ -63,10 +63,15 @@ export function handleContextMenuClick(info: chrome.contextMenus.OnClickData, ta
}, (results) => {
const elementIdentifier = results[0]?.result;
if (elementIdentifier) {
// Then send message to content script
chrome.tabs.sendMessage(tab.id, {
type: 'OPEN_ALIASVAULT_POPUP',
elementIdentifier
// Then send message to content script with proper error handling
chrome.tabs.sendMessage(
tab.id,
{
type: 'OPEN_ALIASVAULT_POPUP',
elementIdentifier
}
).catch(error => {
console.error('Error sending message to content script:', error);
});
}
});

View File

@@ -0,0 +1,113 @@
/**
* Type for field patterns. These patterns are used to detect individual fields in the form.
*/
export type FieldPatterns = {
username: string[];
firstName: string[];
lastName: string[];
email: string[];
emailConfirm: string[];
password: string[];
passwordConfirm: string[];
birthdate: string[];
gender: string[];
birthDateDay: string[];
birthDateMonth: string[];
birthDateYear: string[];
}
/**
* Type for gender option patterns. These patterns are used to detect individual gender options (radio/select) in the form.
*/
export type GenderOptionPatterns = {
male: string[];
female: string[];
other: string[];
}
/**
* English field patterns to detect English form fields.
*/
export const EnglishFieldPatterns: FieldPatterns = {
username: ['username', 'login', 'identifier', 'user'],
firstName: ['firstname', 'first-name', 'fname', 'name', 'given-name'],
lastName: ['lastname', 'last-name', 'lname', 'surname', 'family-name'],
email: ['email', 'mail', 'emailaddress'],
emailConfirm: ['confirm', 'verification', 'repeat', 'retype', 'verify'],
password: ['password', 'pwd', 'pass'],
passwordConfirm: ['confirm', 'verification', 'repeat', 'retype', '2', 'verify'],
birthdate: ['birthdate', 'birth-date', 'dob', 'date-of-birth'],
gender: ['gender', 'sex'],
birthDateDay: ['birth-day', 'birthday', 'day', 'birthdate_d'],
birthDateMonth: ['birth-month', 'birthmonth', 'month', 'birthdate_m'],
birthDateYear: ['birth-year', 'birthyear', 'year', 'birthdate_y']
};
/**
* English gender option patterns.
*/
export const EnglishGenderOptionPatterns: GenderOptionPatterns = {
male: ['male', 'man', 'm', 'gender1'],
female: ['female', 'woman', 'f', 'gender2'],
other: ['other', 'diverse', 'custom', 'prefer not', 'unknown', 'gender3']
};
/**
* Dutch field patterns used to detect Dutch form fields.
*/
export const DutchFieldPatterns: FieldPatterns = {
username: ['gebruikersnaam', 'gebruiker', 'login', 'identifier'],
firstName: ['voornaam', 'naam'],
lastName: ['achternaam'],
email: ['e-mailadres', 'e-mail'],
emailConfirm: ['bevestig', 'herhaal', 'verificatie'],
password: ['wachtwoord', 'pwd'],
passwordConfirm: ['bevestig', 'herhaal', 'verificatie'],
birthdate: ['geboortedatum', 'geboorte-datum'],
gender: ['geslacht', 'aanhef'],
birthDateDay: ['dag'],
birthDateMonth: ['maand'],
birthDateYear: ['jaar']
};
/**
* Dutch gender option patterns
*/
export const DutchGenderOptionPatterns: GenderOptionPatterns = {
male: ['man', 'mannelijk', 'm'],
female: ['vrouw', 'vrouwelijk', 'v'],
other: ['anders', 'iets', 'overig', 'onbekend']
};
/**
* Combined field patterns which includes all supported languages.
*/
export const CombinedFieldPatterns: FieldPatterns = {
username: [...new Set([...EnglishFieldPatterns.username, ...DutchFieldPatterns.username])],
firstName: [...new Set([...EnglishFieldPatterns.firstName, ...DutchFieldPatterns.firstName])],
lastName: [...new Set([...EnglishFieldPatterns.lastName, ...DutchFieldPatterns.lastName])],
/**
* NOTE: Dutch email patterns should be prioritized over English email patterns due to how
* the nl-registration-form5.html honeypot field is named. The order of the patterns
* determine which field is detected. If a pattern entry with higher index is detected, that
* field will be selected instead of the lower index one.
*/
email: [...new Set([...DutchFieldPatterns.email, ...EnglishFieldPatterns.email])],
emailConfirm: [...new Set([...EnglishFieldPatterns.emailConfirm, ...DutchFieldPatterns.emailConfirm])],
password: [...new Set([...EnglishFieldPatterns.password, ...DutchFieldPatterns.password])],
passwordConfirm: [...new Set([...EnglishFieldPatterns.passwordConfirm, ...DutchFieldPatterns.passwordConfirm])],
birthdate: [...new Set([...EnglishFieldPatterns.birthdate, ...DutchFieldPatterns.birthdate])],
gender: [...new Set([...EnglishFieldPatterns.gender, ...DutchFieldPatterns.gender])],
birthDateDay: [...new Set([...EnglishFieldPatterns.birthDateDay, ...DutchFieldPatterns.birthDateDay])],
birthDateMonth: [...new Set([...EnglishFieldPatterns.birthDateMonth, ...DutchFieldPatterns.birthDateMonth])],
birthDateYear: [...new Set([...EnglishFieldPatterns.birthDateYear, ...DutchFieldPatterns.birthDateYear])]
};
/**
* Combined gender option patterns which includes all supported languages.
*/
export const CombinedGenderOptionPatterns: GenderOptionPatterns = {
male: [...new Set([...EnglishGenderOptionPatterns.male, ...DutchGenderOptionPatterns.male])],
female: [...new Set([...EnglishGenderOptionPatterns.female, ...DutchGenderOptionPatterns.female])],
other: [...new Set([...EnglishGenderOptionPatterns.other, ...DutchGenderOptionPatterns.other])]
};

View File

@@ -1,4 +1,5 @@
import { LoginForm } from "./types/LoginForm";
import { CombinedFieldPatterns, CombinedGenderOptionPatterns } from "./FieldPatterns";
/**
* Form detector.
@@ -115,7 +116,7 @@ export class FormDetector {
// Find primary email field
const primaryEmail = this.findInputField(
form,
['e-mailadres', 'e-mail', 'email', 'mail', '@', 'emailaddress'],
CombinedFieldPatterns.email,
['text', 'email']
);
@@ -123,7 +124,7 @@ export class FormDetector {
const confirmEmail = primaryEmail
? this.findInputField(
form,
['confirm', 'verification', 'repeat', 'retype', 'verify'],
CombinedFieldPatterns.emailConfirm,
['text', 'email']
)
: null;
@@ -139,7 +140,7 @@ export class FormDetector {
*/
private findBirthdateFields(form: HTMLFormElement | null, excludeElements: HTMLInputElement[] = []): LoginForm['birthdateField'] {
// First try to find a single date input
const singleDateField = this.findInputField(form, ['birthdate', 'birth-date', 'dob', 'geboortedatum'], ['date', 'text'], excludeElements);
const singleDateField = this.findInputField(form, CombinedFieldPatterns.birthdate, ['date', 'text'], excludeElements);
// Detect date format by searching all text content in the form
let format = 'yyyy-mm-dd'; // default format
@@ -195,9 +196,9 @@ export class FormDetector {
}
// Look for separate day/month/year fields
const dayField = this.findInputField(form, ['birth-day', 'birthday', 'day', 'dag', 'birthdate_d'], ['text', 'number', 'select'], excludeElements);
const monthField = this.findInputField(form, ['birth-month', 'birthmonth', 'month', 'maand', 'birthdate_m'], ['text', 'number', 'select'], excludeElements);
const yearField = this.findInputField(form, ['birth-year', 'birthyear', 'year', 'jaar', 'birthdate_y'], ['text', 'number', 'select'], excludeElements);
const dayField = this.findInputField(form, CombinedFieldPatterns.birthDateDay, ['text', 'number', 'select'], excludeElements);
const monthField = this.findInputField(form, CombinedFieldPatterns.birthDateMonth, ['text', 'number', 'select'], excludeElements);
const yearField = this.findInputField(form, CombinedFieldPatterns.birthDateYear, ['text', 'number', 'select'], excludeElements);
return {
single: null,
@@ -215,7 +216,7 @@ export class FormDetector {
// Try to find select or input element using the shared method
const genderField = this.findInputField(
form,
['gender', 'sex', 'geslacht', 'aanhef'],
CombinedFieldPatterns.gender,
['select'],
excludeElements
);
@@ -233,11 +234,6 @@ export class FormDetector {
: null;
if (radioButtons && radioButtons.length > 0) {
// Map specific gender radio buttons
const malePatterns = ['male', 'man', 'm', 'man', 'gender1'];
const femalePatterns = ['female', 'woman', 'f', 'vrouw', 'gender2'];
const otherPatterns = ['other', 'diverse', 'custom', 'prefer not', 'anders', 'iets', 'unknown', 'gender3'];
/**
* Find a radio button by patterns.
*/
@@ -252,8 +248,8 @@ export class FormDetector {
// For "other" patterns, skip if it matches male or female patterns
if (isOther && (
malePatterns.some(pattern => attributes.some(attr => attr.includes(pattern))) ||
femalePatterns.some(pattern => attributes.some(attr => attr.includes(pattern)))
CombinedGenderOptionPatterns.male.some(pattern => attributes.some(attr => attr.includes(pattern))) ||
CombinedGenderOptionPatterns.female.some(pattern => attributes.some(attr => attr.includes(pattern)))
)) {
return false;
}
@@ -268,15 +264,15 @@ export class FormDetector {
type: 'radio',
field: null, // Set to null since we're providing specific mappings
radioButtons: {
male: findRadioByPatterns(malePatterns),
female: findRadioByPatterns(femalePatterns),
other: findRadioByPatterns(otherPatterns)
male: findRadioByPatterns(CombinedGenderOptionPatterns.male),
female: findRadioByPatterns(CombinedGenderOptionPatterns.female),
other: findRadioByPatterns(CombinedGenderOptionPatterns.other)
}
};
}
// Fall back to regular text input
const textField = this.findInputField(form, ['gender', 'sex', 'geslacht', 'aanhef'], ['text'], excludeElements);
const textField = this.findInputField(form, CombinedFieldPatterns.gender, ['text'], excludeElements);
return {
type: 'text',
@@ -306,8 +302,7 @@ export class FormDetector {
input.placeholder
].map(attr => attr?.toLowerCase() ?? '');
const confirmPatterns = ['confirm', 'verification', 'repeat', 'retype', '2', 'verify'];
if (!confirmPatterns.some(pattern => attributes.some(attr => attr.includes(pattern)))) {
if (!CombinedFieldPatterns.passwordConfirm.some(pattern => attributes.some(attr => attr.includes(pattern)))) {
primaryPassword = input;
break;
}
@@ -370,9 +365,17 @@ export class FormDetector {
}
// Check if the form contains a username field.
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, ['username', 'gebruikersnaam', 'gebruiker', 'login', 'identifier', 'user'], ['text'], []);
if (usernameField) {
const isValid = force || usernameField.getAttribute('autocomplete') !== 'off';
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text'], []);
// Check if the form contains name fields.
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.firstName, ['text'], []);
const lastNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.lastName, ['text'], []);
// Get the first field that is not null.
const field = usernameField ?? firstNameField ?? lastNameField;
if (field) {
// Check if the field is valid by checking if the autocomplete attribute is not set to off (which would indicate that the website considers this field not meant to be autofilled)
const isValid = force || field?.getAttribute('autocomplete') !== 'off';
if (isValid) {
return true;
}
@@ -401,13 +404,13 @@ export class FormDetector {
if (passwordFields.primary) detectedFields.push(passwordFields.primary);
if (passwordFields.confirm) detectedFields.push(passwordFields.confirm);
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, ['username', 'gebruikersnaam', 'gebruiker', 'login', 'identifier', 'user'],['text'], detectedFields);
const usernameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.username, ['text'], detectedFields);
if (usernameField) detectedFields.push(usernameField);
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, ['firstname', 'first-name', 'fname', 'voornaam', 'name'], ['text'], detectedFields);
const firstNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.firstName, ['text'], detectedFields);
if (firstNameField) detectedFields.push(firstNameField);
const lastNameField = this.findInputField(wrapper as HTMLFormElement | null, ['lastname', 'last-name', 'lname', 'achternaam'], ['text'], detectedFields);
const lastNameField = this.findInputField(wrapper as HTMLFormElement | null, CombinedFieldPatterns.lastName, ['text'], detectedFields);
if (lastNameField) detectedFields.push(lastNameField);
const birthdateField = this.findBirthdateFields(wrapper as HTMLFormElement | null, detectedFields);

View File

@@ -108,7 +108,7 @@ export class PasswordGenerator {
this.uppercaseChars[this.getUnbiasedRandomIndex(this.uppercaseChars.length)] +
password.substring(pos + 1);
}
if (this.useNumbers && !RegExp('[\d]').exec(password)) {
if (this.useNumbers && !RegExp('\\d').exec(password)) {
const pos = this.getUnbiasedRandomIndex(this.length);
password = password.substring(0, pos) +
this.numberChars[this.getUnbiasedRandomIndex(this.numberChars.length)] +