diff --git a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts index 847bee987..f4c7a0431 100644 --- a/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts +++ b/browser-extensions/chrome/src/shared/formDetector/FormDetector.ts @@ -6,6 +6,8 @@ import { LoginForm } from "./types/LoginForm"; export class FormDetector { private document: Document; private clickedElement: HTMLElement | null; + private processedForms = new Set(); + private forms: LoginForm[] = []; /** * Constructor. @@ -19,52 +21,16 @@ export class FormDetector { * Detect login forms on the page, prioritizing the form containing the clicked element. */ public detectForms(): LoginForm[] { - const forms: LoginForm[] = []; - - // Create a Set to track processed forms to avoid duplicates - const processedForms = new Set(); - - /** - * Helper to create a form entry - */ - const createFormEntry = (form: HTMLFormElement | null): void => { - // Skip if we've already processed this form - if (form && processedForms.has(form)) return; - processedForms.add(form); - - // Find all relevant fields - const emailFields = this.findEmailField(form); - const usernameField = this.findUsernameField(form); - const passwordFields = this.findPasswordField(form); - const firstNameField = this.findInputField(form, ['firstname', 'first-name', 'fname', 'voornaam', 'name'], ['text']); - const lastNameField = this.findInputField(form, ['lastname', 'last-name', 'lname', 'achternaam'], ['text']); - const birthdateField = this.findBirthdateFields(form); - const genderField = this.findGenderField(form); - - forms.push({ - form, - emailField: emailFields.primary, - emailConfirmField: emailFields.confirm, - usernameField, - passwordField: passwordFields.primary, - passwordConfirmField: passwordFields.confirm, - firstNameField, - lastNameField, - birthdateField, - genderField - }); - }; - // If we have a clicked element, try to find its form first if (this.clickedElement) { const formWrapper = this.clickedElement.closest('form'); if (formWrapper) { - createFormEntry(formWrapper); + this.createFormEntry(formWrapper); // If we found a valid form, return early - if (forms.length > 0) { - return forms; + if (this.forms.length > 0) { + return this.forms; } } } @@ -77,30 +43,30 @@ export class FormDetector { // Process password fields first passwordFields.forEach(passwordField => { const form = passwordField.closest('form'); - createFormEntry(form); + this.createFormEntry(form); }); // Process email fields that aren't already part of a processed form emailFields.forEach(field => { const form = field.closest('form'); - if (form && processedForms.has(form)) return; + if (form && this.processedForms.has(form)) return; if (this.isLikelyEmailField(field)) { - createFormEntry(form); + this.createFormEntry(form); } }); // Process potential username fields that aren't already part of a processed form textFields.forEach(field => { const form = field.closest('form'); - if (form && processedForms.has(form)) return; + if (form && this.processedForms.has(form)) return; if (this.isLikelyUsernameField(field)) { - createFormEntry(form); + this.createFormEntry(form); } }); - return forms; + return this.forms; } /** @@ -109,7 +75,8 @@ export class FormDetector { private findInputField( form: HTMLFormElement | null, patterns: string[], - types: string[] + types: string[], + excludeElements: HTMLInputElement[] = [] ): HTMLInputElement | null { const candidates = form ? form.querySelectorAll('input, select') @@ -120,6 +87,9 @@ export class FormDetector { let bestMatchIndex = patterns.length; for (const input of Array.from(candidates)) { + // Skip if this element is already used + if (excludeElements.includes(input)) continue; + // Handle both input and select elements const type = input.tagName.toLowerCase() === 'select' ? 'select' : input.type.toLowerCase(); if (!types.includes(type)) continue; @@ -170,36 +140,7 @@ export class FormDetector { } /** - * Find the username field in the form containing the password field. - */ - private findUsernameField(form: HTMLFormElement | null): HTMLInputElement | null { - const candidates = form - ? form.querySelectorAll('input') - : this.document.querySelectorAll('input'); - - for (const input of Array.from(candidates)) { - const type = input.type.toLowerCase(); - if (type === 'text') { - const attributes = [ - input.type, - input.id, - input.name, - input.className, - input.placeholder - ].map(attr => attr?.toLowerCase() || ''); - - const patterns = ['user', 'username', 'login', 'identifier']; - if (patterns.some(pattern => attributes.some(attr => attr.includes(pattern)))) { - return input; - } - } - } - - return null; - } - - /** - * Find the email field in the form containing the password field. + * Find the email field in the form. */ private findEmailField(form: HTMLFormElement | null): { primary: HTMLInputElement | null, @@ -230,9 +171,9 @@ export class FormDetector { /** * Find the birthdate fields in the form. */ - private findBirthdateFields(form: HTMLFormElement | null): LoginForm['birthdateField'] { + 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']); + const singleDateField = this.findInputField(form, ['birthdate', 'birth-date', 'dob', 'geboortedatum'], ['date', 'text'], excludeElements); // Detect date format by searching all text content in the form let format = 'yyyy-mm-dd'; // default format @@ -288,9 +229,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']); - const monthField = this.findInputField(form, ['birth-month', 'birthmonth', 'month', 'maand', 'birthdate_m'], ['text', 'number', 'select']); - const yearField = this.findInputField(form, ['birth-year', 'birthyear', 'year', 'jaar', 'birthdate_y'], ['text', 'number', 'select']); + 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); return { single: null, @@ -304,12 +245,13 @@ export class FormDetector { /** * Find the gender field in the form. */ - private findGenderField(form: HTMLFormElement | null): LoginForm['genderField'] { + private findGenderField(form: HTMLFormElement | null, excludeElements: HTMLInputElement[] = []): LoginForm['genderField'] { // Try to find select or input element using the shared method const genderField = this.findInputField( form, ['gender', 'sex', 'geslacht', 'aanhef'], - ['select'] + ['select'], + excludeElements ); if (genderField?.tagName.toLowerCase() === 'select') { @@ -368,7 +310,7 @@ export class FormDetector { } // Fall back to regular text input - const textField = this.findInputField(form, ['gender', 'sex', 'geslacht', 'aanhef'], ['text']); + const textField = this.findInputField(form, ['gender', 'sex', 'geslacht', 'aanhef'], ['text'], excludeElements); return { type: 'text', @@ -467,4 +409,56 @@ export class FormDetector { confirm: confirmPassword }; } + + /** + * Create a form entry. + */ + private createFormEntry(form: HTMLFormElement | null): void { + // Skip if we've already processed this form + if (form && this.processedForms.has(form)) return; + this.processedForms.add(form); + + // Keep track of detected fields to prevent overlap + const detectedFields: HTMLInputElement[] = []; + + // Find fields in priority order (most specific to least specific). + const emailFields = this.findEmailField(form); + if (emailFields.primary) detectedFields.push(emailFields.primary); + if (emailFields.confirm) detectedFields.push(emailFields.confirm); + + const passwordFields = this.findPasswordField(form); + if (passwordFields.primary) detectedFields.push(passwordFields.primary); + if (passwordFields.confirm) detectedFields.push(passwordFields.confirm); + + const usernameField = this.findInputField(form, ['username', 'gebruikersnaam', 'gebruiker', 'login', 'identifier', 'user'],['text'], detectedFields); + if (usernameField) detectedFields.push(usernameField); + + const firstNameField = this.findInputField(form, ['firstname', 'first-name', 'fname', 'voornaam', 'name'], ['text'], detectedFields); + if (firstNameField) detectedFields.push(firstNameField); + + const lastNameField = this.findInputField(form, ['lastname', 'last-name', 'lname', 'achternaam'], ['text'], detectedFields); + if (lastNameField) detectedFields.push(lastNameField); + + const birthdateField = this.findBirthdateFields(form, detectedFields); + if (birthdateField.single) detectedFields.push(birthdateField.single); + if (birthdateField.day) detectedFields.push(birthdateField.day); + if (birthdateField.month) detectedFields.push(birthdateField.month); + if (birthdateField.year) detectedFields.push(birthdateField.year); + + const genderField = this.findGenderField(form, detectedFields); + if (genderField.field) detectedFields.push(genderField.field as HTMLInputElement); + + this.forms.push({ + form, + emailField: emailFields.primary, + emailConfirmField: emailFields.confirm, + usernameField, + passwordField: passwordFields.primary, + passwordConfirmField: passwordFields.confirm, + firstNameField, + lastNameField, + birthdateField, + genderField + }); + } } diff --git a/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.nl.test.ts b/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.nl.test.ts index 0cee503a8..d90a43bfd 100644 --- a/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.nl.test.ts +++ b/browser-extensions/chrome/src/shared/formDetector/__tests__/FormDetector.nl.test.ts @@ -86,4 +86,13 @@ describe('FormDetector', () => { testField(FormField.Email, 'aliasvault-input-email', htmlFile); testField(FormField.LastName, 'aliasvault-input-lastname', htmlFile); }); + + describe('Dutch registration form 9 detection', () => { + const htmlFile = 'nl-registration-form9.html'; + + testField(FormField.Username, 'user_username', htmlFile); + testField(FormField.Email, 'user_email_address', htmlFile); + testField(FormField.Password, 'user_password', htmlFile); + testField(FormField.PasswordConfirm, 'user_password_confirmation', htmlFile); + }); }); diff --git a/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/nl-registration-form9.html b/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/nl-registration-form9.html new file mode 100644 index 000000000..e3cf17c94 --- /dev/null +++ b/browser-extensions/chrome/src/shared/formDetector/__tests__/test-forms/nl-registration-form9.html @@ -0,0 +1,139 @@ + +
+
Sommige velden zijn niet ingevuld of bevatten verkeerde invoer (zie de foutmeldingen bij de velden). Graag herstellen en opnieuw proberen.
+
+ Schoolgegevens + Bent u een ouder? Klik hier + +
+ +
+
+

Geef de naam van het subdomein waarop u uw school op SchouderCom wilt kunnen benaderen. De naam die u invult +wordt voor schoudercom.nl geplaatst en bepaald het webadres.

+
+

Voorbeeld: indien u "regenboog" invult zal uw site onder regenboog.schoudercom.nl bereikbaar zijn.

+
+
+
+ Regio +
+ + +     + +     + + +
+

Selecteer de regio waartoe uw school behoort.


+

+ Wanneer u in de beheeromgeving de vakanties aan de jaarkalender toevoegt, zal SchouderCom + automatisch de juiste vakanties voor uw regio invoegen. Daarna kunt u desgewenst de gegevens + van de vakanties nog wijzigen.

+
+
+
+ Standaardtaal +
+
+ + + + + + + + + + + + +
+
+

+ De standaardtaal voor nieuwe gebruikers bepaalt de taal van de uitnodigingsemails waarmee nieuwe gebruikers + uitgenodigd worden een account aan te maken. Zodra ze een account hebben aangemaakt kunnen ze zelf de taalkeuze + aanpassen aan hun voorkeuren. +

+

+ U kunt de standaardtaal later via de Beheer tab altijd weer aanpassen. +

+
+
+
+
+ Gegevens beheerder + +
+
+
+
+ +
+
+

Iedereen binnen SchouderCom heeft een gebruikersnaam.

+

De gebruikersnaam moet uniek zijn, het kan zijn dat u hierover een melding krijgt en een andere moet kiezen.

+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+ +
+

+ Om het automatisch invullen van formulieren door robots tegen te gaan verzoeken wij u deze vraag te beantwoorden. +

+
+
+ +
+
+ + of + of + +

+
+
\ No newline at end of file