mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 15:18:02 -04:00
Refactor formdetector to separate localization params (#541)
This commit is contained in:
@@ -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;
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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])]
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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)] +
|
||||
|
||||
Reference in New Issue
Block a user