diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 516872a06..7c41302c0 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -11004,9 +11004,6 @@ class FormAnalyzer { */ this.signals = []; - // Analyse the input that was passed. This is pretty arbitrary, but historically it's been working nicely. - this.evaluateElAttributes(input, 1, true); - // If we have a meaningful container (a form), check that, otherwise check the whole page if (form !== input) { this.evaluateForm(); @@ -11103,7 +11100,7 @@ class FormAnalyzer { return this; } const signupRegexToUse = this.matching.getDDGMatcherRegex(shouldBeConservative ? 'conservativeSignupRegex' : 'signupRegex'); - const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new.?password/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); + const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new?.(password|username)/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); // In some cases a login match means the login is somewhere else, i.e. when a link points outside if (shouldFlip) { @@ -11185,72 +11182,12 @@ class FormAnalyzer { } }); } - - /** - * Takes an element and returns all its children that are text-only nodes - * @param {HTMLElement|Element} element - * @param {number} maxDepth - * @param {number} currentDepth - * @returns {HTMLElement[]|Element[]} - */ - getElementsWithOnlyTextChild(element) { - let maxDepth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; - let currentDepth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - // Array to collect elements with only text child nodes - const elementsWithTextChild = []; - - // If we've reached the max depth, stop traversing further - if (currentDepth > maxDepth) { - return elementsWithTextChild; - } - - // Check if the current element has only one text child node - if (element.nodeType === Node.ELEMENT_NODE) { - const childNodes = element.childNodes; - if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) { - elementsWithTextChild.push(element); - } - } - - // Recurse through each child element and collect matching elements - for (const child of element.children) { - // Recursively get elements from child elements, increasing depth by 1 - elementsWithTextChild.push(...this.getElementsWithOnlyTextChild(child, maxDepth, currentDepth + 1)); - } - return elementsWithTextChild; - } - evaluateFormHeaderSignals() { - const isVisuallyBeforeForm = el => el.getBoundingClientRect().top < this.form.getBoundingClientRect().top; - const isHeaderSized = el => { - if (el instanceof HTMLHeadingElement) { - return true; - } - const computedStyle = window.getComputedStyle(el); - const fontWeight = computedStyle.fontWeight; - const isRelativelyTall = parseFloat(computedStyle.height) / this.form.clientHeight > 0.1; - if (isRelativelyTall && (fontWeight === 'bold' || parseFloat(fontWeight) >= 700)) { - return true; - } - }; - const allSiblings = Array.from(this.form.parentElement?.children ?? []).filter(element => element !== this.form).map(element => this.getElementsWithOnlyTextChild(element)).flat(); - if (allSiblings.length === 0) return false; - allSiblings.forEach(element => { - if (element instanceof HTMLElement && isVisuallyBeforeForm(element) && isHeaderSized(element)) { - const string = element.textContent?.trim(); - if (string) { - if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?in|log[- ]?in)$/i, string)) { - return this.decreaseSignalBy(3, 'Strong login signal above form'); - } else if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?up)$/i, string)) { - return this.increaseSignalBy(3, 'Strong signup signal above form'); - } - } - } - }); - } evaluatePasswordHints() { - const hasPasswordHints = Array.from(this.form.querySelectorAll('div, span')).filter(div => div.textContent != null && div.textContent.trim() !== '' && window.getComputedStyle(div).display !== 'none' && window.getComputedStyle(div).visibility !== 'hidden').some(div => div.textContent && (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), div.textContent)); - if (hasPasswordHints) { - this.increaseSignalBy(3, 'Password hints'); + if (this.form.textContent) { + const hasPasswordHints = (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), this.form.textContent, 200); + if (hasPasswordHints) { + this.increaseSignalBy(5, 'Password hints'); + } } } @@ -11344,6 +11281,11 @@ class FormAnalyzer { // Check page title this.evaluatePageTitle(); + // Evaluate form's input elements + this.form.querySelectorAll(this.matching.cssSelector('formInputsSelector')).forEach(input => { + this.evaluateElAttributes(input, 1, true); + }); + // Check form attributes this.evaluateElAttributes(this.form); @@ -11370,7 +11312,6 @@ class FormAnalyzer { // If we can't decide at this point, try reading form headers and password hints if (this.areLoginOrSignupSignalsWeak()) { this.evaluatePasswordHints(); - this.evaluateFormHeaderSignals(); } // If we can't decide at this point, try reading page headings @@ -17800,7 +17741,8 @@ function isFormLikelyToBeUsedAsPageWrapper(form) { * @returns {boolean} */ function safeRegexTest(regex, string) { - if (!string || !regex || string.length > _constants.constants.TEXT_LENGTH_CUTOFF) return false; + let textLengthCutoff = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _constants.constants.TEXT_LENGTH_CUTOFF; + if (!string || !regex || string.length > textLengthCutoff) return false; return regex.test(string); } diff --git a/dist/autofill.js b/dist/autofill.js index 25c5d33e3..91552d992 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -6641,9 +6641,6 @@ class FormAnalyzer { */ this.signals = []; - // Analyse the input that was passed. This is pretty arbitrary, but historically it's been working nicely. - this.evaluateElAttributes(input, 1, true); - // If we have a meaningful container (a form), check that, otherwise check the whole page if (form !== input) { this.evaluateForm(); @@ -6740,7 +6737,7 @@ class FormAnalyzer { return this; } const signupRegexToUse = this.matching.getDDGMatcherRegex(shouldBeConservative ? 'conservativeSignupRegex' : 'signupRegex'); - const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new.?password/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); + const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new?.(password|username)/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); // In some cases a login match means the login is somewhere else, i.e. when a link points outside if (shouldFlip) { @@ -6822,72 +6819,12 @@ class FormAnalyzer { } }); } - - /** - * Takes an element and returns all its children that are text-only nodes - * @param {HTMLElement|Element} element - * @param {number} maxDepth - * @param {number} currentDepth - * @returns {HTMLElement[]|Element[]} - */ - getElementsWithOnlyTextChild(element) { - let maxDepth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; - let currentDepth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - // Array to collect elements with only text child nodes - const elementsWithTextChild = []; - - // If we've reached the max depth, stop traversing further - if (currentDepth > maxDepth) { - return elementsWithTextChild; - } - - // Check if the current element has only one text child node - if (element.nodeType === Node.ELEMENT_NODE) { - const childNodes = element.childNodes; - if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) { - elementsWithTextChild.push(element); - } - } - - // Recurse through each child element and collect matching elements - for (const child of element.children) { - // Recursively get elements from child elements, increasing depth by 1 - elementsWithTextChild.push(...this.getElementsWithOnlyTextChild(child, maxDepth, currentDepth + 1)); - } - return elementsWithTextChild; - } - evaluateFormHeaderSignals() { - const isVisuallyBeforeForm = el => el.getBoundingClientRect().top < this.form.getBoundingClientRect().top; - const isHeaderSized = el => { - if (el instanceof HTMLHeadingElement) { - return true; - } - const computedStyle = window.getComputedStyle(el); - const fontWeight = computedStyle.fontWeight; - const isRelativelyTall = parseFloat(computedStyle.height) / this.form.clientHeight > 0.1; - if (isRelativelyTall && (fontWeight === 'bold' || parseFloat(fontWeight) >= 700)) { - return true; - } - }; - const allSiblings = Array.from(this.form.parentElement?.children ?? []).filter(element => element !== this.form).map(element => this.getElementsWithOnlyTextChild(element)).flat(); - if (allSiblings.length === 0) return false; - allSiblings.forEach(element => { - if (element instanceof HTMLElement && isVisuallyBeforeForm(element) && isHeaderSized(element)) { - const string = element.textContent?.trim(); - if (string) { - if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?in|log[- ]?in)$/i, string)) { - return this.decreaseSignalBy(3, 'Strong login signal above form'); - } else if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?up)$/i, string)) { - return this.increaseSignalBy(3, 'Strong signup signal above form'); - } - } - } - }); - } evaluatePasswordHints() { - const hasPasswordHints = Array.from(this.form.querySelectorAll('div, span')).filter(div => div.textContent != null && div.textContent.trim() !== '' && window.getComputedStyle(div).display !== 'none' && window.getComputedStyle(div).visibility !== 'hidden').some(div => div.textContent && (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), div.textContent)); - if (hasPasswordHints) { - this.increaseSignalBy(3, 'Password hints'); + if (this.form.textContent) { + const hasPasswordHints = (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), this.form.textContent, 200); + if (hasPasswordHints) { + this.increaseSignalBy(5, 'Password hints'); + } } } @@ -6981,6 +6918,11 @@ class FormAnalyzer { // Check page title this.evaluatePageTitle(); + // Evaluate form's input elements + this.form.querySelectorAll(this.matching.cssSelector('formInputsSelector')).forEach(input => { + this.evaluateElAttributes(input, 1, true); + }); + // Check form attributes this.evaluateElAttributes(this.form); @@ -7007,7 +6949,6 @@ class FormAnalyzer { // If we can't decide at this point, try reading form headers and password hints if (this.areLoginOrSignupSignalsWeak()) { this.evaluatePasswordHints(); - this.evaluateFormHeaderSignals(); } // If we can't decide at this point, try reading page headings @@ -13437,7 +13378,8 @@ function isFormLikelyToBeUsedAsPageWrapper(form) { * @returns {boolean} */ function safeRegexTest(regex, string) { - if (!string || !regex || string.length > _constants.constants.TEXT_LENGTH_CUTOFF) return false; + let textLengthCutoff = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _constants.constants.TEXT_LENGTH_CUTOFF; + if (!string || !regex || string.length > textLengthCutoff) return false; return regex.test(string); } diff --git a/src/Form/FormAnalyzer.js b/src/Form/FormAnalyzer.js index 003038353..27b7ed31b 100644 --- a/src/Form/FormAnalyzer.js +++ b/src/Form/FormAnalyzer.js @@ -40,9 +40,6 @@ class FormAnalyzer { */ this.signals = []; - // Analyse the input that was passed. This is pretty arbitrary, but historically it's been working nicely. - this.evaluateElAttributes(input, 1, true); - // If we have a meaningful container (a form), check that, otherwise check the whole page if (form !== input) { this.evaluateForm(); @@ -149,7 +146,7 @@ class FormAnalyzer { } const signupRegexToUse = this.matching.getDDGMatcherRegex(shouldBeConservative ? 'conservativeSignupRegex' : 'signupRegex'); - const matchesSignup = safeRegexTest(/new.?password/i, string) || safeRegexTest(signupRegexToUse, string); + const matchesSignup = safeRegexTest(/new?.(password|username)/i, string) || safeRegexTest(signupRegexToUse, string); // In some cases a login match means the login is somewhere else, i.e. when a link points outside if (shouldFlip) { @@ -232,89 +229,12 @@ class FormAnalyzer { }); } - /** - * Takes an element and returns all its children that are text-only nodes - * @param {HTMLElement|Element} element - * @param {number} maxDepth - * @param {number} currentDepth - * @returns {HTMLElement[]|Element[]} - */ - getElementsWithOnlyTextChild(element, maxDepth = 2, currentDepth = 0) { - // Array to collect elements with only text child nodes - const elementsWithTextChild = []; - - // If we've reached the max depth, stop traversing further - if (currentDepth > maxDepth) { - return elementsWithTextChild; - } - - // Check if the current element has only one text child node - if (element.nodeType === Node.ELEMENT_NODE) { - const childNodes = element.childNodes; - - if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) { - elementsWithTextChild.push(element); - } - } - - // Recurse through each child element and collect matching elements - for (const child of element.children) { - // Recursively get elements from child elements, increasing depth by 1 - elementsWithTextChild.push(...this.getElementsWithOnlyTextChild(child, maxDepth, currentDepth + 1)); - } - - return elementsWithTextChild; - } - - evaluateFormHeaderSignals() { - const isVisuallyBeforeForm = (el) => el.getBoundingClientRect().top < this.form.getBoundingClientRect().top; - - const isHeaderSized = (el) => { - if (el instanceof HTMLHeadingElement) { - return true; - } - - const computedStyle = window.getComputedStyle(el); - const fontWeight = computedStyle.fontWeight; - const isRelativelyTall = parseFloat(computedStyle.height) / this.form.clientHeight > 0.1; - if (isRelativelyTall && (fontWeight === 'bold' || parseFloat(fontWeight) >= 700)) { - return true; - } - }; - - const allSiblings = Array.from(this.form.parentElement?.children ?? []) - .filter((element) => element !== this.form) - .map((element) => this.getElementsWithOnlyTextChild(element)) - .flat(); - - if (allSiblings.length === 0) return false; - - allSiblings.forEach((element) => { - if (element instanceof HTMLElement && isVisuallyBeforeForm(element) && isHeaderSized(element)) { - const string = element.textContent?.trim(); - if (string) { - if (safeRegexTest(/^(sign[- ]?in|log[- ]?in)$/i, string)) { - return this.decreaseSignalBy(3, 'Strong login signal above form'); - } else if (safeRegexTest(/^(sign[- ]?up)$/i, string)) { - return this.increaseSignalBy(3, 'Strong signup signal above form'); - } - } - } - }); - } - evaluatePasswordHints() { - const hasPasswordHints = Array.from(this.form.querySelectorAll('div, span')) - .filter( - (div) => - div.textContent != null && - div.textContent.trim() !== '' && - window.getComputedStyle(div).display !== 'none' && - window.getComputedStyle(div).visibility !== 'hidden', - ) - .some((div) => div.textContent && safeRegexTest(this.matching.getDDGMatcherRegex('passwordHintsRegex'), div.textContent)); - if (hasPasswordHints) { - this.increaseSignalBy(3, 'Password hints'); + if (this.form.textContent) { + const hasPasswordHints = safeRegexTest(this.matching.getDDGMatcherRegex('passwordHintsRegex'), this.form.textContent, 200); + if (hasPasswordHints) { + this.increaseSignalBy(5, 'Password hints'); + } } } @@ -401,6 +321,11 @@ class FormAnalyzer { // Check page title this.evaluatePageTitle(); + // Evaluate form's input elements + this.form.querySelectorAll(this.matching.cssSelector('formInputsSelector')).forEach((input) => { + this.evaluateElAttributes(input, 1, true); + }); + // Check form attributes this.evaluateElAttributes(this.form); @@ -428,7 +353,6 @@ class FormAnalyzer { // If we can't decide at this point, try reading form headers and password hints if (this.areLoginOrSignupSignalsWeak()) { this.evaluatePasswordHints(); - this.evaluateFormHeaderSignals(); } // If we can't decide at this point, try reading page headings diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 99a11fdc4..019abe639 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -518,8 +518,8 @@ function isFormLikelyToBeUsedAsPageWrapper(form) { * @param {String} string * @returns {boolean} */ -function safeRegexTest(regex, string) { - if (!string || !regex || string.length > constants.TEXT_LENGTH_CUTOFF) return false; +function safeRegexTest(regex, string, textLengthCutoff = constants.TEXT_LENGTH_CUTOFF) { + if (!string || !regex || string.length > textLengthCutoff) return false; return regex.test(string); } diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 516872a06..7c41302c0 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -11004,9 +11004,6 @@ class FormAnalyzer { */ this.signals = []; - // Analyse the input that was passed. This is pretty arbitrary, but historically it's been working nicely. - this.evaluateElAttributes(input, 1, true); - // If we have a meaningful container (a form), check that, otherwise check the whole page if (form !== input) { this.evaluateForm(); @@ -11103,7 +11100,7 @@ class FormAnalyzer { return this; } const signupRegexToUse = this.matching.getDDGMatcherRegex(shouldBeConservative ? 'conservativeSignupRegex' : 'signupRegex'); - const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new.?password/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); + const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new?.(password|username)/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); // In some cases a login match means the login is somewhere else, i.e. when a link points outside if (shouldFlip) { @@ -11185,72 +11182,12 @@ class FormAnalyzer { } }); } - - /** - * Takes an element and returns all its children that are text-only nodes - * @param {HTMLElement|Element} element - * @param {number} maxDepth - * @param {number} currentDepth - * @returns {HTMLElement[]|Element[]} - */ - getElementsWithOnlyTextChild(element) { - let maxDepth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; - let currentDepth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - // Array to collect elements with only text child nodes - const elementsWithTextChild = []; - - // If we've reached the max depth, stop traversing further - if (currentDepth > maxDepth) { - return elementsWithTextChild; - } - - // Check if the current element has only one text child node - if (element.nodeType === Node.ELEMENT_NODE) { - const childNodes = element.childNodes; - if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) { - elementsWithTextChild.push(element); - } - } - - // Recurse through each child element and collect matching elements - for (const child of element.children) { - // Recursively get elements from child elements, increasing depth by 1 - elementsWithTextChild.push(...this.getElementsWithOnlyTextChild(child, maxDepth, currentDepth + 1)); - } - return elementsWithTextChild; - } - evaluateFormHeaderSignals() { - const isVisuallyBeforeForm = el => el.getBoundingClientRect().top < this.form.getBoundingClientRect().top; - const isHeaderSized = el => { - if (el instanceof HTMLHeadingElement) { - return true; - } - const computedStyle = window.getComputedStyle(el); - const fontWeight = computedStyle.fontWeight; - const isRelativelyTall = parseFloat(computedStyle.height) / this.form.clientHeight > 0.1; - if (isRelativelyTall && (fontWeight === 'bold' || parseFloat(fontWeight) >= 700)) { - return true; - } - }; - const allSiblings = Array.from(this.form.parentElement?.children ?? []).filter(element => element !== this.form).map(element => this.getElementsWithOnlyTextChild(element)).flat(); - if (allSiblings.length === 0) return false; - allSiblings.forEach(element => { - if (element instanceof HTMLElement && isVisuallyBeforeForm(element) && isHeaderSized(element)) { - const string = element.textContent?.trim(); - if (string) { - if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?in|log[- ]?in)$/i, string)) { - return this.decreaseSignalBy(3, 'Strong login signal above form'); - } else if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?up)$/i, string)) { - return this.increaseSignalBy(3, 'Strong signup signal above form'); - } - } - } - }); - } evaluatePasswordHints() { - const hasPasswordHints = Array.from(this.form.querySelectorAll('div, span')).filter(div => div.textContent != null && div.textContent.trim() !== '' && window.getComputedStyle(div).display !== 'none' && window.getComputedStyle(div).visibility !== 'hidden').some(div => div.textContent && (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), div.textContent)); - if (hasPasswordHints) { - this.increaseSignalBy(3, 'Password hints'); + if (this.form.textContent) { + const hasPasswordHints = (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), this.form.textContent, 200); + if (hasPasswordHints) { + this.increaseSignalBy(5, 'Password hints'); + } } } @@ -11344,6 +11281,11 @@ class FormAnalyzer { // Check page title this.evaluatePageTitle(); + // Evaluate form's input elements + this.form.querySelectorAll(this.matching.cssSelector('formInputsSelector')).forEach(input => { + this.evaluateElAttributes(input, 1, true); + }); + // Check form attributes this.evaluateElAttributes(this.form); @@ -11370,7 +11312,6 @@ class FormAnalyzer { // If we can't decide at this point, try reading form headers and password hints if (this.areLoginOrSignupSignalsWeak()) { this.evaluatePasswordHints(); - this.evaluateFormHeaderSignals(); } // If we can't decide at this point, try reading page headings @@ -17800,7 +17741,8 @@ function isFormLikelyToBeUsedAsPageWrapper(form) { * @returns {boolean} */ function safeRegexTest(regex, string) { - if (!string || !regex || string.length > _constants.constants.TEXT_LENGTH_CUTOFF) return false; + let textLengthCutoff = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _constants.constants.TEXT_LENGTH_CUTOFF; + if (!string || !regex || string.length > textLengthCutoff) return false; return regex.test(string); } diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 25c5d33e3..91552d992 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -6641,9 +6641,6 @@ class FormAnalyzer { */ this.signals = []; - // Analyse the input that was passed. This is pretty arbitrary, but historically it's been working nicely. - this.evaluateElAttributes(input, 1, true); - // If we have a meaningful container (a form), check that, otherwise check the whole page if (form !== input) { this.evaluateForm(); @@ -6740,7 +6737,7 @@ class FormAnalyzer { return this; } const signupRegexToUse = this.matching.getDDGMatcherRegex(shouldBeConservative ? 'conservativeSignupRegex' : 'signupRegex'); - const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new.?password/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); + const matchesSignup = (0, _autofillUtils.safeRegexTest)(/new?.(password|username)/i, string) || (0, _autofillUtils.safeRegexTest)(signupRegexToUse, string); // In some cases a login match means the login is somewhere else, i.e. when a link points outside if (shouldFlip) { @@ -6822,72 +6819,12 @@ class FormAnalyzer { } }); } - - /** - * Takes an element and returns all its children that are text-only nodes - * @param {HTMLElement|Element} element - * @param {number} maxDepth - * @param {number} currentDepth - * @returns {HTMLElement[]|Element[]} - */ - getElementsWithOnlyTextChild(element) { - let maxDepth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; - let currentDepth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - // Array to collect elements with only text child nodes - const elementsWithTextChild = []; - - // If we've reached the max depth, stop traversing further - if (currentDepth > maxDepth) { - return elementsWithTextChild; - } - - // Check if the current element has only one text child node - if (element.nodeType === Node.ELEMENT_NODE) { - const childNodes = element.childNodes; - if (childNodes.length === 1 && childNodes[0].nodeType === Node.TEXT_NODE) { - elementsWithTextChild.push(element); - } - } - - // Recurse through each child element and collect matching elements - for (const child of element.children) { - // Recursively get elements from child elements, increasing depth by 1 - elementsWithTextChild.push(...this.getElementsWithOnlyTextChild(child, maxDepth, currentDepth + 1)); - } - return elementsWithTextChild; - } - evaluateFormHeaderSignals() { - const isVisuallyBeforeForm = el => el.getBoundingClientRect().top < this.form.getBoundingClientRect().top; - const isHeaderSized = el => { - if (el instanceof HTMLHeadingElement) { - return true; - } - const computedStyle = window.getComputedStyle(el); - const fontWeight = computedStyle.fontWeight; - const isRelativelyTall = parseFloat(computedStyle.height) / this.form.clientHeight > 0.1; - if (isRelativelyTall && (fontWeight === 'bold' || parseFloat(fontWeight) >= 700)) { - return true; - } - }; - const allSiblings = Array.from(this.form.parentElement?.children ?? []).filter(element => element !== this.form).map(element => this.getElementsWithOnlyTextChild(element)).flat(); - if (allSiblings.length === 0) return false; - allSiblings.forEach(element => { - if (element instanceof HTMLElement && isVisuallyBeforeForm(element) && isHeaderSized(element)) { - const string = element.textContent?.trim(); - if (string) { - if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?in|log[- ]?in)$/i, string)) { - return this.decreaseSignalBy(3, 'Strong login signal above form'); - } else if ((0, _autofillUtils.safeRegexTest)(/^(sign[- ]?up)$/i, string)) { - return this.increaseSignalBy(3, 'Strong signup signal above form'); - } - } - } - }); - } evaluatePasswordHints() { - const hasPasswordHints = Array.from(this.form.querySelectorAll('div, span')).filter(div => div.textContent != null && div.textContent.trim() !== '' && window.getComputedStyle(div).display !== 'none' && window.getComputedStyle(div).visibility !== 'hidden').some(div => div.textContent && (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), div.textContent)); - if (hasPasswordHints) { - this.increaseSignalBy(3, 'Password hints'); + if (this.form.textContent) { + const hasPasswordHints = (0, _autofillUtils.safeRegexTest)(this.matching.getDDGMatcherRegex('passwordHintsRegex'), this.form.textContent, 200); + if (hasPasswordHints) { + this.increaseSignalBy(5, 'Password hints'); + } } } @@ -6981,6 +6918,11 @@ class FormAnalyzer { // Check page title this.evaluatePageTitle(); + // Evaluate form's input elements + this.form.querySelectorAll(this.matching.cssSelector('formInputsSelector')).forEach(input => { + this.evaluateElAttributes(input, 1, true); + }); + // Check form attributes this.evaluateElAttributes(this.form); @@ -7007,7 +6949,6 @@ class FormAnalyzer { // If we can't decide at this point, try reading form headers and password hints if (this.areLoginOrSignupSignalsWeak()) { this.evaluatePasswordHints(); - this.evaluateFormHeaderSignals(); } // If we can't decide at this point, try reading page headings @@ -13437,7 +13378,8 @@ function isFormLikelyToBeUsedAsPageWrapper(form) { * @returns {boolean} */ function safeRegexTest(regex, string) { - if (!string || !regex || string.length > _constants.constants.TEXT_LENGTH_CUTOFF) return false; + let textLengthCutoff = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _constants.constants.TEXT_LENGTH_CUTOFF; + if (!string || !regex || string.length > textLengthCutoff) return false; return regex.test(string); } diff --git a/test-forms/index.json b/test-forms/index.json index 02daabde9..b7f6ee7aa 100644 --- a/test-forms/index.json +++ b/test-forms/index.json @@ -308,7 +308,7 @@ { "html": "weblogin_utoronto_ca_login.html", "generated": true, "title": "weblogin | University of Toronto", "comment": "rank: 1280" }, { "html": "customerportal_mastercard_com_login.html", "generated": true, "title": "Sign in - Mastercard", "comment": "rank: 1362" }, { "html": "www_khanacademy_org_login.html", "generated": true, "title": "Khan Academy", "comment": "rank: 1379" }, - { "html": "accounts_hindustantimes_com_login.html", "generated": true, "title": "Livemint Subscription Plan and Pricing", "comment": "rank: 1467" }, + { "html": "accounts_hindustantimes_com_login.html", "generated": true, "title": "Livemint Subscription Plan and Pricing", "comment": "rank: 1467", "expectedFailures": ["emailAddress"] }, { "html": "rule34_us_signup.html", "generated": true, "title": "Rule34 - If it exists, there is porn of it", "comment": "rank: 1479" }, { "html": "gsw_gda_pl_login.html", "generated": true, "title": "Log in | Gdańska Szkoła Wyższa", "comment": "rank: 1504" }, { "html": "secure_xserver_ne_jp_login.html", "generated": true, "title": "Xserverアカウント - ログイン | レンタルサーバーならエックスサーバー", "comment": "rank: 1523" },