diff --git a/browser-extensions/chrome/contentScript.ts b/browser-extensions/chrome/contentScript.ts index 4a14b2972..4747b895a 100644 --- a/browser-extensions/chrome/contentScript.ts +++ b/browser-extensions/chrome/contentScript.ts @@ -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; }); \ No newline at end of file diff --git a/browser-extensions/chrome/src/background/ContextMenu.ts b/browser-extensions/chrome/src/background/ContextMenu.ts index d8eee20c0..568721ffa 100644 --- a/browser-extensions/chrome/src/background/ContextMenu.ts +++ b/browser-extensions/chrome/src/background/ContextMenu.ts @@ -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); }); } }); diff --git a/browser-extensions/chrome/src/shared/formDetector/FieldPatterns.ts b/browser-extensions/chrome/src/shared/formDetector/FieldPatterns.ts new file mode 100644 index 000000000..fe46dff7e --- /dev/null +++ b/browser-extensions/chrome/src/shared/formDetector/FieldPatterns.ts @@ -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])] +}; \ No newline at end of file diff --git a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts index c1267abba..502c3432d 100644 --- a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts +++ b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts @@ -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); diff --git a/browser-extensions/chrome/src/shared/generators/Password/PasswordGenerator.ts b/browser-extensions/chrome/src/shared/generators/Password/PasswordGenerator.ts index 5282bb4b6..ac1c4f744 100644 --- a/browser-extensions/chrome/src/shared/generators/Password/PasswordGenerator.ts +++ b/browser-extensions/chrome/src/shared/generators/Password/PasswordGenerator.ts @@ -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)] +