From 09f36e7789a665f3235ddc5b4d486e6e0195e7a1 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Mon, 25 Sep 2023 04:12:43 -0500 Subject: [PATCH] Update autofill to 8.4.0 (#3562) Task/Issue URL: https://app.asana.com/0/1205538042573894/1205538042573894 Autofill Release: https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/8.4.0 ## Description Updates Autofill to version [8.4.0](https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/8.4.0). ### Autofill 8.4.0 release notes ## What's Changed * Improve non-English matching by @GioSensation in https://github.com/duckduckgo/duckduckgo-autofill/pull/374 **Full Changelog**: https://github.com/duckduckgo/duckduckgo-autofill/compare/8.3.0...8.4.0 ## Steps to test This release has been tested during autofill development. For smoke test steps see [this task](https://app.asana.com/0/1198964220583541/1200583647142330/f). Co-authored-by: GioSensation --- .../autofill/dist/autofill-debug.js | 893 ++++++++++++------ .../@duckduckgo/autofill/dist/autofill.js | 893 ++++++++++++------ .../autofill/dist/shared-credentials.json | 6 + package-lock.json | 4 +- package.json | 2 +- 5 files changed, 1213 insertions(+), 585 deletions(-) diff --git a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js index b830b634dff4..9b980676b8a7 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js @@ -8147,15 +8147,8 @@ class ExtensionInterface extends _InterfacePrototype.default { return null; } - removeAutofillUIFromPage() { - var _this$activeForm2; - - super.removeAutofillUIFromPage(); - (_this$activeForm2 = this.activeForm) === null || _this$activeForm2 === void 0 ? void 0 : _this$activeForm2.removeAllDecorations(); - } - async resetAutofillUI(callback) { - this.removeAutofillUIFromPage(); + this.removeAutofillUIFromPage('Resetting autofill.'); await this.setupAutofill(); if (callback) await callback(); this.uiController = this.createUIController(); @@ -8193,7 +8186,7 @@ class ExtensionInterface extends _InterfacePrototype.default { switch (this.getActiveTooltipType()) { case TOOLTIP_TYPES.EmailProtection: { - var _this$activeForm3; + var _this$activeForm2; this._scannerCleanup = this.scanner.init(); this.addLogoutListener(() => { @@ -8208,12 +8201,12 @@ class ExtensionInterface extends _InterfacePrototype.default { } }); - if ((_this$activeForm3 = this.activeForm) !== null && _this$activeForm3 !== void 0 && _this$activeForm3.activeInput) { - var _this$activeForm4; + if ((_this$activeForm2 = this.activeForm) !== null && _this$activeForm2 !== void 0 && _this$activeForm2.activeInput) { + var _this$activeForm3; this.attachTooltip({ form: this.activeForm, - input: (_this$activeForm4 = this.activeForm) === null || _this$activeForm4 === void 0 ? void 0 : _this$activeForm4.activeInput, + input: (_this$activeForm3 = this.activeForm) === null || _this$activeForm3 === void 0 ? void 0 : _this$activeForm3.activeInput, click: null, trigger: 'postSignup', triggerMetaData: { @@ -8449,7 +8442,7 @@ class InterfacePrototype { /** @type {boolean} */ - /** @type {(()=>void) | null} */ + /** @type {((reason, ...rest) => void) | null} */ /** * @param {GlobalConfig} config @@ -8523,12 +8516,16 @@ class InterfacePrototype { createUIController() { return new _NativeUIController.NativeUIController(); } + /** + * @param {string} reason + */ - removeAutofillUIFromPage() { + + removeAutofillUIFromPage(reason) { var _this$uiController, _this$_scannerCleanup; (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.destroy(); - (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this); + (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this, reason); } get hasLocalAddresses() { @@ -8708,7 +8705,7 @@ class InterfacePrototype { await this.postInit(); if (this.settings.featureToggles.credentials_saving) { - (0, _initFormSubmissionsApi.initFormSubmissionsApi)(this.scanner.forms); + (0, _initFormSubmissionsApi.initFormSubmissionsApi)(this.scanner.forms, this.scanner.matching); } } /** @@ -8768,7 +8765,7 @@ class InterfacePrototype { postInit() { const cleanup = this.scanner.init(); this.addLogoutListener(() => { - cleanup(); + cleanup('Logged out'); if (this.globalConfig.isDDGDomain) { (0, _autofillUtils.notifyWebApp)({ @@ -9857,16 +9854,17 @@ Object.defineProperty(exports, "__esModule", { }); exports.initFormSubmissionsApi = initFormSubmissionsApi; -var _selectorsCss = require("../Form/selectors-css.js"); - var _autofillUtils = require("../autofill-utils.js"); +var _labelUtil = require("../Form/label-util.js"); + /** * This is a single place to contain all functionality relating to form submission detection * * @param {Map} forms + * @param {import("../Form/matching").Matching} matching */ -function initFormSubmissionsApi(forms) { +function initFormSubmissionsApi(forms, matching) { /** * Global submit events */ @@ -9902,16 +9900,16 @@ function initFormSubmissionsApi(forms) { matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler('global pointerdown event + matching form'); if (!matchingForm) { - var _event$target, _event$target2; + var _event$target, _matching$getDDGMatch, _event$target2; - const selector = _selectorsCss.SUBMIT_BUTTON_SELECTOR + ', a[href="#"], a[href^=javascript], *[onclick]'; // check if the click happened on a button + const selector = matching.cssSelector('submitButtonSelector') + ', a[href="#"], a[href^=javascript], *[onclick], [class*=button i]'; // check if the click happened on a button const button = /** @type HTMLElement */ (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(selector); if (!button) return; - const text = (0, _autofillUtils.getText)(button); - const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text); + const text = (0, _autofillUtils.getTextShallow)(button) || (0, _labelUtil.extractElementStrings)(button).join(' '); + const hasRelevantText = (_matching$getDDGMatch = matching.getDDGMatcherRegex('submitButtonRegex')) === null || _matching$getDDGMatch === void 0 ? void 0 : _matching$getDDGMatch.test(text); if (hasRelevantText && text.length < 25) { // check if there's a form with values @@ -9954,7 +9952,7 @@ function initFormSubmissionsApi(forms) { }); } -},{"../Form/selectors-css.js":44,"../autofill-utils.js":63}],31:[function(require,module,exports){ +},{"../Form/label-util.js":39,"../autofill-utils.js":63}],31:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10220,6 +10218,10 @@ class Form { return this.formAnalyzer.isHybrid; } + get isCCForm() { + return this.formAnalyzer.isCCForm(); + } + logFormInfo() { if (!(0, _autofillUtils.shouldLog)()) return; console.log("Form type: %c".concat(this.getFormType()), 'font-weight: bold'); @@ -10317,10 +10319,10 @@ class Form { formValues.credentials.username = formValues.identities.phone; } else { // If we still don't have a username, try scanning the form's text for an email address - this.form.querySelectorAll('*:not(select):not(option)').forEach(el => { + this.form.querySelectorAll(this.matching.cssSelector('safeUniversalSelector')).forEach(el => { var _elText$match; - const elText = (0, _autofillUtils.getText)(el); // Ignore long texts to avoid false positives + const elText = (0, _autofillUtils.getTextShallow)(el); // Ignore long texts to avoid false positives if (elText.length > 70) return; const emailOrUsername = (_elText$match = elText.match( // https://www.emailregex.com/ @@ -10476,18 +10478,25 @@ class Form { destroy() { this.removeAllDecorations(); this.removeTooltip(); + this.forgetAllInputs(); this.mutObs.disconnect(); this.matching.clear(); this.intObs = null; } categorizeInputs() { - const selector = this.matching.cssSelector('FORM_INPUTS_SELECTOR'); + const selector = this.matching.cssSelector('formInputsSelector'); if (this.form.matches(selector)) { this.addInput(this.form); } else { - const foundInputs = this.form.querySelectorAll(selector); + let foundInputs = this.form.querySelectorAll(selector); // If the markup is broken form.querySelectorAll may not return the fields, so we select from the parent + + if (foundInputs.length === 0 && this.form instanceof HTMLFormElement && this.form.length > 0) { + var _this$form$parentElem; + + foundInputs = ((_this$form$parentElem = this.form.parentElement) === null || _this$form$parentElem === void 0 ? void 0 : _this$form$parentElem.querySelectorAll(selector)) || foundInputs; + } if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); @@ -10502,11 +10511,11 @@ class Form { } get submitButtons() { - const selector = this.matching.cssSelector('SUBMIT_BUTTON_SELECTOR'); + const selector = this.matching.cssSelector('submitButtonSelector'); const allButtons = /** @type {HTMLElement[]} */ [...this.form.querySelectorAll(selector)]; - return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); + return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn, this.matching) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } attemptSubmissionIfNeeded() { @@ -10578,6 +10587,7 @@ class Form { const opts = { isLogin: this.isLogin, isHybrid: this.isHybrid, + isCCForm: this.isCCForm, hasCredentials: Boolean((_this$device$settings = this.device.settings.availableInputTypes.credentials) === null || _this$device$settings === void 0 ? void 0 : _this$device$settings.username), supportsIdentitiesAutofill: this.device.settings.featureToggles.inputType_identities }; @@ -11008,13 +11018,6 @@ var _autofillUtils = require("../autofill-utils.js"); function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -const loginRegex = new RegExp(/sign(ing)?.?in(?!g)|log.?(i|o)n|log.?out|unsubscri|(forgot(ten)?|reset) (your )?password|password (forgotten|lost)|unlock|logged in as|mfa-submit-form/i); -const signupRegex = new RegExp(/sign(ing)?.?up|join|\bregist(er|ration)|newsletter|\bsubscri(be|ption)|contact|create|start|enroll|settings|preferences|profile|update|checkout|guest|purchase|buy|order|schedule|estimate|request|new.?customer|(confirm|retype|repeat) password|password confirm?/i); -const conservativeSignupRegex = new RegExp(/sign.?up|join|register|enroll|newsletter|subscri(be|ption)|settings|preferences|profile|update/i); -const strictSignupRegex = new RegExp(/sign.?up|join|register|(create|new).+account|enroll|settings|preferences|profile|update/i); -const resetPasswordLink = new RegExp(/(forgot(ten)?|reset|don't remember) (your )?password|password forgotten/i); -const loginProvidersRegex = new RegExp(/ with /i); - class FormAnalyzer { /** @type HTMLElement */ @@ -11030,6 +11033,8 @@ class FormAnalyzer { _defineProperty(this, "matching", void 0); + _defineProperty(this, "_isCCForm", undefined); + this.form = form; this.matching = matching || new _matching.Matching(_matchingConfiguration.matchingConfiguration); /** @@ -11128,6 +11133,8 @@ class FormAnalyzer { updateSignal(_ref) { + var _this$matching$getDDG, _this$matching$getDDG2, _this$matching$getDDG3; + let { string, strength, @@ -11136,15 +11143,15 @@ class FormAnalyzer { shouldCheckUnifiedForm = false, shouldBeConservative = false } = _ref; - const matchesLogin = /current.?password/i.test(string) || loginRegex.test(string) || resetPasswordLink.test(string); // Check explicitly for unified login/signup forms + const matchesLogin = /current.?password/i.test(string) || ((_this$matching$getDDG = this.matching.getDDGMatcherRegex('loginRegex')) === null || _this$matching$getDDG === void 0 ? void 0 : _this$matching$getDDG.test(string)) || ((_this$matching$getDDG2 = this.matching.getDDGMatcherRegex('resetPasswordLink')) === null || _this$matching$getDDG2 === void 0 ? void 0 : _this$matching$getDDG2.test(string)); // Check explicitly for unified login/signup forms - if (shouldCheckUnifiedForm && matchesLogin && strictSignupRegex.test(string)) { + if (shouldCheckUnifiedForm && matchesLogin && (_this$matching$getDDG3 = this.matching.getDDGMatcherRegex('conservativeSignupRegex')) !== null && _this$matching$getDDG3 !== void 0 && _this$matching$getDDG3.test(string)) { this.increaseHybridSignal(strength, signalType); return this; } - const signupRegexToUse = shouldBeConservative ? conservativeSignupRegex : signupRegex; - const matchesSignup = /new.?password/i.test(string) || signupRegexToUse.test(string); // In some cases a login match means the login is somewhere else, i.e. when a link points outside + const signupRegexToUse = this.matching.getDDGMatcherRegex(shouldBeConservative ? 'conservativeSignupRegex' : 'signupRegex'); + const matchesSignup = /new.?password/i.test(string) || (signupRegexToUse === null || signupRegexToUse === void 0 ? void 0 : signupRegexToUse.test(string)); // In some cases a login match means the login is somewhere else, i.e. when a link points outside if (shouldFlip) { if (matchesLogin) this.increaseSignalBy(strength, signalType); @@ -11172,6 +11179,24 @@ class FormAnalyzer { }); } + evaluateUrl() { + var _this$matching$getDDG4, _this$matching$getDDG5; + + const path = window.location.pathname; + const matchesLogin = (_this$matching$getDDG4 = this.matching.getDDGMatcherRegex('loginRegex')) === null || _this$matching$getDDG4 === void 0 ? void 0 : _this$matching$getDDG4.test(path); + const matchesSignup = (_this$matching$getDDG5 = this.matching.getDDGMatcherRegex('conservativeSignupRegex')) === null || _this$matching$getDDG5 === void 0 ? void 0 : _this$matching$getDDG5.test(path); // If the url matches both, do nothing: the signal is probably confounding + + if (matchesLogin && matchesSignup) return; + + if (matchesLogin) { + this.decreaseSignalBy(1, 'url matches login'); + } + + if (matchesSignup) { + this.increaseSignalBy(1, 'url matches signup'); + } + } + evaluatePageTitle() { const pageTitle = document.title; this.updateSignal({ @@ -11203,7 +11228,7 @@ class FormAnalyzer { this.evaluatePageTitle(); this.evaluatePageHeadings(); // Check for submit buttons - const buttons = document.querySelectorAll(this.matching.cssSelector('SUBMIT_BUTTON_SELECTOR')); + const buttons = document.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); buttons.forEach(button => { // if the button has a form, it's not related to our input, because our input has no form here if (button instanceof HTMLButtonElement) { @@ -11216,7 +11241,7 @@ class FormAnalyzer { } evaluateElement(el) { - const string = (0, _autofillUtils.getText)(el); + const string = (0, _autofillUtils.getTextShallow)(el); if (el.matches(this.matching.cssSelector('password'))) { // These are explicit signals by the web author, so we weigh them heavily @@ -11225,12 +11250,13 @@ class FormAnalyzer { strength: 5, signalType: "explicit: ".concat(el.getAttribute('autocomplete')) }); + return; } // check button contents - if (el.matches(this.matching.cssSelector('SUBMIT_BUTTON_SELECTOR'))) { + if (el.matches(this.matching.cssSelector('submitButtonSelector') + ', *[class*=button]')) { // If we're confident this is the submit button, it's a stronger signal - let likelyASubmit = (0, _autofillUtils.isLikelyASubmitButton)(el); + let likelyASubmit = (0, _autofillUtils.isLikelyASubmitButton)(el, this.matching); if (likelyASubmit) { this.form.querySelectorAll('input[type=submit], button[type=submit]').forEach(submit => { @@ -11247,21 +11273,27 @@ class FormAnalyzer { strength, signalType: "submit: ".concat(string) }); + return; } // if an external link matches one of the regexes, we assume the match is not pertinent to the current form if (el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]')) { + var _this$matching$getDDG6, _this$matching$getDDG7; + let shouldFlip = true; + let strength = 1; // Don't flip forgotten password links - if (resetPasswordLink.test(string) || // Don't flip forgotten password links - loginProvidersRegex.test(string) // Don't flip login providers links - ) { + if ((_this$matching$getDDG6 = this.matching.getDDGMatcherRegex('resetPasswordLink')) !== null && _this$matching$getDDG6 !== void 0 && _this$matching$getDDG6.test(string)) { + shouldFlip = false; + strength = 3; + } else if ((_this$matching$getDDG7 = this.matching.getDDGMatcherRegex('loginProvidersRegex')) !== null && _this$matching$getDDG7 !== void 0 && _this$matching$getDDG7.test(string)) { + // Don't flip login providers links shouldFlip = false; } this.updateSignal({ string, - strength: 1, + strength, signalType: "external link: ".concat(string), shouldFlip }); @@ -11282,12 +11314,14 @@ class FormAnalyzer { } evaluateForm() { - // Check page title + // Check page url + this.evaluateUrl(); // Check page title + this.evaluatePageTitle(); // Check form attributes - this.evaluateElAttributes(this.form); // Check form contents (skip select and option because they contain too much noise) + this.evaluateElAttributes(this.form); // Check form contents (noisy elements are skipped with the safeUniversalSelector) - this.form.querySelectorAll('*:not(select):not(option):not(script)').forEach(el => { + this.form.querySelectorAll(this.matching.cssSelector('safeUniversalSelector')).forEach(el => { // Check if element is not hidden. Note that we can't use offsetHeight // nor intersectionObserver, because the element could be outside the // viewport or its parent hidden @@ -11295,7 +11329,7 @@ class FormAnalyzer { if (displayValue !== 'none') this.evaluateElement(el); }); // A form with many fields is unlikely to be a login form - const relevantFields = this.form.querySelectorAll(this.matching.cssSelector('GENERIC_TEXT_FIELD')); + const relevantFields = this.form.querySelectorAll(this.matching.cssSelector('genericTextField')); if (relevantFields.length >= 4) { this.increaseSignalBy(relevantFields.length * 1.5, 'many fields: it is probably not a login'); @@ -11308,6 +11342,52 @@ class FormAnalyzer { return this; } + /** @type {undefined|boolean} */ + + + /** + * Tries to infer if it's a credit card form + * @returns {boolean} + */ + isCCForm() { + var _formEl$textContent; + + if (this._isCCForm !== undefined) return this._isCCForm; + const formEl = this.form; + const ccFieldSelector = this.matching.joinCssSelectors('cc'); + + if (!ccFieldSelector) { + this._isCCForm = false; + return this._isCCForm; + } + + const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence + + if (hasCCSelectorChild) { + this._isCCForm = true; + return this._isCCForm; + } // Read form attributes to find a signal + + + const hasCCAttribute = [...formEl.attributes].some(_ref3 => { + let { + name, + value + } = _ref3; + return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); + }); + + if (hasCCAttribute) { + this._isCCForm = true; + return this._isCCForm; + } // Match form textContent against common cc fields (includes hidden labels) + + + const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives + + this._isCCForm = Boolean(textMatches && textMatches.length > 1); + return this._isCCForm; + } } @@ -11911,7 +11991,7 @@ const FOUR_DIGIT_YEAR_REGEX = /(\D)\1{3}|\d{4}/i; */ const formatCCYear = (input, year, form) => { - const selector = form.matching.cssSelector('FORM_INPUTS_SELECTOR'); + const selector = form.matching.cssSelector('formInputsSelector'); if (input.maxLength === 4 || (0, _matching.checkPlaceholderAndLabels)(input, FOUR_DIGIT_YEAR_REGEX, form.form, selector)) return year; return "".concat(Number(year) - 2000); }; @@ -11932,7 +12012,7 @@ const getUnifiedExpiryDate = (input, month, year, form) => { const formattedYear = formatCCYear(input, year, form); const paddedMonth = "".concat(month).padStart(2, '0'); - const cssSelector = form.matching.cssSelector('FORM_INPUTS_SELECTOR'); + const cssSelector = form.matching.cssSelector('formInputsSelector'); const separator = ((_matchInPlaceholderAn = (0, _matching.matchInPlaceholderAndLabels)(input, DATE_SEPARATOR_REGEX, form.form, cssSelector)) === null || _matchInPlaceholderAn === void 0 ? void 0 : (_matchInPlaceholderAn2 = _matchInPlaceholderAn.groups) === null || _matchInPlaceholderAn2 === void 0 ? void 0 : _matchInPlaceholderAn2.separator) || '/'; return "".concat(paddedMonth).concat(separator).concat(formattedYear); }; @@ -12627,22 +12707,24 @@ exports.isFieldDecorated = isFieldDecorated; Object.defineProperty(exports, "__esModule", { value: true }); -exports.extractElementStrings = void 0; +exports.extractElementStrings = exports.EXCLUDED_TAGS = void 0; var _matching = require("./matching.js"); -const EXCLUDED_TAGS = ['SCRIPT', 'NOSCRIPT', 'OPTION', 'STYLE']; +const EXCLUDED_TAGS = ['BR', 'SCRIPT', 'NOSCRIPT', 'OPTION', 'STYLE']; /** * Extract all strings of an element's children to an array. * "element.textContent" is a string which is merged of all children nodes, * which can cause issues with things like script tags etc. * - * @param {HTMLElement} element + * @param {Element} element * A DOM element to be extracted. * @returns {string[]} * All strings in an element. */ +exports.EXCLUDED_TAGS = EXCLUDED_TAGS; + const extractElementStrings = element => { const strings = new Set(); @@ -12702,11 +12784,7 @@ Object.defineProperty(exports, "__esModule", { }); exports.matchingConfiguration = void 0; -var css = _interopRequireWildcard(require("./selectors-css.js")); - -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +var _selectorsCss = require("./selectors-css.js"); /** * This is here to mimic what Remote Configuration might look like @@ -12718,11 +12796,17 @@ const matchingConfiguration = { /** @type {MatcherConfiguration} */ matchers: { fields: { - email: { - type: 'email', + unknown: { + type: 'unknown', strategies: { - cssSelector: 'email', - ddgMatcher: 'email', + ddgMatcher: 'unknown' + } + }, + emailAddress: { + type: 'emailAddress', + strategies: { + cssSelector: 'emailAddress', + ddgMatcher: 'emailAddress', vendorRegex: 'email' } }, @@ -12898,7 +12982,8 @@ const matchingConfiguration = { } }, lists: { - email: ['email'], + unknown: ['unknown'], + emailAddress: ['emailAddress'], password: ['password'], username: ['username'], cc: ['cardName', 'cardNumber', 'cardSecurityCode', 'expirationMonth', 'expirationYear', 'expiration'], @@ -12908,55 +12993,50 @@ const matchingConfiguration = { strategies: { /** @type {CssSelectorConfiguration} */ cssSelector: { - selectors: { - // Generic - FORM_INPUTS_SELECTOR: css.__secret_do_not_use.FORM_INPUTS_SELECTOR, - SUBMIT_BUTTON_SELECTOR: css.__secret_do_not_use.SUBMIT_BUTTON_SELECTOR, - GENERIC_TEXT_FIELD: css.__secret_do_not_use.GENERIC_TEXT_FIELD, - // user - email: css.__secret_do_not_use.email, - password: css.__secret_do_not_use.password, - username: css.__secret_do_not_use.username, - // CC - cardName: css.__secret_do_not_use.cardName, - cardNumber: css.__secret_do_not_use.cardNumber, - cardSecurityCode: css.__secret_do_not_use.cardSecurityCode, - expirationMonth: css.__secret_do_not_use.expirationMonth, - expirationYear: css.__secret_do_not_use.expirationYear, - expiration: css.__secret_do_not_use.expiration, - // Identities - firstName: css.__secret_do_not_use.firstName, - middleName: css.__secret_do_not_use.middleName, - lastName: css.__secret_do_not_use.lastName, - fullName: css.__secret_do_not_use.fullName, - phone: css.__secret_do_not_use.phone, - addressStreet: css.__secret_do_not_use.addressStreet1, - addressStreet2: css.__secret_do_not_use.addressStreet2, - addressCity: css.__secret_do_not_use.addressCity, - addressProvince: css.__secret_do_not_use.addressProvince, - addressPostalCode: css.__secret_do_not_use.addressPostalCode, - addressCountryCode: css.__secret_do_not_use.addressCountryCode, - birthdayDay: css.__secret_do_not_use.birthdayDay, - birthdayMonth: css.__secret_do_not_use.birthdayMonth, - birthdayYear: css.__secret_do_not_use.birthdayYear - } + selectors: _selectorsCss.selectors }, /** @type {DDGMatcherConfiguration} */ ddgMatcher: { matchers: { - email: { - match: '.mail\\b|apple.?id', + unknown: { + match: 'search|filter|subject|title|captcha|mfa|2fa|two factor|one-time|otp' + // Italian + '|cerca|filtr|oggetto|titolo|(due|più) fattori' + // German + '|suche|filtern|betreff' + // Dutch + '|zoeken|filter|onderwerp|titel' + // French + '|chercher|filtrer|objet|titre|authentification multifacteur|double authentification|à usage unique' + // Spanish + '|busca|busqueda|filtra|dos pasos|un solo uso' + // Swedish + '|sök|filter|ämne|multifaktorsautentisering|tvåfaktorsautentisering|två.?faktor|engångs', + skip: 'phone|mobile|email|password' + }, + emailAddress: { + match: '.mail\\b|apple.?id' + // Italian + '|posta elettronica' + // Dutch + '|e.?mailadres' + // Spanish + '|correo electr|correo-e|^correo$' + // Swedish + '|\\be.?post|e.?postadress', skip: 'phone|(first.?|last.?)name|number|code', forceUnknown: 'search|filter|subject|title|\btab\b|otp' }, password: { - match: 'password', + match: 'password' + // German + '|passwort|kennwort' + // Dutch + '|wachtwoord' + // French + '|mot de passe' + // Spanish + '|clave|contraseña' + // Swedish + '|lösenord', skip: 'email|one-time|error|hint', - forceUnknown: 'captcha|mfa|2fa|two factor|otp' + forceUnknown: 'captcha|mfa|2fa|two factor|otp|pin' }, username: { - match: '(user|account|log(i|o)n|net)((.)?(name|i.?d.?|log(i|o)n).?)?(.?((or|/).+|\\*|:))?$|benutzername', + match: '(user|account|log(i|o)n|net)((.)?(name|i.?d.?|log(i|o)n).?)?(.?((or|/).+|\\*|:)( required)?)?$' + // Italian + '|(nome|id|login).?utente|(nome|id) (dell.)?account|codice cliente' + // German + '|nutzername|anmeldename' + // Dutch + '|gebruikersnaam' + // French + '|nom d.utilisateur|identifiant|pseudo' + // Spanish + '|usuari|cuenta|identificador|apodo' + // in Spanish dni and nie stand for id number, often used as username + '|\\bdni\\b|\\bnie\\b| del? documento|documento de identidad' + // Swedish + '|användarnamn|kontonamn|användar-id', skip: 'phone', forceUnknown: 'search|policy' }, @@ -12966,6 +13046,7 @@ const matchingConfiguration = { }, cardNumber: { match: 'card.*number|number.*card', + skip: 'phone', forceUnknown: 'plus' }, cardSecurityCode: { @@ -12981,33 +13062,37 @@ const matchingConfiguration = { }, expiration: { match: '(\\bmm\\b|\\b\\d\\d\\b)[/\\s.\\-_—–](\\byy|\\bjj|\\baa|\\b\\d\\d)|\\bexp|\\bvalid(idity| through| until)', - skip: 'invalid' + skip: 'invalid|^dd/' }, // Identities firstName: { - match: '(first|given|fore).?name', - skip: 'last' + match: '(first|given|fore).?name' + // Italian + '|\\bnome', + skip: 'last|cognome|completo' }, middleName: { match: '(middle|additional).?name' }, lastName: { - match: '(last|family|sur)[^i]?name', - skip: 'first' + match: '(last|family|sur)[^i]?name' + // Italian + '|cognome', + skip: 'first|\\bnome' }, fullName: { - match: '^(full.?|whole\\s|first.*last\\s|real\\s|contact.?)?name\\b', + match: '^(full.?|whole\\s|first.*last\\s|real\\s|contact.?)?name\\b' + // Italian + '|\\bnome', forceUnknown: 'company|org|item' }, phone: { - match: 'phone', + match: 'phone|mobile' + // Italian + '|telefono|cellulare', skip: 'code|pass|country', forceUnknown: 'ext|type|otp' }, addressStreet: { match: 'address', forceUnknown: '\\bip\\b|duck|web|url', - skip: 'address.*(2|two|3|three)|email|log.?in|sign.?in' + skip: 'address.*(2|two|3|three)|email|log.?in|sign.?in|civico' }, addressStreet2: { match: 'address.*(2|two)|apartment|\\bapt\\b|\\bflat\\b|\\bline.*(2|two)', @@ -13015,19 +13100,20 @@ const matchingConfiguration = { skip: 'email|log.?in|sign.?in' }, addressCity: { - match: 'city|town', + match: 'city|town|città|comune', + skip: '\\bzip\\b|\\bcap\\b', forceUnknown: 'vatican' }, addressProvince: { - match: 'state|province|region|county', + match: 'state|province|region|county|provincia|regione', forceUnknown: 'united', skip: 'country' }, addressPostalCode: { - match: '\\bzip\\b|postal\b|post.?code' + match: '\\bzip\\b|postal\b|post.?code|\\bcap\\b|codice postale' }, addressCountryCode: { - match: 'country' + match: 'country|\\bnation\\b|nazione|paese' }, birthdayDay: { match: '(birth.*day|day.*birth)', @@ -13039,6 +13125,69 @@ const matchingConfiguration = { }, birthdayYear: { match: '(birth.*year|year.*birth)' + }, + loginRegex: { + match: 'sign(ing)?.?in(?!g)|log.?(i|o)n|log.?out|unsubscri|(forgot(ten)?|reset) (your )?password|password (forgotten|lost)' + '|mfa-submit-form' + // fix chase.com + '|unlock|logged in as' + // fix bitwarden + // Italian + '|entra|accedi|accesso|resetta password|password dimenticata|dimenticato la password|recuper[ao] password' + // German + '|(ein|aus)loggen|anmeld(eformular|ung|efeld)|abmelden|passwort (vergessen|verloren)|zugang| zugangsformular|einwahl' + // Dutch + '|inloggen' + // French + '|se (dé)?connecter|(dé)?connexion|récupérer ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)' + // Spanish + '|clave(?! su)|olvidó su (clave|contraseña)|.*sesión|conect(arse|ado)|conéctate|acce(de|so)|entrar' + // Swedish + '|logga (in|ut)|avprenumerera|avregistrera|glömt lösenord|återställ lösenord' + }, + signupRegex: { + match: 'sign(ing)?.?up|join|\\bregist(er|ration)|newsletter|\\bsubscri(be|ption)|contact|create|start|enroll|settings|preferences|profile|update|checkout|guest|purchase|buy|order|schedule|estimate|request|new.?customer|(confirm|retype|repeat) password|password confirm' + // Italian + '|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)i|sottoscriv|sottoscrizione|compra|acquist(a|o)|ordin[aeio]|richie(?:di|sta)|(?:conferma|ripeti) password|inizia|nuovo cliente|impostazioni|preferenze|profilo|aggiorna|paga' + // German + '|registrier(ung|en)|profil (anlegen|erstellen)| nachrichten|verteiler|neukunde|neuer (kunde|benutzer|nutzer)|passwort wiederholen|anmeldeseite' + // Dutch + '|nieuwsbrief|aanmaken|profiel' + // French + '|s.inscrire|inscription|s.abonner|créer|préférences|profil|mise à jour|payer|ach(eter|at)| nouvel utilisateur|(confirmer|réessayer) ((mon|ton|votre|le) )?mot de passe' + // Spanish + '|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|solicitar|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo' + // Swedish + '|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera|till kassan|gäst|köp|beställ|schemalägg|ny kund|(repetera|bekräfta) lösenord' + }, + conservativeSignupRegex: { + match: 'sign.?up|join|register|enroll|(create|new).+account|newsletter|subscri(be|ption)|settings|preferences|profile|update' + // Italian + '|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)?i|sottoscriv|sottoscrizione|impostazioni|preferenze|aggiorna' + // German + '|anmeld(en|ung)|registrier(en|ung)|neukunde|neuer (kunde|benutzer|nutzer)' + // Dutch + '|registreren|eigenschappen|profiel|bijwerken' + // French + '|s.inscrire|inscription|s.abonner|abonnement|préférences|profil|créer un compte' + // Spanish + '|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo' + // Swedish + '|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera' + }, + resetPasswordLink: { + match: '(forgot(ten)?|reset|don\'t remember) (your )?password|password forgotten' + // Italian + '|password dimenticata|reset(?:ta) password|recuper[ao] password' + // German + '|(vergessen|verloren|verlegt|wiederherstellen) passwort' + // Dutch + '|wachtwoord (vergeten|reset)' + // French + '|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)' + // Spanish + '|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)' + // Swedish + '|glömt lösenord|återställ lösenord' + }, + loginProvidersRegex: { + match: ' with ' + // Italian and Spanish + '| con ' + // German + '| mit ' + // Dutch + '| met ' + // French + '| avec ' + }, + submitButtonRegex: { + match: 'submit|send|confirm|save|continue|next|sign|log.?([io])n|buy|purchase|check.?out|subscribe|donate' + // Italian + '|invia|conferma|salva|continua|entra|acced|accesso|compra|paga|sottoscriv|registra|dona' + // German + '|senden|\\bja\\b|bestätigen|weiter|nächste|kaufen|bezahlen|spenden' + // Dutch + '|versturen|verzenden|opslaan|volgende|koop|kopen|voeg toe|aanmelden' + // French + '|envoyer|confirmer|sauvegarder|continuer|suivant|signer|connexion|acheter|payer|s.abonner|donner' + // Spanish + '|enviar|confirmar|registrarse|continuar|siguiente|comprar|donar' + // Swedish + '|skicka|bekräfta|spara|fortsätt|nästa|logga in|köp|handla|till kassan|registrera|donera' + }, + submitButtonUnlikelyRegex: { + match: 'facebook|twitter|google|apple|cancel|password|show|toggle|reveal|hide|print|back|already' + // Italian + '|annulla|mostra|nascondi|stampa|indietro|già' + // German + '|abbrechen|passwort|zeigen|verbergen|drucken|zurück' + // Dutch + '|annuleer|wachtwoord|toon|vorige' + // French + '|annuler|mot de passe|montrer|cacher|imprimer|retour|déjà' + // Spanish + '|anular|cancelar|imprimir|cerrar' + // Swedish + '|avbryt|lösenord|visa|dölj|skirv ut|tillbaka|redan' } } }, @@ -13224,7 +13373,7 @@ const matchingConfiguration = { '|സംസ്ഥാനം' + // ml '|استان' + // fa '|राज्य' + // hi - '|((\\b|_|\\*)(eyalet|[şs]ehir|[İii̇]l(imiz)?|kent)(\\b|_|\\*))' + // tr + '|((\\b|_|\\*)(eyalet|[şs]ehir|[İii̇]limiz|kent)(\\b|_|\\*))' + // tr '|^시[·・]?도', // ko-KR 'postal-code': 'zip|postal|post.*code|pcode' + '|pin.?code' + // en-IN @@ -13261,7 +13410,7 @@ const matchingConfiguration = { '|持卡人姓名', // zh-TW name: '^name|full.?name|your.?name|customer.?name|bill.?name|ship.?name' + '|name.*first.*last|firstandlastname' + '|nombre.*y.*apellidos' + // es - '|^nom(?!bre)' + // fr-FR + '|^nom(?!bre)\\b' + // fr-FR '|お名前|氏名' + // ja-JP '|^nome' + // pt-BR, pt-PT '|نام.*نام.*خانوادگی' + // fa @@ -13273,7 +13422,7 @@ const matchingConfiguration = { '|nombre' + // es '|forename|prénom|prenom' + // fr-FR '|名' + // ja-JP - '|nome' + // pt-BR, pt-PT + '|\\bnome' + // pt-BR, pt-PT '|Имя' + // ru '|نام' + // fa '|이름' + // ko-KR @@ -13444,8 +13593,6 @@ var _constants = require("../constants.js"); var _labelUtil = require("./label-util.js"); -var _selectorsCss = require("./selectors-css.js"); - var _matchingConfiguration = require("./matching-configuration.js"); var _matchingUtils = require("./matching-utils.js"); @@ -13475,8 +13622,8 @@ const { /** @type {{[K in keyof MatcherLists]?: { minWidth: number }} } */ const dimensionBounds = { - email: { - minWidth: 40 + emailAddress: { + minWidth: 35 } }; /** @@ -13573,11 +13720,12 @@ class Matching { _classPrivateFieldSet(this, _ddgMatchers, _classPrivateFieldGet(this, _config).strategies.ddgMatcher.matchers); _classPrivateFieldSet(this, _matcherLists, { + unknown: [], cc: [], id: [], password: [], username: [], - email: [] + emailAddress: [] }); /** * Convert the raw config data into actual references. @@ -13624,6 +13772,19 @@ class Matching { return match; } + /** + * Strategies can have different lookup names. This returns the correct one + * @param {MatcherTypeNames} matcherName + * @param {StrategyNames} vendorRegex + * @returns {MatcherTypeNames} + */ + + + getStrategyLookupByType(matcherName, vendorRegex) { + var _classPrivateFieldGet2; + + return (_classPrivateFieldGet2 = _classPrivateFieldGet(this, _config).matchers.fields[matcherName]) === null || _classPrivateFieldGet2 === void 0 ? void 0 : _classPrivateFieldGet2.strategies[vendorRegex]; + } /** * Try to access a 'css selector' by name from configuration * @param {keyof RequiredCssSelectors | string} selectorName @@ -13662,6 +13823,23 @@ class Matching { return match; } + /** + * Returns the RegExp for the given matcherName, with proper flags + * @param {AllDDGMatcherNames} matcherName + * @returns {RegExp|undefined} + */ + + + getDDGMatcherRegex(matcherName) { + const matcher = this.ddgMatcher(matcherName); + + if (!matcher || !matcher.match) { + console.warn('DDG matcher has unexpected format'); + return undefined; + } + + return safeRegex(matcher.match); + } /** * Try to access a list of matchers by name - these are the ones collected in the constructor * @param {keyof MatcherLists} listName @@ -13752,10 +13930,11 @@ class Matching { return presetType; } - this.setActiveElementStrings(input, formEl); // // For CC forms we run aggressive matches, so we want to make sure we only + this.setActiveElementStrings(input, formEl); + if (this.subtypeFromMatchers('unknown', input)) return 'unknown'; // // For CC forms we run aggressive matches, so we want to make sure we only // // run them on actual CC forms to avoid false positives and expensive loops - if (this.isCCForm(formEl)) { + if (opts.isCCForm) { const subtype = this.subtypeFromMatchers('cc', input); if (subtype && isValidCreditCardSubtype(subtype)) { @@ -13765,10 +13944,14 @@ class Matching { if (input instanceof HTMLInputElement) { if (this.subtypeFromMatchers('password', input)) { - return 'credentials.password'; + // Any other input type is likely a false match + // Arguably "text" should be as well, but it can be used for password reveal fields + if (['password', 'text'].includes(input.type) && input.name !== 'email' && input.placeholder !== 'Username') { + return 'credentials.password'; + } } - if (this.subtypeFromMatchers('email', input) && this.isInputLargeEnough('email', input)) { + if (this.subtypeFromMatchers('emailAddress', input) && this.isInputLargeEnough('emailAddress', input)) { if (opts.isLogin || opts.isHybrid) { // TODO: Being this support back in the future // https://app.asana.com/0/1198964220583541/1204686960531034/f @@ -13806,6 +13989,7 @@ class Matching { * @typedef {{ * isLogin?: boolean, * isHybrid?: boolean, + * isCCForm?: boolean, * hasCredentials?: boolean, * supportsIdentitiesAutofill?: boolean * }} SetInputTypeOpts @@ -13965,8 +14149,7 @@ class Matching { for (let stringName of matchableStrings) { let elementString = this.activeElementStrings[stringName]; - if (!elementString) continue; - elementString = elementString.toLowerCase(); // Scoring to ensure all DDG tests are valid + if (!elementString) continue; // Scoring to ensure all DDG tests are valid let score = 0; /** @type {MatchingResult} */ @@ -14117,7 +14300,7 @@ class Matching { labelText: explicitLabelsText, placeholderAttr: el.placeholder || '', id: el.id, - relatedText: explicitLabelsText ? '' : getRelatedText(el, form, this.cssSelector('FORM_INPUTS_SELECTOR')) + relatedText: explicitLabelsText ? '' : getRelatedText(el, form, this.cssSelector('formInputsSelector')) }; this._elementStringCache.set(el, next); @@ -14139,39 +14322,6 @@ class Matching { this.setActiveElementStrings(input, form); return this; } - /** - * Tries to infer if it's a credit card form - * @param {HTMLElement} formEl - * @returns {boolean} - */ - - - isCCForm(formEl) { - var _formEl$textContent; - - const ccFieldSelector = this.joinCssSelectors('cc'); - - if (!ccFieldSelector) { - return false; - } - - const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence - - if (hasCCSelectorChild) return true; // Read form attributes to find a signal - - const hasCCAttribute = [...formEl.attributes].some(_ref => { - let { - name, - value - } = _ref; - return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); - }); - if (hasCCAttribute) return true; // Match form textContent against common cc fields (includes hidden labels) - - const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives - - return Boolean(textMatches && textMatches.length > 1); - } /** * @type {MatchingConfiguration} */ @@ -14199,9 +14349,7 @@ _defineProperty(Matching, "emptyConfig", { matchers: {} }, 'cssSelector': { - selectors: { - FORM_INPUTS_SELECTOR: _selectorsCss.FORM_INPUTS_SELECTOR - } + selectors: {} } } }); @@ -14340,7 +14488,8 @@ function getInputSubtype(input) { const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return (string || '').replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); + if (!string) return ''; + return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; /** * Get text from all explicit labels @@ -14382,6 +14531,25 @@ const getExplicitLabelsText = el => { return ''; }; +/** + * Tries to get a relevant previous Element sibling, excluding certain tags + * @param {Element} el + * @returns {Element|null} + */ + + +exports.getExplicitLabelsText = getExplicitLabelsText; + +const recursiveGetPreviousElSibling = el => { + const previousEl = el.previousElementSibling; + if (!previousEl) return null; // Skip elements with no childNodes + + if (_labelUtil.EXCLUDED_TAGS.includes(previousEl.tagName)) { + return recursiveGetPreviousElSibling(previousEl); + } + + return previousEl; +}; /** * Get all text close to the input (useful when no labels are defined) * @param {HTMLInputElement|HTMLSelectElement} el @@ -14391,28 +14559,44 @@ const getExplicitLabelsText = el => { */ -exports.getExplicitLabelsText = getExplicitLabelsText; - const getRelatedText = (el, form, cssSelector) => { let scope = getLargestMeaningfulContainer(el, form, cssSelector); // If we didn't find a container, try looking for an adjacent label if (scope === el) { - if (el.previousElementSibling instanceof HTMLLabelElement) { - scope = el.previousElementSibling; + let previousEl = recursiveGetPreviousElSibling(el); + + if (previousEl instanceof HTMLElement) { + scope = previousEl; + } // If there is still no meaningful container return empty string + + + if (scope === el || scope instanceof HTMLSelectElement) { + if (el.previousSibling instanceof Text) { + return removeExcessWhitespace(el.previousSibling.textContent); + } + + return ''; } } // If there is still no meaningful container return empty string - if (scope === el || scope.nodeName === 'SELECT') return ''; + if (scope === el || scope instanceof HTMLSelectElement) { + if (el.previousSibling instanceof Text) { + return removeExcessWhitespace(el.previousSibling.textContent); + } + + return ''; + } + let trimmedText = ''; const label = scope.querySelector('label'); if (label) { // Try searching for a label first - trimmedText = removeExcessWhitespace((0, _autofillUtils.getText)(label)); + trimmedText = (0, _autofillUtils.getTextShallow)(label); } else { // If the container has a select element, remove its contents to avoid noise - trimmedText = removeExcessWhitespace((0, _labelUtil.extractElementStrings)(scope).join(' ')); + trimmedText = (0, _labelUtil.extractElementStrings)(scope).join(' '); } // If the text is longer than n chars it's too noisy and likely to yield false positives, so return '' @@ -14434,7 +14618,7 @@ const getLargestMeaningfulContainer = (el, form, cssSelector) => { /* TODO: there could be more than one select el for the same label, in that case we should change how we compute the container */ const parentElement = el.parentElement; - if (!parentElement || el === form) return el; + if (!parentElement || el === form || !cssSelector) return el; const inputsInParentsScope = parentElement.querySelectorAll(cssSelector); // To avoid noise, ensure that our input is the only in scope if (inputsInParentsScope.length === 1) { @@ -14474,7 +14658,7 @@ const checkPlaceholderAndLabels = (input, regex, form, cssSelector) => { return !!matchInPlaceholderAndLabels(input, regex, form, cssSelector); }; /** - * Creating Regex instances can throw, so we add this to be + * Returns a RegExp from a string * @param {string} string * @returns {RegExp | undefined} string */ @@ -14484,9 +14668,8 @@ exports.checkPlaceholderAndLabels = checkPlaceholderAndLabels; const safeRegex = string => { try { - // This is lower-cased here because giving a `i` on a regex flag is a performance problem in some cases - const input = String(string).toLowerCase().normalize('NFKC'); - return new RegExp(input, 'u'); + const input = String(string).normalize('NFKC'); + return new RegExp(input, 'ui'); } catch (e) { console.warn('Could not generate regex from string input', string); return undefined; @@ -14505,35 +14688,57 @@ function createMatching() { return new Matching(_matchingConfiguration.matchingConfiguration); } -},{"../autofill-utils.js":63,"../constants.js":66,"./label-util.js":39,"./matching-configuration.js":41,"./matching-utils.js":42,"./selectors-css.js":44,"./vendor-regex.js":45}],44:[function(require,module,exports){ +},{"../autofill-utils.js":63,"../constants.js":66,"./label-util.js":39,"./matching-configuration.js":41,"./matching-utils.js":42,"./vendor-regex.js":45}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.__secret_do_not_use = exports.SUBMIT_BUTTON_SELECTOR = exports.FORM_INPUTS_SELECTOR = void 0; -const FORM_INPUTS_SELECTOR = "\ninput:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]),\n[autocomplete=username],\nselect"; -exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR; -const SUBMIT_BUTTON_SELECTOR = "\ninput[type=submit],\ninput[type=button],\ninput[type=image],\nbutton:not([role=switch]):not([role=link]),\n[role=button],\na[href=\"#\"][id*=button i],\na[href=\"#\"][id*=btn i]"; -exports.SUBMIT_BUTTON_SELECTOR = SUBMIT_BUTTON_SELECTOR; -const email = ["\ninput:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=\"\"][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]),\ninput[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]),\ninput:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=\"\"][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=email],\ninput[type=text][aria-label*=email i]:not([aria-label*=search i]),\ninput:not([type])[aria-label*=email i]:not([aria-label*=search i]),\ninput[name=username][type=email],\ninput[autocomplete=username][type=email],\ninput[autocomplete=username][placeholder*=email i],\ninput[autocomplete=email]", // https://account.nicovideo.jp/login -"input[name=\"mail_tel\" i]"]; // We've seen non-standard types like 'user'. This selector should get them, too - -const GENERIC_TEXT_FIELD = "\ninput:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])"; -const password = ["input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i])", // DDG's CloudSave feature https://emanuele.duckduckgo.com/settings +exports.selectors = void 0; +const formInputsSelector = "\ninput:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete=\"fake\"]),\n[autocomplete=username],\nselect"; +const submitButtonSelector = "\ninput[type=submit],\ninput[type=button],\ninput[type=image],\nbutton:not([role=switch]):not([role=link]),\n[role=button],\na[href=\"#\"][id*=button i],\na[href=\"#\"][id*=btn i]"; +const safeUniversalSelector = '*:not(select):not(option):not(script):not(noscript):not(style):not(br)'; // We've seen non-standard types like 'user'. This selector should get them, too + +const genericTextField = "\ninput:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])"; +const emailAddress = ["\ninput:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=\"\"][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]),\ninput[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]),\ninput:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=\"\"][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=email],\ninput[type=text][aria-label*=email i]:not([aria-label*=search i]),\ninput:not([type])[aria-label*=email i]:not([aria-label*=search i]),\ninput[name=username][type=email],\ninput[autocomplete=username][type=email],\ninput[autocomplete=username][placeholder*=email i],\ninput[autocomplete=email]", // https://account.nicovideo.jp/login +"input[name=\"mail_tel\" i]", // https://www.morningstar.it/it/membership/LoginPopup.aspx +"input[value=email i]"]; +const username = ["".concat(genericTextField, "[autocomplete^=user i]"), "input[name=username i]", // fix for `aa.com` +"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login +"input[name=\"userid\" i]", "input[id=\"userid\" i]", "input[name=\"user_id\" i]", "input[name=\"user-id\" i]", "input[id=\"login-id\" i]", "input[id=\"login_id\" i]", "input[id=\"loginid\" i]", "input[name=\"login\" i]", "input[name=accountname i]", "input[autocomplete=username i]", "input[name*=accountid i]", "input[name=\"j_username\" i]", "input[id=\"j_username\" i]", // https://account.uwindsor.ca/login +"input[name=\"uwinid\" i]", // livedoor.com +"input[name=\"livedoor_id\" i]", // https://login.oracle.com/mysso/signon.jsp?request_id= +"input[name=\"ssousername\" i]", // https://secure.nsandi.com/ +"input[name=\"j_userlogin_pwd\" i]", // https://freelance.habr.com/users/sign_up +"input[name=\"user[login]\" i]", // https://weblogin.utoronto.ca +"input[name=\"user\" i]", // https://customerportal.mastercard.com/login +"input[name$=\"_username\" i]", // https://accounts.hindustantimes.com/?type=plain&ref=lm +"input[id=\"lmSsoinput\" i]", // bigcartel.com/login +"input[name=\"account_subdomain\" i]", // https://www.mydns.jp/members/ +"input[name=\"masterid\" i]", // https://giris.turkiye.gov.tr +"input[name=\"tridField\" i]", // https://membernetprb2c.b2clogin.com +"input[id=\"signInName\" i]", // https://www.w3.org/accounts/request +"input[id=\"w3c_accountsbundle_accountrequeststep1_login\" i]", "input[id=\"username\" i]", "input[name=\"_user\" i]", "input[name=\"login_username\" i]", // https://www.flytap.com/ +"input[name^=\"login-user-account\" i]", // https://www.sanitas.es +"input[id=\"loginusuario\" i]", // https://www.guardiacivil.es/administracion/login.html +"input[name=\"usuario\" i]", // https://m.bintercanarias.com/ +"input[id=\"UserLoginFormUsername\" i]", // https://id.docker.com/login +"input[id=\"nw_username\" i]", // https://appleid.apple.com/es/sign-in (needed for all languages) +"input[can-field=\"accountName\"]", "input[placeholder^=\"username\" i]"]; +const password = ["input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i]):not([name*=card i]):not([name*=cvv i])", // DDG's CloudSave feature https://emanuele.duckduckgo.com/settings 'input.js-cloudsave-phrase']; const cardName = "\ninput[autocomplete=\"cc-name\" i],\ninput[autocomplete=\"ccname\" i],\ninput[name=\"ccname\" i],\ninput[name=\"cc-name\" i],\ninput[name=\"ppw-accountHolderName\" i],\ninput[id*=cardname i],\ninput[id*=card-name i],\ninput[id*=card_name i]"; const cardNumber = "\ninput[autocomplete=\"cc-number\" i],\ninput[autocomplete=\"ccnumber\" i],\ninput[autocomplete=\"cardnumber\" i],\ninput[autocomplete=\"card-number\" i],\ninput[name=\"ccnumber\" i],\ninput[name=\"cc-number\" i],\ninput[name*=card i][name*=number i],\ninput[name*=cardnumber i],\ninput[id*=cardnumber i],\ninput[id*=card-number i],\ninput[id*=card_number i]"; const cardSecurityCode = "\ninput[autocomplete=\"cc-csc\" i],\ninput[autocomplete=\"csc\" i],\ninput[autocomplete=\"cc-cvc\" i],\ninput[autocomplete=\"cvc\" i],\ninput[name=\"cvc\" i],\ninput[name=\"cc-cvc\" i],\ninput[name=\"cc-csc\" i],\ninput[name=\"csc\" i],\ninput[name*=security i][name*=code i]"; -const expirationMonth = "\n[autocomplete=\"cc-exp-month\" i],\n[autocomplete=\"cc_exp_month\" i],\n[name=\"ccmonth\" i],\n[name=\"ppw-expirationDate_month\" i],\n[name=cardExpiryMonth i],\n[name*=ExpDate_Month i],\n[name*=expiration i][name*=month i],\n[id*=expiration i][id*=month i],\n[name*=cc-exp-month i],\n[name*=cc_exp_month i]"; -const expirationYear = "\n[autocomplete=\"cc-exp-year\" i],\n[autocomplete=\"cc_exp_year\" i],\n[name=\"ccyear\" i],\n[name=\"ppw-expirationDate_year\" i],\n[name=cardExpiryYear i],\n[name*=ExpDate_Year i],\n[name*=expiration i][name*=year i],\n[id*=expiration i][id*=year i],\n[name*=cc-exp-year i],\n[name*=cc_exp_year i]"; +const expirationMonth = "\n[autocomplete=\"cc-exp-month\" i],\n[autocomplete=\"cc_exp_month\" i],\n[name=\"ccmonth\" i],\n[name=\"ppw-expirationDate_month\" i],\n[name=cardExpiryMonth i],\n[name*=ExpDate_Month i],\n[name*=expiration i][name*=month i],\n[id*=expiration i][id*=month i],\n[name*=cc-exp-month i],\n[name*=\"card_exp-month\" i],\n[name*=cc_exp_month i]"; +const expirationYear = "\n[autocomplete=\"cc-exp-year\" i],\n[autocomplete=\"cc_exp_year\" i],\n[name=\"ccyear\" i],\n[name=\"ppw-expirationDate_year\" i],\n[name=cardExpiryYear i],\n[name*=ExpDate_Year i],\n[name*=expiration i][name*=year i],\n[id*=expiration i][id*=year i],\n[name*=\"cc-exp-year\" i],\n[name*=\"card_exp-year\" i],\n[name*=cc_exp_year i]"; const expiration = "\n[autocomplete=\"cc-exp\" i],\n[name=\"cc-exp\" i],\n[name=\"exp-date\" i],\n[name=\"expirationDate\" i],\ninput[id*=expiration i]"; const firstName = "\n[name*=fname i], [autocomplete*=given-name i],\n[name*=firstname i], [autocomplete*=firstname i],\n[name*=first-name i], [autocomplete*=first-name i],\n[name*=first_name i], [autocomplete*=first_name i],\n[name*=givenname i], [autocomplete*=givenname i],\n[name*=given-name i],\n[name*=given_name i], [autocomplete*=given_name i],\n[name*=forename i], [autocomplete*=forename i]"; const middleName = "\n[name*=mname i], [autocomplete*=additional-name i],\n[name*=middlename i], [autocomplete*=middlename i],\n[name*=middle-name i], [autocomplete*=middle-name i],\n[name*=middle_name i], [autocomplete*=middle_name i],\n[name*=additionalname i], [autocomplete*=additionalname i],\n[name*=additional-name i],\n[name*=additional_name i], [autocomplete*=additional_name i]"; const lastName = "\n[name=lname], [autocomplete*=family-name i],\n[name*=lastname i], [autocomplete*=lastname i],\n[name*=last-name i], [autocomplete*=last-name i],\n[name*=last_name i], [autocomplete*=last_name i],\n[name*=familyname i], [autocomplete*=familyname i],\n[name*=family-name i],\n[name*=family_name i], [autocomplete*=family_name i],\n[name*=surname i], [autocomplete*=surname i]"; -const fullName = "\n[name=name], [autocomplete=name],\n[name*=fullname i], [autocomplete*=fullname i],\n[name*=full-name i], [autocomplete*=full-name i],\n[name*=full_name i], [autocomplete*=full_name i],\n[name*=your-name i], [autocomplete*=your-name i]"; +const fullName = "\n[autocomplete=name],\n[name*=fullname i], [autocomplete*=fullname i],\n[name*=full-name i], [autocomplete*=full-name i],\n[name*=full_name i], [autocomplete*=full_name i],\n[name*=your-name i], [autocomplete*=your-name i]"; const phone = "\n[name*=phone i]:not([name*=extension i]):not([name*=type i]):not([name*=country i]),\n[name*=mobile i]:not([name*=type i]),\n[autocomplete=tel],\n[autocomplete=\"tel-national\"],\n[placeholder*=\"phone number\" i]"; -const addressStreet1 = "\n[name=address i], [autocomplete=street-address i], [autocomplete=address-line1 i],\n[name=street i],\n[name=ppw-line1 i], [name*=addressLine1 i]"; +const addressStreet = "\n[name=address i], [autocomplete=street-address i], [autocomplete=address-line1 i],\n[name=street i],\n[name=ppw-line1 i], [name*=addressLine1 i]"; const addressStreet2 = "\n[name=address2 i], [autocomplete=address-line2 i],\n[name=ppw-line2 i], [name*=addressLine2 i]"; const addressCity = "\n[name=city i], [autocomplete=address-level2 i],\n[name=ppw-city i], [name*=addressCity i]"; const addressProvince = "\n[name=province i], [name=state i], [autocomplete=address-level1 i]"; @@ -14543,46 +14748,30 @@ const addressCountryCode = ["[name=country i], [autocomplete=country i],\n [ const birthdayDay = "\n[name=bday-day i],\n[name*=birthday_day i], [name*=birthday-day i],\n[name=date_of_birth_day i], [name=date-of-birth-day i],\n[name^=birthdate_d i], [name^=birthdate-d i],\n[aria-label=\"birthday\" i][placeholder=\"day\" i]"; const birthdayMonth = "\n[name=bday-month i],\n[name*=birthday_month i], [name*=birthday-month i],\n[name=date_of_birth_month i], [name=date-of-birth-month i],\n[name^=birthdate_m i], [name^=birthdate-m i],\nselect[name=\"mm\" i]"; const birthdayYear = "\n[name=bday-year i],\n[name*=birthday_year i], [name*=birthday-year i],\n[name=date_of_birth_year i], [name=date-of-birth-year i],\n[name^=birthdate_y i], [name^=birthdate-y i],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]"; -const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user i]"), "input[name=username i]", // fix for `aa.com` -"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login -"input[name=\"userid\" i]", "input[id=\"userid\" i]", "input[name=\"user_id\" i]", "input[name=\"user-id\" i]", "input[id=\"login-id\" i]", "input[id=\"login_id\" i]", "input[id=\"loginid\" i]", "input[name=\"login\" i]", "input[name=accountname i]", "input[autocomplete=username i]", "input[name*=accountid i]", "input[name=\"j_username\" i]", "input[id=\"j_username\" i]", // https://account.uwindsor.ca/login -"input[name=\"uwinid\" i]", // livedoor.com -"input[name=\"livedoor_id\" i]", // https://login.oracle.com/mysso/signon.jsp?request_id= -"input[name=\"ssousername\" i]", // https://secure.nsandi.com/ -"input[name=\"j_userlogin_pwd\" i]", // https://freelance.habr.com/users/sign_up -"input[name=\"user[login]\" i]", // https://weblogin.utoronto.ca -"input[name=\"user\" i]", // https://customerportal.mastercard.com/login -"input[name$=\"_username\" i]", // https://accounts.hindustantimes.com/?type=plain&ref=lm -"input[id=\"lmSsoinput\" i]", // bigcartel.com/login -"input[name=\"account_subdomain\" i]", // https://www.mydns.jp/members/ -"input[name=\"masterid\" i]", // https://giris.turkiye.gov.tr -"input[name=\"tridField\" i]", // https://membernetprb2c.b2clogin.com -"input[id=\"signInName\" i]", // https://www.w3.org/accounts/request -"input[id=\"w3c_accountsbundle_accountrequeststep1_login\" i]", "input[id=\"username\" i]", "input[name=\"_user\" i]", "input[name=\"login_username\" i]", // https://www.flytap.com/ -"input[name^=\"login-user-account\" i]", "input[placeholder^=\"username\" i]"]; // todo: these are still used directly right now, mostly in scanForInputs -// todo: ensure these can be set via configuration - -// Exported here for now, to be moved to configuration later -// eslint-disable-next-line camelcase -const __secret_do_not_use = { - GENERIC_TEXT_FIELD, - SUBMIT_BUTTON_SELECTOR, - FORM_INPUTS_SELECTOR, - email: email, - password, +const selectors = { + // Generic + genericTextField, + submitButtonSelector, + formInputsSelector, + safeUniversalSelector, + // Credentials + emailAddress, username, + password, + // Credit Card cardName, cardNumber, cardSecurityCode, expirationMonth, expirationYear, expiration, + // Identities firstName, middleName, lastName, fullName, phone, - addressStreet1, + addressStreet, addressStreet2, addressCity, addressProvince, @@ -14592,7 +14781,7 @@ const __secret_do_not_use = { birthdayMonth, birthdayYear }; -exports.__secret_do_not_use = __secret_do_not_use; +exports.selectors = selectors; },{}],45:[function(require,module,exports){ "use strict"; @@ -14780,7 +14969,7 @@ class InContextSignup { }; if (options.shouldHideTooltip) { - this.device.removeAutofillUIFromPage(); + this.device.removeAutofillUIFromPage('Email Protection in-context signup dismissed.'); this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null)); } @@ -15231,8 +15420,6 @@ exports.createScanner = createScanner; var _Form = require("./Form/Form.js"); -var _selectorsCss = require("./Form/selectors-css.js"); - var _constants = require("./constants.js"); var _matching = require("./Form/matching.js"); @@ -15251,9 +15438,10 @@ const { /** * @typedef {{ * forms: Map; - * init(): ()=> void; + * init(): (reason, ...rest)=> void; * enqueue(elements: (HTMLElement|Document)[]): void; * findEligibleInputs(context): Scanner; + * matching: import("./Form/matching").Matching; * options: ScannerOptions; * }} Scanner * @@ -15304,6 +15492,10 @@ class DefaultScanner { /** @type {boolean} A flag to indicate the whole page will be re-scanned */ + /** @type {boolean} Indicates whether we called stopScanning */ + + /** @type {import("./Form/matching").Matching} matching */ + /** * @param {import("./DeviceInterface/InterfacePrototype").default} device * @param {ScannerOptions} options @@ -15321,6 +15513,10 @@ class DefaultScanner { _defineProperty(this, "rescanAll", false); + _defineProperty(this, "stopped", false); + + _defineProperty(this, "matching", void 0); + _defineProperty(this, "mutObs", new MutationObserver(mutationList => { /** @type {HTMLElement[]} */ if (this.rescanAll) { @@ -15365,11 +15561,13 @@ class DefaultScanner { * Call this to scan once and then watch for changes. * * Call the returned function to remove listeners. - * @returns {() => void} + * @returns {(reason: string, ...rest) => void} */ init() { + var _this = this; + if (this.device.globalConfig.isExtension) { this.device.deviceApi.notify(new _deviceApiCalls.AddDebugFlagCall({ flag: 'autofill' @@ -15385,20 +15583,12 @@ class DefaultScanner { setTimeout(() => this.scanAndObserve(), delay); } - return () => { - var _this$device$activeFo; - - const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers - - clearTimeout(this.debounceTimer); - this.mutObs.disconnect(); - this.forms.forEach(form => { - form.resetAllInputs(); - form.removeAllDecorations(); - }); - this.forms.clear(); // Bring the user back to the input they were interacting with + return function (reason) { + for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + rest[_key - 1] = arguments[_key]; + } - activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + _this.stopScanner(reason, ...rest); }; } /** @@ -15409,9 +15599,10 @@ class DefaultScanner { scanAndObserve() { var _window$performance, _window$performance$m, _window$performance2, _window$performance2$; - (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); + (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'initial_scanner:init:start'); this.findEligibleInputs(document); - (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'initial_scanner:init:end'); + (0, _autofillUtils.logPerformance)('initial_scanner'); this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -15430,12 +15621,13 @@ class DefaultScanner { return this; } - if ('matches' in context && (_context$matches = context.matches) !== null && _context$matches !== void 0 && _context$matches.call(context, _selectorsCss.FORM_INPUTS_SELECTOR)) { + if ('matches' in context && (_context$matches = context.matches) !== null && _context$matches !== void 0 && _context$matches.call(context, this.matching.cssSelector('formInputsSelector'))) { this.addInput(context); } else { - const inputs = context.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); + const inputs = context.querySelectorAll(this.matching.cssSelector('formInputsSelector')); if (inputs.length > this.options.maxInputsPerPage) { + this.stopScanner('Too many input fields in the given context, stop scanning', context); return this; } @@ -15444,6 +15636,38 @@ class DefaultScanner { return this; } + /** + * Stops scanning, switches off the mutation observer and clears all forms + * @param {string} reason + * @param {...any} rest + */ + + + stopScanner(reason) { + var _this$device$activeFo; + + this.stopped = true; + + if ((0, _autofillUtils.shouldLog)()) { + for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + rest[_key2 - 1] = arguments[_key2]; + } + + console.log(reason, ...rest); + } + + const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers + + clearTimeout(this.debounceTimer); + this.changedElements.clear(); + this.mutObs.disconnect(); + this.forms.forEach(form => { + form.destroy(); + }); + this.forms.clear(); // Bring the user back to the input they were interacting with + + activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + } /** * @param {HTMLElement|HTMLInputElement|HTMLSelectElement} input * @returns {HTMLFormElement|HTMLElement} @@ -15452,10 +15676,13 @@ class DefaultScanner { getParentForm(input) { if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { - // Use input.form unless it encloses most of the DOM - // In that case we proceed to identify more precise wrappers - if (input.form && !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { - return input.form; + if (input.form) { + // Use input.form unless it encloses most of the DOM + // In that case we proceed to identify more precise wrappers + if (this.forms.has(input.form) || // If we've added the form we've already checked that it's not a page wrapper + !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { + return input.form; + } } } @@ -15472,8 +15699,8 @@ class DefaultScanner { } element = element.parentElement; - const inputs = element.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); - const buttons = element.querySelectorAll(_selectorsCss.SUBMIT_BUTTON_SELECTOR); // If we find a button or another input, we assume that's our form + const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector')); + const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); // If we find a button or another input, we assume that's our form if (inputs.length > 1 || buttons.length) { // found related input, return common ancestor @@ -15489,28 +15716,55 @@ class DefaultScanner { addInput(input) { - const parentForm = this.getParentForm(input); // Note that el.contains returns true for el itself + if (this.stopped) return; + const parentForm = this.getParentForm(input); + + if (parentForm instanceof HTMLFormElement && this.forms.has(parentForm)) { + var _this$forms$get; + + // We've met the form, add the input + (_this$forms$get = this.forms.get(parentForm)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + return; + } // Check if the forms we've seen are either disconnected, + // or are parent/child of the currently-found form + + + let previouslyFoundParent, childForm; + + for (const [formEl] of this.forms) { + // Remove disconnected forms to avoid leaks + if (!formEl.isConnected) { + this.forms.delete(formEl); + continue; + } - const previouslyFoundParent = [...this.forms.keys()].find(form => form.contains(parentForm)); + if (formEl.contains(parentForm)) { + previouslyFoundParent = formEl; + break; + } + + if (parentForm.contains(formEl)) { + childForm = formEl; + break; + } + } if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { // If we had a prior parent but this is an explicit form, the previous was a false positive this.forms.delete(previouslyFoundParent); } else { - var _this$forms$get; + var _this$forms$get2; // If we've already met the form or a descendant, add the input - (_this$forms$get = this.forms.get(previouslyFoundParent)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + (_this$forms$get2 = this.forms.get(previouslyFoundParent)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.addInput(input); } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = [...this.forms.keys()].find(form => parentForm.contains(form)); - if (childForm) { - var _this$forms$get2; + var _this$forms$get3; - (_this$forms$get2 = this.forms.get(childForm)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.destroy(); + (_this$forms$get3 = this.forms.get(childForm)) === null || _this$forms$get3 === void 0 ? void 0 : _this$forms$get3.destroy(); this.forms.delete(childForm); } // Only add the form if below the limit of forms per page @@ -15518,9 +15772,7 @@ class DefaultScanner { if (this.forms.size < this.options.maxFormsPerPage) { this.forms.set(parentForm, new _Form.Form(parentForm, input, this.device, this.matching, this.shouldAutoprompt)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The page has too many forms, stop adding them.'); - } + this.stopScanner('The page has too many forms, stop adding them.'); } } } @@ -15546,9 +15798,14 @@ class DefaultScanner { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { + var _window$performance3, _window$performance3$, _window$performance4, _window$performance4$; + + (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : (_window$performance3$ = _window$performance3.mark) === null || _window$performance3$ === void 0 ? void 0 : _window$performance3$.call(_window$performance3, 'scanner:init:start'); this.processChangedElements(); this.changedElements.clear(); this.rescanAll = false; + (_window$performance4 = window.performance) === null || _window$performance4 === void 0 ? void 0 : (_window$performance4$ = _window$performance4.mark) === null || _window$performance4$ === void 0 ? void 0 : _window$performance4$.call(_window$performance4, 'scanner:init:end'); + (0, _autofillUtils.logPerformance)('scanner'); }, this.options.debounceTimePeriod); } /** @@ -15589,7 +15846,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":33,"./Form/matching.js":43,"./Form/selectors-css.js":44,"./autofill-utils.js":63,"./constants.js":66,"./deviceApiCalls/__generated__/deviceApiCalls.js":67}],52:[function(require,module,exports){ +},{"./Form/Form.js":33,"./Form/matching.js":43,"./autofill-utils.js":63,"./constants.js":66,"./deviceApiCalls/__generated__/deviceApiCalls.js":67}],52:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17720,14 +17977,16 @@ Object.defineProperty(exports, "__esModule", { }); exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getText = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; +exports.logPerformance = logPerformance; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; +exports.shouldLogPerformance = shouldLogPerformance; exports.truncateFromMiddle = truncateFromMiddle; exports.wasAutofilledByChrome = void 0; exports.whenIdle = whenIdle; @@ -17802,6 +18061,10 @@ const isAutofillEnabledFromProcessedConfig = processedConfig => { const site = processedConfig.site; if (site.isBroken || !site.enabledFeatures.includes('autofill')) { + if (shouldLog()) { + console.log('⚠️ Autofill disabled by remote config'); + } + return false; } @@ -17814,6 +18077,10 @@ const isIncontextSignupEnabledFromProcessedConfig = processedConfig => { const site = processedConfig.site; if (site.isBroken || !site.enabledFeatures.includes('incontextSignup')) { + if (shouldLog()) { + console.log('⚠️ In-context signup disabled by remote config'); + } + return false; } @@ -18106,26 +18373,28 @@ function escapeXML(str) { }; return String(str).replace(/[&"'<>/]/g, m => replacements[m]); } - -const SUBMIT_BUTTON_REGEX = /submit|send|confirm|save|continue|next|sign|log.?([io])n|buy|purchase|check.?out|subscribe|donate/i; -const SUBMIT_BUTTON_UNLIKELY_REGEX = /facebook|twitter|google|apple|cancel|password|show|toggle|reveal|hide|print/i; /** * Determines if an element is likely to be a submit button * @param {HTMLElement} el A button, input, anchor or other element with role=button + * @param {import("./Form/matching").Matching} matching * @return {boolean} */ -const isLikelyASubmitButton = el => { - const text = getText(el); + +const isLikelyASubmitButton = (el, matching) => { + var _matching$getDDGMatch, _matching$getDDGMatch2, _matching$getDDGMatch3; + + const text = getTextShallow(el); const ariaLabel = el.getAttribute('aria-label') || ''; const dataTestId = el.getAttribute('data-test-id') || ''; - return (el.getAttribute('type') === 'submit' || // is explicitly set as "submit" - el.getAttribute('name') === 'submit' || // is called "submit" - /primary|submit/i.test(el.className) || // has high-signal submit classes - /submit/i.test(dataTestId) || SUBMIT_BUTTON_REGEX.test(text) || // has high-signal text + if ((el.getAttribute('type') === 'submit' || // is explicitly set as "submit" + el.getAttribute('name') === 'submit') && // is called "submit" + !((_matching$getDDGMatch = matching.getDDGMatcherRegex('submitButtonUnlikelyRegex')) !== null && _matching$getDDGMatch !== void 0 && _matching$getDDGMatch.test(text + ' ' + ariaLabel))) return true; + return (/primary|submit/i.test(el.className) || // has high-signal submit classes + /submit/i.test(dataTestId) || ((_matching$getDDGMatch2 = matching.getDDGMatcherRegex('submitButtonRegex')) === null || _matching$getDDGMatch2 === void 0 ? void 0 : _matching$getDDGMatch2.test(text)) || // has high-signal text el.offsetHeight * el.offsetWidth >= 10000 && !/secondary/i.test(el.className) // it's a large element 250x40px ) && el.offsetHeight * el.offsetWidth >= 2000 && // it's not a very small button like inline links and such - !SUBMIT_BUTTON_UNLIKELY_REGEX.test(text + ' ' + ariaLabel); + !((_matching$getDDGMatch3 = matching.getDDGMatcherRegex('submitButtonUnlikelyRegex')) !== null && _matching$getDDGMatch3 !== void 0 && _matching$getDDGMatch3.test(text + ' ' + ariaLabel)); }; /** * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form @@ -18145,26 +18414,39 @@ const buttonMatchesFormType = (el, formObj) => { return true; } }; + +exports.buttonMatchesFormType = buttonMatchesFormType; +const buttonInputTypes = ['submit', 'button']; /** - * Get the text of an element - * @param {Element} el + * Get the text of an element, one level deep max + * @param {Node} el * @returns {string} */ - -exports.buttonMatchesFormType = buttonMatchesFormType; - -const getText = el => { +const getTextShallow = el => { // for buttons, we don't care about descendants, just get the whole text as is // this is important in order to give proper attribution of the text to the button if (el instanceof HTMLButtonElement) return (0, _matching.removeExcessWhitespace)(el.textContent); - if (el instanceof HTMLInputElement && ['submit', 'button'].includes(el.type)) return el.value; - if (el instanceof HTMLInputElement && el.type === 'image') { - return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + if (el instanceof HTMLInputElement) { + if (buttonInputTypes.includes(el.type)) { + return el.value; + } + + if (el.type === 'image') { + return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + } + } + + let text = ''; + + for (const childNode of el.childNodes) { + if (childNode instanceof Text) { + text += ' ' + childNode.textContent; + } } - return (0, _matching.removeExcessWhitespace)(Array.from(el.childNodes).reduce((text, child) => child instanceof Text ? text + ' ' + child.textContent : text, '')); + return (0, _matching.removeExcessWhitespace)(text); }; /** * Check if hostname is a local address @@ -18173,7 +18455,7 @@ const getText = el => { */ -exports.getText = getText; +exports.getTextShallow = getTextShallow; function isLocalNetwork() { let hostname = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.hostname; @@ -18208,7 +18490,7 @@ const wasAutofilledByChrome = input => { } }; /** - * Checks if we should log debug info to the console + * Checks if we should log form analysis debug info to the console * @returns {boolean} */ @@ -18216,15 +18498,44 @@ const wasAutofilledByChrome = input => { exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { + return readDebugSetting('ddg-autofill-debug'); +} +/** + * Checks if we should log performance info to the console + * @returns {boolean} + */ + + +function shouldLogPerformance() { + return readDebugSetting('ddg-autofill-perf'); +} +/** + * Check if a sessionStorage item is set to 'true' + * @param setting + * @returns {boolean} + */ + + +function readDebugSetting(setting) { // sessionStorage throws in invalid schemes like data: and file: try { var _window$sessionStorag; - return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem('ddg-autofill-debug')) === 'true'; + return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem(setting)) === 'true'; } catch (e) { return false; } } + +function logPerformance(markName) { + if (shouldLogPerformance()) { + var _window$performance, _window$performance2; + + const measurement = (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : _window$performance.measure("".concat(markName, ":init"), "".concat(markName, ":init:start"), "".concat(markName, ":init:end")); + console.log("".concat(markName, " took ").concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : _window$performance2.clearMarks(); + } +} /** * * @param {Function} callback @@ -18419,7 +18730,7 @@ exports.constants = void 0; const constants = { ATTR_INPUT_TYPE: 'data-ddg-inputType', ATTR_AUTOFILL: 'data-ddg-autofill', - TEXT_LENGTH_CUTOFF: 50, + TEXT_LENGTH_CUTOFF: 100, MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, diff --git a/node_modules/@duckduckgo/autofill/dist/autofill.js b/node_modules/@duckduckgo/autofill/dist/autofill.js index 6923dae24a96..d7116109b75a 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill.js @@ -4471,15 +4471,8 @@ class ExtensionInterface extends _InterfacePrototype.default { return null; } - removeAutofillUIFromPage() { - var _this$activeForm2; - - super.removeAutofillUIFromPage(); - (_this$activeForm2 = this.activeForm) === null || _this$activeForm2 === void 0 ? void 0 : _this$activeForm2.removeAllDecorations(); - } - async resetAutofillUI(callback) { - this.removeAutofillUIFromPage(); + this.removeAutofillUIFromPage('Resetting autofill.'); await this.setupAutofill(); if (callback) await callback(); this.uiController = this.createUIController(); @@ -4517,7 +4510,7 @@ class ExtensionInterface extends _InterfacePrototype.default { switch (this.getActiveTooltipType()) { case TOOLTIP_TYPES.EmailProtection: { - var _this$activeForm3; + var _this$activeForm2; this._scannerCleanup = this.scanner.init(); this.addLogoutListener(() => { @@ -4532,12 +4525,12 @@ class ExtensionInterface extends _InterfacePrototype.default { } }); - if ((_this$activeForm3 = this.activeForm) !== null && _this$activeForm3 !== void 0 && _this$activeForm3.activeInput) { - var _this$activeForm4; + if ((_this$activeForm2 = this.activeForm) !== null && _this$activeForm2 !== void 0 && _this$activeForm2.activeInput) { + var _this$activeForm3; this.attachTooltip({ form: this.activeForm, - input: (_this$activeForm4 = this.activeForm) === null || _this$activeForm4 === void 0 ? void 0 : _this$activeForm4.activeInput, + input: (_this$activeForm3 = this.activeForm) === null || _this$activeForm3 === void 0 ? void 0 : _this$activeForm3.activeInput, click: null, trigger: 'postSignup', triggerMetaData: { @@ -4773,7 +4766,7 @@ class InterfacePrototype { /** @type {boolean} */ - /** @type {(()=>void) | null} */ + /** @type {((reason, ...rest) => void) | null} */ /** * @param {GlobalConfig} config @@ -4847,12 +4840,16 @@ class InterfacePrototype { createUIController() { return new _NativeUIController.NativeUIController(); } + /** + * @param {string} reason + */ - removeAutofillUIFromPage() { + + removeAutofillUIFromPage(reason) { var _this$uiController, _this$_scannerCleanup; (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.destroy(); - (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this); + (_this$_scannerCleanup = this._scannerCleanup) === null || _this$_scannerCleanup === void 0 ? void 0 : _this$_scannerCleanup.call(this, reason); } get hasLocalAddresses() { @@ -5032,7 +5029,7 @@ class InterfacePrototype { await this.postInit(); if (this.settings.featureToggles.credentials_saving) { - (0, _initFormSubmissionsApi.initFormSubmissionsApi)(this.scanner.forms); + (0, _initFormSubmissionsApi.initFormSubmissionsApi)(this.scanner.forms, this.scanner.matching); } } /** @@ -5092,7 +5089,7 @@ class InterfacePrototype { postInit() { const cleanup = this.scanner.init(); this.addLogoutListener(() => { - cleanup(); + cleanup('Logged out'); if (this.globalConfig.isDDGDomain) { (0, _autofillUtils.notifyWebApp)({ @@ -6181,16 +6178,17 @@ Object.defineProperty(exports, "__esModule", { }); exports.initFormSubmissionsApi = initFormSubmissionsApi; -var _selectorsCss = require("../Form/selectors-css.js"); - var _autofillUtils = require("../autofill-utils.js"); +var _labelUtil = require("../Form/label-util.js"); + /** * This is a single place to contain all functionality relating to form submission detection * * @param {Map} forms + * @param {import("../Form/matching").Matching} matching */ -function initFormSubmissionsApi(forms) { +function initFormSubmissionsApi(forms, matching) { /** * Global submit events */ @@ -6226,16 +6224,16 @@ function initFormSubmissionsApi(forms) { matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler('global pointerdown event + matching form'); if (!matchingForm) { - var _event$target, _event$target2; + var _event$target, _matching$getDDGMatch, _event$target2; - const selector = _selectorsCss.SUBMIT_BUTTON_SELECTOR + ', a[href="#"], a[href^=javascript], *[onclick]'; // check if the click happened on a button + const selector = matching.cssSelector('submitButtonSelector') + ', a[href="#"], a[href^=javascript], *[onclick], [class*=button i]'; // check if the click happened on a button const button = /** @type HTMLElement */ (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(selector); if (!button) return; - const text = (0, _autofillUtils.getText)(button); - const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text); + const text = (0, _autofillUtils.getTextShallow)(button) || (0, _labelUtil.extractElementStrings)(button).join(' '); + const hasRelevantText = (_matching$getDDGMatch = matching.getDDGMatcherRegex('submitButtonRegex')) === null || _matching$getDDGMatch === void 0 ? void 0 : _matching$getDDGMatch.test(text); if (hasRelevantText && text.length < 25) { // check if there's a form with values @@ -6278,7 +6276,7 @@ function initFormSubmissionsApi(forms) { }); } -},{"../Form/selectors-css.js":36,"../autofill-utils.js":55}],23:[function(require,module,exports){ +},{"../Form/label-util.js":31,"../autofill-utils.js":55}],23:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -6544,6 +6542,10 @@ class Form { return this.formAnalyzer.isHybrid; } + get isCCForm() { + return this.formAnalyzer.isCCForm(); + } + logFormInfo() { if (!(0, _autofillUtils.shouldLog)()) return; console.log("Form type: %c".concat(this.getFormType()), 'font-weight: bold'); @@ -6641,10 +6643,10 @@ class Form { formValues.credentials.username = formValues.identities.phone; } else { // If we still don't have a username, try scanning the form's text for an email address - this.form.querySelectorAll('*:not(select):not(option)').forEach(el => { + this.form.querySelectorAll(this.matching.cssSelector('safeUniversalSelector')).forEach(el => { var _elText$match; - const elText = (0, _autofillUtils.getText)(el); // Ignore long texts to avoid false positives + const elText = (0, _autofillUtils.getTextShallow)(el); // Ignore long texts to avoid false positives if (elText.length > 70) return; const emailOrUsername = (_elText$match = elText.match( // https://www.emailregex.com/ @@ -6800,18 +6802,25 @@ class Form { destroy() { this.removeAllDecorations(); this.removeTooltip(); + this.forgetAllInputs(); this.mutObs.disconnect(); this.matching.clear(); this.intObs = null; } categorizeInputs() { - const selector = this.matching.cssSelector('FORM_INPUTS_SELECTOR'); + const selector = this.matching.cssSelector('formInputsSelector'); if (this.form.matches(selector)) { this.addInput(this.form); } else { - const foundInputs = this.form.querySelectorAll(selector); + let foundInputs = this.form.querySelectorAll(selector); // If the markup is broken form.querySelectorAll may not return the fields, so we select from the parent + + if (foundInputs.length === 0 && this.form instanceof HTMLFormElement && this.form.length > 0) { + var _this$form$parentElem; + + foundInputs = ((_this$form$parentElem = this.form.parentElement) === null || _this$form$parentElem === void 0 ? void 0 : _this$form$parentElem.querySelectorAll(selector)) || foundInputs; + } if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); @@ -6826,11 +6835,11 @@ class Form { } get submitButtons() { - const selector = this.matching.cssSelector('SUBMIT_BUTTON_SELECTOR'); + const selector = this.matching.cssSelector('submitButtonSelector'); const allButtons = /** @type {HTMLElement[]} */ [...this.form.querySelectorAll(selector)]; - return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); + return allButtons.filter(btn => (0, _autofillUtils.isPotentiallyViewable)(btn) && (0, _autofillUtils.isLikelyASubmitButton)(btn, this.matching) && (0, _autofillUtils.buttonMatchesFormType)(btn, this)); } attemptSubmissionIfNeeded() { @@ -6902,6 +6911,7 @@ class Form { const opts = { isLogin: this.isLogin, isHybrid: this.isHybrid, + isCCForm: this.isCCForm, hasCredentials: Boolean((_this$device$settings = this.device.settings.availableInputTypes.credentials) === null || _this$device$settings === void 0 ? void 0 : _this$device$settings.username), supportsIdentitiesAutofill: this.device.settings.featureToggles.inputType_identities }; @@ -7332,13 +7342,6 @@ var _autofillUtils = require("../autofill-utils.js"); function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -const loginRegex = new RegExp(/sign(ing)?.?in(?!g)|log.?(i|o)n|log.?out|unsubscri|(forgot(ten)?|reset) (your )?password|password (forgotten|lost)|unlock|logged in as|mfa-submit-form/i); -const signupRegex = new RegExp(/sign(ing)?.?up|join|\bregist(er|ration)|newsletter|\bsubscri(be|ption)|contact|create|start|enroll|settings|preferences|profile|update|checkout|guest|purchase|buy|order|schedule|estimate|request|new.?customer|(confirm|retype|repeat) password|password confirm?/i); -const conservativeSignupRegex = new RegExp(/sign.?up|join|register|enroll|newsletter|subscri(be|ption)|settings|preferences|profile|update/i); -const strictSignupRegex = new RegExp(/sign.?up|join|register|(create|new).+account|enroll|settings|preferences|profile|update/i); -const resetPasswordLink = new RegExp(/(forgot(ten)?|reset|don't remember) (your )?password|password forgotten/i); -const loginProvidersRegex = new RegExp(/ with /i); - class FormAnalyzer { /** @type HTMLElement */ @@ -7354,6 +7357,8 @@ class FormAnalyzer { _defineProperty(this, "matching", void 0); + _defineProperty(this, "_isCCForm", undefined); + this.form = form; this.matching = matching || new _matching.Matching(_matchingConfiguration.matchingConfiguration); /** @@ -7452,6 +7457,8 @@ class FormAnalyzer { updateSignal(_ref) { + var _this$matching$getDDG, _this$matching$getDDG2, _this$matching$getDDG3; + let { string, strength, @@ -7460,15 +7467,15 @@ class FormAnalyzer { shouldCheckUnifiedForm = false, shouldBeConservative = false } = _ref; - const matchesLogin = /current.?password/i.test(string) || loginRegex.test(string) || resetPasswordLink.test(string); // Check explicitly for unified login/signup forms + const matchesLogin = /current.?password/i.test(string) || ((_this$matching$getDDG = this.matching.getDDGMatcherRegex('loginRegex')) === null || _this$matching$getDDG === void 0 ? void 0 : _this$matching$getDDG.test(string)) || ((_this$matching$getDDG2 = this.matching.getDDGMatcherRegex('resetPasswordLink')) === null || _this$matching$getDDG2 === void 0 ? void 0 : _this$matching$getDDG2.test(string)); // Check explicitly for unified login/signup forms - if (shouldCheckUnifiedForm && matchesLogin && strictSignupRegex.test(string)) { + if (shouldCheckUnifiedForm && matchesLogin && (_this$matching$getDDG3 = this.matching.getDDGMatcherRegex('conservativeSignupRegex')) !== null && _this$matching$getDDG3 !== void 0 && _this$matching$getDDG3.test(string)) { this.increaseHybridSignal(strength, signalType); return this; } - const signupRegexToUse = shouldBeConservative ? conservativeSignupRegex : signupRegex; - const matchesSignup = /new.?password/i.test(string) || signupRegexToUse.test(string); // In some cases a login match means the login is somewhere else, i.e. when a link points outside + const signupRegexToUse = this.matching.getDDGMatcherRegex(shouldBeConservative ? 'conservativeSignupRegex' : 'signupRegex'); + const matchesSignup = /new.?password/i.test(string) || (signupRegexToUse === null || signupRegexToUse === void 0 ? void 0 : signupRegexToUse.test(string)); // In some cases a login match means the login is somewhere else, i.e. when a link points outside if (shouldFlip) { if (matchesLogin) this.increaseSignalBy(strength, signalType); @@ -7496,6 +7503,24 @@ class FormAnalyzer { }); } + evaluateUrl() { + var _this$matching$getDDG4, _this$matching$getDDG5; + + const path = window.location.pathname; + const matchesLogin = (_this$matching$getDDG4 = this.matching.getDDGMatcherRegex('loginRegex')) === null || _this$matching$getDDG4 === void 0 ? void 0 : _this$matching$getDDG4.test(path); + const matchesSignup = (_this$matching$getDDG5 = this.matching.getDDGMatcherRegex('conservativeSignupRegex')) === null || _this$matching$getDDG5 === void 0 ? void 0 : _this$matching$getDDG5.test(path); // If the url matches both, do nothing: the signal is probably confounding + + if (matchesLogin && matchesSignup) return; + + if (matchesLogin) { + this.decreaseSignalBy(1, 'url matches login'); + } + + if (matchesSignup) { + this.increaseSignalBy(1, 'url matches signup'); + } + } + evaluatePageTitle() { const pageTitle = document.title; this.updateSignal({ @@ -7527,7 +7552,7 @@ class FormAnalyzer { this.evaluatePageTitle(); this.evaluatePageHeadings(); // Check for submit buttons - const buttons = document.querySelectorAll(this.matching.cssSelector('SUBMIT_BUTTON_SELECTOR')); + const buttons = document.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); buttons.forEach(button => { // if the button has a form, it's not related to our input, because our input has no form here if (button instanceof HTMLButtonElement) { @@ -7540,7 +7565,7 @@ class FormAnalyzer { } evaluateElement(el) { - const string = (0, _autofillUtils.getText)(el); + const string = (0, _autofillUtils.getTextShallow)(el); if (el.matches(this.matching.cssSelector('password'))) { // These are explicit signals by the web author, so we weigh them heavily @@ -7549,12 +7574,13 @@ class FormAnalyzer { strength: 5, signalType: "explicit: ".concat(el.getAttribute('autocomplete')) }); + return; } // check button contents - if (el.matches(this.matching.cssSelector('SUBMIT_BUTTON_SELECTOR'))) { + if (el.matches(this.matching.cssSelector('submitButtonSelector') + ', *[class*=button]')) { // If we're confident this is the submit button, it's a stronger signal - let likelyASubmit = (0, _autofillUtils.isLikelyASubmitButton)(el); + let likelyASubmit = (0, _autofillUtils.isLikelyASubmitButton)(el, this.matching); if (likelyASubmit) { this.form.querySelectorAll('input[type=submit], button[type=submit]').forEach(submit => { @@ -7571,21 +7597,27 @@ class FormAnalyzer { strength, signalType: "submit: ".concat(string) }); + return; } // if an external link matches one of the regexes, we assume the match is not pertinent to the current form if (el instanceof HTMLAnchorElement && el.href && el.getAttribute('href') !== '#' || (el.getAttribute('role') || '').toUpperCase() === 'LINK' || el.matches('button[class*=secondary]')) { + var _this$matching$getDDG6, _this$matching$getDDG7; + let shouldFlip = true; + let strength = 1; // Don't flip forgotten password links - if (resetPasswordLink.test(string) || // Don't flip forgotten password links - loginProvidersRegex.test(string) // Don't flip login providers links - ) { + if ((_this$matching$getDDG6 = this.matching.getDDGMatcherRegex('resetPasswordLink')) !== null && _this$matching$getDDG6 !== void 0 && _this$matching$getDDG6.test(string)) { + shouldFlip = false; + strength = 3; + } else if ((_this$matching$getDDG7 = this.matching.getDDGMatcherRegex('loginProvidersRegex')) !== null && _this$matching$getDDG7 !== void 0 && _this$matching$getDDG7.test(string)) { + // Don't flip login providers links shouldFlip = false; } this.updateSignal({ string, - strength: 1, + strength, signalType: "external link: ".concat(string), shouldFlip }); @@ -7606,12 +7638,14 @@ class FormAnalyzer { } evaluateForm() { - // Check page title + // Check page url + this.evaluateUrl(); // Check page title + this.evaluatePageTitle(); // Check form attributes - this.evaluateElAttributes(this.form); // Check form contents (skip select and option because they contain too much noise) + this.evaluateElAttributes(this.form); // Check form contents (noisy elements are skipped with the safeUniversalSelector) - this.form.querySelectorAll('*:not(select):not(option):not(script)').forEach(el => { + this.form.querySelectorAll(this.matching.cssSelector('safeUniversalSelector')).forEach(el => { // Check if element is not hidden. Note that we can't use offsetHeight // nor intersectionObserver, because the element could be outside the // viewport or its parent hidden @@ -7619,7 +7653,7 @@ class FormAnalyzer { if (displayValue !== 'none') this.evaluateElement(el); }); // A form with many fields is unlikely to be a login form - const relevantFields = this.form.querySelectorAll(this.matching.cssSelector('GENERIC_TEXT_FIELD')); + const relevantFields = this.form.querySelectorAll(this.matching.cssSelector('genericTextField')); if (relevantFields.length >= 4) { this.increaseSignalBy(relevantFields.length * 1.5, 'many fields: it is probably not a login'); @@ -7632,6 +7666,52 @@ class FormAnalyzer { return this; } + /** @type {undefined|boolean} */ + + + /** + * Tries to infer if it's a credit card form + * @returns {boolean} + */ + isCCForm() { + var _formEl$textContent; + + if (this._isCCForm !== undefined) return this._isCCForm; + const formEl = this.form; + const ccFieldSelector = this.matching.joinCssSelectors('cc'); + + if (!ccFieldSelector) { + this._isCCForm = false; + return this._isCCForm; + } + + const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence + + if (hasCCSelectorChild) { + this._isCCForm = true; + return this._isCCForm; + } // Read form attributes to find a signal + + + const hasCCAttribute = [...formEl.attributes].some(_ref3 => { + let { + name, + value + } = _ref3; + return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); + }); + + if (hasCCAttribute) { + this._isCCForm = true; + return this._isCCForm; + } // Match form textContent against common cc fields (includes hidden labels) + + + const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives + + this._isCCForm = Boolean(textMatches && textMatches.length > 1); + return this._isCCForm; + } } @@ -8235,7 +8315,7 @@ const FOUR_DIGIT_YEAR_REGEX = /(\D)\1{3}|\d{4}/i; */ const formatCCYear = (input, year, form) => { - const selector = form.matching.cssSelector('FORM_INPUTS_SELECTOR'); + const selector = form.matching.cssSelector('formInputsSelector'); if (input.maxLength === 4 || (0, _matching.checkPlaceholderAndLabels)(input, FOUR_DIGIT_YEAR_REGEX, form.form, selector)) return year; return "".concat(Number(year) - 2000); }; @@ -8256,7 +8336,7 @@ const getUnifiedExpiryDate = (input, month, year, form) => { const formattedYear = formatCCYear(input, year, form); const paddedMonth = "".concat(month).padStart(2, '0'); - const cssSelector = form.matching.cssSelector('FORM_INPUTS_SELECTOR'); + const cssSelector = form.matching.cssSelector('formInputsSelector'); const separator = ((_matchInPlaceholderAn = (0, _matching.matchInPlaceholderAndLabels)(input, DATE_SEPARATOR_REGEX, form.form, cssSelector)) === null || _matchInPlaceholderAn === void 0 ? void 0 : (_matchInPlaceholderAn2 = _matchInPlaceholderAn.groups) === null || _matchInPlaceholderAn2 === void 0 ? void 0 : _matchInPlaceholderAn2.separator) || '/'; return "".concat(paddedMonth).concat(separator).concat(formattedYear); }; @@ -8951,22 +9031,24 @@ exports.isFieldDecorated = isFieldDecorated; Object.defineProperty(exports, "__esModule", { value: true }); -exports.extractElementStrings = void 0; +exports.extractElementStrings = exports.EXCLUDED_TAGS = void 0; var _matching = require("./matching.js"); -const EXCLUDED_TAGS = ['SCRIPT', 'NOSCRIPT', 'OPTION', 'STYLE']; +const EXCLUDED_TAGS = ['BR', 'SCRIPT', 'NOSCRIPT', 'OPTION', 'STYLE']; /** * Extract all strings of an element's children to an array. * "element.textContent" is a string which is merged of all children nodes, * which can cause issues with things like script tags etc. * - * @param {HTMLElement} element + * @param {Element} element * A DOM element to be extracted. * @returns {string[]} * All strings in an element. */ +exports.EXCLUDED_TAGS = EXCLUDED_TAGS; + const extractElementStrings = element => { const strings = new Set(); @@ -9026,11 +9108,7 @@ Object.defineProperty(exports, "__esModule", { }); exports.matchingConfiguration = void 0; -var css = _interopRequireWildcard(require("./selectors-css.js")); - -function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } - -function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +var _selectorsCss = require("./selectors-css.js"); /** * This is here to mimic what Remote Configuration might look like @@ -9042,11 +9120,17 @@ const matchingConfiguration = { /** @type {MatcherConfiguration} */ matchers: { fields: { - email: { - type: 'email', + unknown: { + type: 'unknown', strategies: { - cssSelector: 'email', - ddgMatcher: 'email', + ddgMatcher: 'unknown' + } + }, + emailAddress: { + type: 'emailAddress', + strategies: { + cssSelector: 'emailAddress', + ddgMatcher: 'emailAddress', vendorRegex: 'email' } }, @@ -9222,7 +9306,8 @@ const matchingConfiguration = { } }, lists: { - email: ['email'], + unknown: ['unknown'], + emailAddress: ['emailAddress'], password: ['password'], username: ['username'], cc: ['cardName', 'cardNumber', 'cardSecurityCode', 'expirationMonth', 'expirationYear', 'expiration'], @@ -9232,55 +9317,50 @@ const matchingConfiguration = { strategies: { /** @type {CssSelectorConfiguration} */ cssSelector: { - selectors: { - // Generic - FORM_INPUTS_SELECTOR: css.__secret_do_not_use.FORM_INPUTS_SELECTOR, - SUBMIT_BUTTON_SELECTOR: css.__secret_do_not_use.SUBMIT_BUTTON_SELECTOR, - GENERIC_TEXT_FIELD: css.__secret_do_not_use.GENERIC_TEXT_FIELD, - // user - email: css.__secret_do_not_use.email, - password: css.__secret_do_not_use.password, - username: css.__secret_do_not_use.username, - // CC - cardName: css.__secret_do_not_use.cardName, - cardNumber: css.__secret_do_not_use.cardNumber, - cardSecurityCode: css.__secret_do_not_use.cardSecurityCode, - expirationMonth: css.__secret_do_not_use.expirationMonth, - expirationYear: css.__secret_do_not_use.expirationYear, - expiration: css.__secret_do_not_use.expiration, - // Identities - firstName: css.__secret_do_not_use.firstName, - middleName: css.__secret_do_not_use.middleName, - lastName: css.__secret_do_not_use.lastName, - fullName: css.__secret_do_not_use.fullName, - phone: css.__secret_do_not_use.phone, - addressStreet: css.__secret_do_not_use.addressStreet1, - addressStreet2: css.__secret_do_not_use.addressStreet2, - addressCity: css.__secret_do_not_use.addressCity, - addressProvince: css.__secret_do_not_use.addressProvince, - addressPostalCode: css.__secret_do_not_use.addressPostalCode, - addressCountryCode: css.__secret_do_not_use.addressCountryCode, - birthdayDay: css.__secret_do_not_use.birthdayDay, - birthdayMonth: css.__secret_do_not_use.birthdayMonth, - birthdayYear: css.__secret_do_not_use.birthdayYear - } + selectors: _selectorsCss.selectors }, /** @type {DDGMatcherConfiguration} */ ddgMatcher: { matchers: { - email: { - match: '.mail\\b|apple.?id', + unknown: { + match: 'search|filter|subject|title|captcha|mfa|2fa|two factor|one-time|otp' + // Italian + '|cerca|filtr|oggetto|titolo|(due|più) fattori' + // German + '|suche|filtern|betreff' + // Dutch + '|zoeken|filter|onderwerp|titel' + // French + '|chercher|filtrer|objet|titre|authentification multifacteur|double authentification|à usage unique' + // Spanish + '|busca|busqueda|filtra|dos pasos|un solo uso' + // Swedish + '|sök|filter|ämne|multifaktorsautentisering|tvåfaktorsautentisering|två.?faktor|engångs', + skip: 'phone|mobile|email|password' + }, + emailAddress: { + match: '.mail\\b|apple.?id' + // Italian + '|posta elettronica' + // Dutch + '|e.?mailadres' + // Spanish + '|correo electr|correo-e|^correo$' + // Swedish + '|\\be.?post|e.?postadress', skip: 'phone|(first.?|last.?)name|number|code', forceUnknown: 'search|filter|subject|title|\btab\b|otp' }, password: { - match: 'password', + match: 'password' + // German + '|passwort|kennwort' + // Dutch + '|wachtwoord' + // French + '|mot de passe' + // Spanish + '|clave|contraseña' + // Swedish + '|lösenord', skip: 'email|one-time|error|hint', - forceUnknown: 'captcha|mfa|2fa|two factor|otp' + forceUnknown: 'captcha|mfa|2fa|two factor|otp|pin' }, username: { - match: '(user|account|log(i|o)n|net)((.)?(name|i.?d.?|log(i|o)n).?)?(.?((or|/).+|\\*|:))?$|benutzername', + match: '(user|account|log(i|o)n|net)((.)?(name|i.?d.?|log(i|o)n).?)?(.?((or|/).+|\\*|:)( required)?)?$' + // Italian + '|(nome|id|login).?utente|(nome|id) (dell.)?account|codice cliente' + // German + '|nutzername|anmeldename' + // Dutch + '|gebruikersnaam' + // French + '|nom d.utilisateur|identifiant|pseudo' + // Spanish + '|usuari|cuenta|identificador|apodo' + // in Spanish dni and nie stand for id number, often used as username + '|\\bdni\\b|\\bnie\\b| del? documento|documento de identidad' + // Swedish + '|användarnamn|kontonamn|användar-id', skip: 'phone', forceUnknown: 'search|policy' }, @@ -9290,6 +9370,7 @@ const matchingConfiguration = { }, cardNumber: { match: 'card.*number|number.*card', + skip: 'phone', forceUnknown: 'plus' }, cardSecurityCode: { @@ -9305,33 +9386,37 @@ const matchingConfiguration = { }, expiration: { match: '(\\bmm\\b|\\b\\d\\d\\b)[/\\s.\\-_—–](\\byy|\\bjj|\\baa|\\b\\d\\d)|\\bexp|\\bvalid(idity| through| until)', - skip: 'invalid' + skip: 'invalid|^dd/' }, // Identities firstName: { - match: '(first|given|fore).?name', - skip: 'last' + match: '(first|given|fore).?name' + // Italian + '|\\bnome', + skip: 'last|cognome|completo' }, middleName: { match: '(middle|additional).?name' }, lastName: { - match: '(last|family|sur)[^i]?name', - skip: 'first' + match: '(last|family|sur)[^i]?name' + // Italian + '|cognome', + skip: 'first|\\bnome' }, fullName: { - match: '^(full.?|whole\\s|first.*last\\s|real\\s|contact.?)?name\\b', + match: '^(full.?|whole\\s|first.*last\\s|real\\s|contact.?)?name\\b' + // Italian + '|\\bnome', forceUnknown: 'company|org|item' }, phone: { - match: 'phone', + match: 'phone|mobile' + // Italian + '|telefono|cellulare', skip: 'code|pass|country', forceUnknown: 'ext|type|otp' }, addressStreet: { match: 'address', forceUnknown: '\\bip\\b|duck|web|url', - skip: 'address.*(2|two|3|three)|email|log.?in|sign.?in' + skip: 'address.*(2|two|3|three)|email|log.?in|sign.?in|civico' }, addressStreet2: { match: 'address.*(2|two)|apartment|\\bapt\\b|\\bflat\\b|\\bline.*(2|two)', @@ -9339,19 +9424,20 @@ const matchingConfiguration = { skip: 'email|log.?in|sign.?in' }, addressCity: { - match: 'city|town', + match: 'city|town|città|comune', + skip: '\\bzip\\b|\\bcap\\b', forceUnknown: 'vatican' }, addressProvince: { - match: 'state|province|region|county', + match: 'state|province|region|county|provincia|regione', forceUnknown: 'united', skip: 'country' }, addressPostalCode: { - match: '\\bzip\\b|postal\b|post.?code' + match: '\\bzip\\b|postal\b|post.?code|\\bcap\\b|codice postale' }, addressCountryCode: { - match: 'country' + match: 'country|\\bnation\\b|nazione|paese' }, birthdayDay: { match: '(birth.*day|day.*birth)', @@ -9363,6 +9449,69 @@ const matchingConfiguration = { }, birthdayYear: { match: '(birth.*year|year.*birth)' + }, + loginRegex: { + match: 'sign(ing)?.?in(?!g)|log.?(i|o)n|log.?out|unsubscri|(forgot(ten)?|reset) (your )?password|password (forgotten|lost)' + '|mfa-submit-form' + // fix chase.com + '|unlock|logged in as' + // fix bitwarden + // Italian + '|entra|accedi|accesso|resetta password|password dimenticata|dimenticato la password|recuper[ao] password' + // German + '|(ein|aus)loggen|anmeld(eformular|ung|efeld)|abmelden|passwort (vergessen|verloren)|zugang| zugangsformular|einwahl' + // Dutch + '|inloggen' + // French + '|se (dé)?connecter|(dé)?connexion|récupérer ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)' + // Spanish + '|clave(?! su)|olvidó su (clave|contraseña)|.*sesión|conect(arse|ado)|conéctate|acce(de|so)|entrar' + // Swedish + '|logga (in|ut)|avprenumerera|avregistrera|glömt lösenord|återställ lösenord' + }, + signupRegex: { + match: 'sign(ing)?.?up|join|\\bregist(er|ration)|newsletter|\\bsubscri(be|ption)|contact|create|start|enroll|settings|preferences|profile|update|checkout|guest|purchase|buy|order|schedule|estimate|request|new.?customer|(confirm|retype|repeat) password|password confirm' + // Italian + '|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)i|sottoscriv|sottoscrizione|compra|acquist(a|o)|ordin[aeio]|richie(?:di|sta)|(?:conferma|ripeti) password|inizia|nuovo cliente|impostazioni|preferenze|profilo|aggiorna|paga' + // German + '|registrier(ung|en)|profil (anlegen|erstellen)| nachrichten|verteiler|neukunde|neuer (kunde|benutzer|nutzer)|passwort wiederholen|anmeldeseite' + // Dutch + '|nieuwsbrief|aanmaken|profiel' + // French + '|s.inscrire|inscription|s.abonner|créer|préférences|profil|mise à jour|payer|ach(eter|at)| nouvel utilisateur|(confirmer|réessayer) ((mon|ton|votre|le) )?mot de passe' + // Spanish + '|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|solicitar|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo' + // Swedish + '|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera|till kassan|gäst|köp|beställ|schemalägg|ny kund|(repetera|bekräfta) lösenord' + }, + conservativeSignupRegex: { + match: 'sign.?up|join|register|enroll|(create|new).+account|newsletter|subscri(be|ption)|settings|preferences|profile|update' + // Italian + '|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)?i|sottoscriv|sottoscrizione|impostazioni|preferenze|aggiorna' + // German + '|anmeld(en|ung)|registrier(en|ung)|neukunde|neuer (kunde|benutzer|nutzer)' + // Dutch + '|registreren|eigenschappen|profiel|bijwerken' + // French + '|s.inscrire|inscription|s.abonner|abonnement|préférences|profil|créer un compte' + // Spanish + '|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo' + // Swedish + '|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera' + }, + resetPasswordLink: { + match: '(forgot(ten)?|reset|don\'t remember) (your )?password|password forgotten' + // Italian + '|password dimenticata|reset(?:ta) password|recuper[ao] password' + // German + '|(vergessen|verloren|verlegt|wiederherstellen) passwort' + // Dutch + '|wachtwoord (vergeten|reset)' + // French + '|(oublié|récupérer) ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)' + // Spanish + '|re(iniciar|cuperar) (contraseña|clave)|olvid(ó su|aste tu|é mi) (contraseña|clave)|recordar( su)? (contraseña|clave)' + // Swedish + '|glömt lösenord|återställ lösenord' + }, + loginProvidersRegex: { + match: ' with ' + // Italian and Spanish + '| con ' + // German + '| mit ' + // Dutch + '| met ' + // French + '| avec ' + }, + submitButtonRegex: { + match: 'submit|send|confirm|save|continue|next|sign|log.?([io])n|buy|purchase|check.?out|subscribe|donate' + // Italian + '|invia|conferma|salva|continua|entra|acced|accesso|compra|paga|sottoscriv|registra|dona' + // German + '|senden|\\bja\\b|bestätigen|weiter|nächste|kaufen|bezahlen|spenden' + // Dutch + '|versturen|verzenden|opslaan|volgende|koop|kopen|voeg toe|aanmelden' + // French + '|envoyer|confirmer|sauvegarder|continuer|suivant|signer|connexion|acheter|payer|s.abonner|donner' + // Spanish + '|enviar|confirmar|registrarse|continuar|siguiente|comprar|donar' + // Swedish + '|skicka|bekräfta|spara|fortsätt|nästa|logga in|köp|handla|till kassan|registrera|donera' + }, + submitButtonUnlikelyRegex: { + match: 'facebook|twitter|google|apple|cancel|password|show|toggle|reveal|hide|print|back|already' + // Italian + '|annulla|mostra|nascondi|stampa|indietro|già' + // German + '|abbrechen|passwort|zeigen|verbergen|drucken|zurück' + // Dutch + '|annuleer|wachtwoord|toon|vorige' + // French + '|annuler|mot de passe|montrer|cacher|imprimer|retour|déjà' + // Spanish + '|anular|cancelar|imprimir|cerrar' + // Swedish + '|avbryt|lösenord|visa|dölj|skirv ut|tillbaka|redan' } } }, @@ -9548,7 +9697,7 @@ const matchingConfiguration = { '|സംസ്ഥാനം' + // ml '|استان' + // fa '|राज्य' + // hi - '|((\\b|_|\\*)(eyalet|[şs]ehir|[İii̇]l(imiz)?|kent)(\\b|_|\\*))' + // tr + '|((\\b|_|\\*)(eyalet|[şs]ehir|[İii̇]limiz|kent)(\\b|_|\\*))' + // tr '|^시[·・]?도', // ko-KR 'postal-code': 'zip|postal|post.*code|pcode' + '|pin.?code' + // en-IN @@ -9585,7 +9734,7 @@ const matchingConfiguration = { '|持卡人姓名', // zh-TW name: '^name|full.?name|your.?name|customer.?name|bill.?name|ship.?name' + '|name.*first.*last|firstandlastname' + '|nombre.*y.*apellidos' + // es - '|^nom(?!bre)' + // fr-FR + '|^nom(?!bre)\\b' + // fr-FR '|お名前|氏名' + // ja-JP '|^nome' + // pt-BR, pt-PT '|نام.*نام.*خانوادگی' + // fa @@ -9597,7 +9746,7 @@ const matchingConfiguration = { '|nombre' + // es '|forename|prénom|prenom' + // fr-FR '|名' + // ja-JP - '|nome' + // pt-BR, pt-PT + '|\\bnome' + // pt-BR, pt-PT '|Имя' + // ru '|نام' + // fa '|이름' + // ko-KR @@ -9768,8 +9917,6 @@ var _constants = require("../constants.js"); var _labelUtil = require("./label-util.js"); -var _selectorsCss = require("./selectors-css.js"); - var _matchingConfiguration = require("./matching-configuration.js"); var _matchingUtils = require("./matching-utils.js"); @@ -9799,8 +9946,8 @@ const { /** @type {{[K in keyof MatcherLists]?: { minWidth: number }} } */ const dimensionBounds = { - email: { - minWidth: 40 + emailAddress: { + minWidth: 35 } }; /** @@ -9897,11 +10044,12 @@ class Matching { _classPrivateFieldSet(this, _ddgMatchers, _classPrivateFieldGet(this, _config).strategies.ddgMatcher.matchers); _classPrivateFieldSet(this, _matcherLists, { + unknown: [], cc: [], id: [], password: [], username: [], - email: [] + emailAddress: [] }); /** * Convert the raw config data into actual references. @@ -9948,6 +10096,19 @@ class Matching { return match; } + /** + * Strategies can have different lookup names. This returns the correct one + * @param {MatcherTypeNames} matcherName + * @param {StrategyNames} vendorRegex + * @returns {MatcherTypeNames} + */ + + + getStrategyLookupByType(matcherName, vendorRegex) { + var _classPrivateFieldGet2; + + return (_classPrivateFieldGet2 = _classPrivateFieldGet(this, _config).matchers.fields[matcherName]) === null || _classPrivateFieldGet2 === void 0 ? void 0 : _classPrivateFieldGet2.strategies[vendorRegex]; + } /** * Try to access a 'css selector' by name from configuration * @param {keyof RequiredCssSelectors | string} selectorName @@ -9986,6 +10147,23 @@ class Matching { return match; } + /** + * Returns the RegExp for the given matcherName, with proper flags + * @param {AllDDGMatcherNames} matcherName + * @returns {RegExp|undefined} + */ + + + getDDGMatcherRegex(matcherName) { + const matcher = this.ddgMatcher(matcherName); + + if (!matcher || !matcher.match) { + console.warn('DDG matcher has unexpected format'); + return undefined; + } + + return safeRegex(matcher.match); + } /** * Try to access a list of matchers by name - these are the ones collected in the constructor * @param {keyof MatcherLists} listName @@ -10076,10 +10254,11 @@ class Matching { return presetType; } - this.setActiveElementStrings(input, formEl); // // For CC forms we run aggressive matches, so we want to make sure we only + this.setActiveElementStrings(input, formEl); + if (this.subtypeFromMatchers('unknown', input)) return 'unknown'; // // For CC forms we run aggressive matches, so we want to make sure we only // // run them on actual CC forms to avoid false positives and expensive loops - if (this.isCCForm(formEl)) { + if (opts.isCCForm) { const subtype = this.subtypeFromMatchers('cc', input); if (subtype && isValidCreditCardSubtype(subtype)) { @@ -10089,10 +10268,14 @@ class Matching { if (input instanceof HTMLInputElement) { if (this.subtypeFromMatchers('password', input)) { - return 'credentials.password'; + // Any other input type is likely a false match + // Arguably "text" should be as well, but it can be used for password reveal fields + if (['password', 'text'].includes(input.type) && input.name !== 'email' && input.placeholder !== 'Username') { + return 'credentials.password'; + } } - if (this.subtypeFromMatchers('email', input) && this.isInputLargeEnough('email', input)) { + if (this.subtypeFromMatchers('emailAddress', input) && this.isInputLargeEnough('emailAddress', input)) { if (opts.isLogin || opts.isHybrid) { // TODO: Being this support back in the future // https://app.asana.com/0/1198964220583541/1204686960531034/f @@ -10130,6 +10313,7 @@ class Matching { * @typedef {{ * isLogin?: boolean, * isHybrid?: boolean, + * isCCForm?: boolean, * hasCredentials?: boolean, * supportsIdentitiesAutofill?: boolean * }} SetInputTypeOpts @@ -10289,8 +10473,7 @@ class Matching { for (let stringName of matchableStrings) { let elementString = this.activeElementStrings[stringName]; - if (!elementString) continue; - elementString = elementString.toLowerCase(); // Scoring to ensure all DDG tests are valid + if (!elementString) continue; // Scoring to ensure all DDG tests are valid let score = 0; /** @type {MatchingResult} */ @@ -10441,7 +10624,7 @@ class Matching { labelText: explicitLabelsText, placeholderAttr: el.placeholder || '', id: el.id, - relatedText: explicitLabelsText ? '' : getRelatedText(el, form, this.cssSelector('FORM_INPUTS_SELECTOR')) + relatedText: explicitLabelsText ? '' : getRelatedText(el, form, this.cssSelector('formInputsSelector')) }; this._elementStringCache.set(el, next); @@ -10463,39 +10646,6 @@ class Matching { this.setActiveElementStrings(input, form); return this; } - /** - * Tries to infer if it's a credit card form - * @param {HTMLElement} formEl - * @returns {boolean} - */ - - - isCCForm(formEl) { - var _formEl$textContent; - - const ccFieldSelector = this.joinCssSelectors('cc'); - - if (!ccFieldSelector) { - return false; - } - - const hasCCSelectorChild = formEl.matches(ccFieldSelector) || formEl.querySelector(ccFieldSelector); // If the form contains one of the specific selectors, we have high confidence - - if (hasCCSelectorChild) return true; // Read form attributes to find a signal - - const hasCCAttribute = [...formEl.attributes].some(_ref => { - let { - name, - value - } = _ref; - return /(credit|payment).?card/i.test("".concat(name, "=").concat(value)); - }); - if (hasCCAttribute) return true; // Match form textContent against common cc fields (includes hidden labels) - - const textMatches = (_formEl$textContent = formEl.textContent) === null || _formEl$textContent === void 0 ? void 0 : _formEl$textContent.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); // We check for more than one to minimise false positives - - return Boolean(textMatches && textMatches.length > 1); - } /** * @type {MatchingConfiguration} */ @@ -10523,9 +10673,7 @@ _defineProperty(Matching, "emptyConfig", { matchers: {} }, 'cssSelector': { - selectors: { - FORM_INPUTS_SELECTOR: _selectorsCss.FORM_INPUTS_SELECTOR - } + selectors: {} } } }); @@ -10664,7 +10812,8 @@ function getInputSubtype(input) { const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - return (string || '').replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); + if (!string) return ''; + return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; /** * Get text from all explicit labels @@ -10706,6 +10855,25 @@ const getExplicitLabelsText = el => { return ''; }; +/** + * Tries to get a relevant previous Element sibling, excluding certain tags + * @param {Element} el + * @returns {Element|null} + */ + + +exports.getExplicitLabelsText = getExplicitLabelsText; + +const recursiveGetPreviousElSibling = el => { + const previousEl = el.previousElementSibling; + if (!previousEl) return null; // Skip elements with no childNodes + + if (_labelUtil.EXCLUDED_TAGS.includes(previousEl.tagName)) { + return recursiveGetPreviousElSibling(previousEl); + } + + return previousEl; +}; /** * Get all text close to the input (useful when no labels are defined) * @param {HTMLInputElement|HTMLSelectElement} el @@ -10715,28 +10883,44 @@ const getExplicitLabelsText = el => { */ -exports.getExplicitLabelsText = getExplicitLabelsText; - const getRelatedText = (el, form, cssSelector) => { let scope = getLargestMeaningfulContainer(el, form, cssSelector); // If we didn't find a container, try looking for an adjacent label if (scope === el) { - if (el.previousElementSibling instanceof HTMLLabelElement) { - scope = el.previousElementSibling; + let previousEl = recursiveGetPreviousElSibling(el); + + if (previousEl instanceof HTMLElement) { + scope = previousEl; + } // If there is still no meaningful container return empty string + + + if (scope === el || scope instanceof HTMLSelectElement) { + if (el.previousSibling instanceof Text) { + return removeExcessWhitespace(el.previousSibling.textContent); + } + + return ''; } } // If there is still no meaningful container return empty string - if (scope === el || scope.nodeName === 'SELECT') return ''; + if (scope === el || scope instanceof HTMLSelectElement) { + if (el.previousSibling instanceof Text) { + return removeExcessWhitespace(el.previousSibling.textContent); + } + + return ''; + } + let trimmedText = ''; const label = scope.querySelector('label'); if (label) { // Try searching for a label first - trimmedText = removeExcessWhitespace((0, _autofillUtils.getText)(label)); + trimmedText = (0, _autofillUtils.getTextShallow)(label); } else { // If the container has a select element, remove its contents to avoid noise - trimmedText = removeExcessWhitespace((0, _labelUtil.extractElementStrings)(scope).join(' ')); + trimmedText = (0, _labelUtil.extractElementStrings)(scope).join(' '); } // If the text is longer than n chars it's too noisy and likely to yield false positives, so return '' @@ -10758,7 +10942,7 @@ const getLargestMeaningfulContainer = (el, form, cssSelector) => { /* TODO: there could be more than one select el for the same label, in that case we should change how we compute the container */ const parentElement = el.parentElement; - if (!parentElement || el === form) return el; + if (!parentElement || el === form || !cssSelector) return el; const inputsInParentsScope = parentElement.querySelectorAll(cssSelector); // To avoid noise, ensure that our input is the only in scope if (inputsInParentsScope.length === 1) { @@ -10798,7 +10982,7 @@ const checkPlaceholderAndLabels = (input, regex, form, cssSelector) => { return !!matchInPlaceholderAndLabels(input, regex, form, cssSelector); }; /** - * Creating Regex instances can throw, so we add this to be + * Returns a RegExp from a string * @param {string} string * @returns {RegExp | undefined} string */ @@ -10808,9 +10992,8 @@ exports.checkPlaceholderAndLabels = checkPlaceholderAndLabels; const safeRegex = string => { try { - // This is lower-cased here because giving a `i` on a regex flag is a performance problem in some cases - const input = String(string).toLowerCase().normalize('NFKC'); - return new RegExp(input, 'u'); + const input = String(string).normalize('NFKC'); + return new RegExp(input, 'ui'); } catch (e) { console.warn('Could not generate regex from string input', string); return undefined; @@ -10829,35 +11012,57 @@ function createMatching() { return new Matching(_matchingConfiguration.matchingConfiguration); } -},{"../autofill-utils.js":55,"../constants.js":58,"./label-util.js":31,"./matching-configuration.js":33,"./matching-utils.js":34,"./selectors-css.js":36,"./vendor-regex.js":37}],36:[function(require,module,exports){ +},{"../autofill-utils.js":55,"../constants.js":58,"./label-util.js":31,"./matching-configuration.js":33,"./matching-utils.js":34,"./vendor-regex.js":37}],36:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.__secret_do_not_use = exports.SUBMIT_BUTTON_SELECTOR = exports.FORM_INPUTS_SELECTOR = void 0; -const FORM_INPUTS_SELECTOR = "\ninput:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]),\n[autocomplete=username],\nselect"; -exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR; -const SUBMIT_BUTTON_SELECTOR = "\ninput[type=submit],\ninput[type=button],\ninput[type=image],\nbutton:not([role=switch]):not([role=link]),\n[role=button],\na[href=\"#\"][id*=button i],\na[href=\"#\"][id*=btn i]"; -exports.SUBMIT_BUTTON_SELECTOR = SUBMIT_BUTTON_SELECTOR; -const email = ["\ninput:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=\"\"][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]),\ninput[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]),\ninput:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=\"\"][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=email],\ninput[type=text][aria-label*=email i]:not([aria-label*=search i]),\ninput:not([type])[aria-label*=email i]:not([aria-label*=search i]),\ninput[name=username][type=email],\ninput[autocomplete=username][type=email],\ninput[autocomplete=username][placeholder*=email i],\ninput[autocomplete=email]", // https://account.nicovideo.jp/login -"input[name=\"mail_tel\" i]"]; // We've seen non-standard types like 'user'. This selector should get them, too - -const GENERIC_TEXT_FIELD = "\ninput:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])"; -const password = ["input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i])", // DDG's CloudSave feature https://emanuele.duckduckgo.com/settings +exports.selectors = void 0; +const formInputsSelector = "\ninput:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete=\"fake\"]),\n[autocomplete=username],\nselect"; +const submitButtonSelector = "\ninput[type=submit],\ninput[type=button],\ninput[type=image],\nbutton:not([role=switch]):not([role=link]),\n[role=button],\na[href=\"#\"][id*=button i],\na[href=\"#\"][id*=btn i]"; +const safeUniversalSelector = '*:not(select):not(option):not(script):not(noscript):not(style):not(br)'; // We've seen non-standard types like 'user'. This selector should get them, too + +const genericTextField = "\ninput:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])"; +const emailAddress = ["\ninput:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=\"\"][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]),\ninput[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]),\ninput:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]),\ninput[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=\"\"][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]),\ninput[type=email],\ninput[type=text][aria-label*=email i]:not([aria-label*=search i]),\ninput:not([type])[aria-label*=email i]:not([aria-label*=search i]),\ninput[name=username][type=email],\ninput[autocomplete=username][type=email],\ninput[autocomplete=username][placeholder*=email i],\ninput[autocomplete=email]", // https://account.nicovideo.jp/login +"input[name=\"mail_tel\" i]", // https://www.morningstar.it/it/membership/LoginPopup.aspx +"input[value=email i]"]; +const username = ["".concat(genericTextField, "[autocomplete^=user i]"), "input[name=username i]", // fix for `aa.com` +"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login +"input[name=\"userid\" i]", "input[id=\"userid\" i]", "input[name=\"user_id\" i]", "input[name=\"user-id\" i]", "input[id=\"login-id\" i]", "input[id=\"login_id\" i]", "input[id=\"loginid\" i]", "input[name=\"login\" i]", "input[name=accountname i]", "input[autocomplete=username i]", "input[name*=accountid i]", "input[name=\"j_username\" i]", "input[id=\"j_username\" i]", // https://account.uwindsor.ca/login +"input[name=\"uwinid\" i]", // livedoor.com +"input[name=\"livedoor_id\" i]", // https://login.oracle.com/mysso/signon.jsp?request_id= +"input[name=\"ssousername\" i]", // https://secure.nsandi.com/ +"input[name=\"j_userlogin_pwd\" i]", // https://freelance.habr.com/users/sign_up +"input[name=\"user[login]\" i]", // https://weblogin.utoronto.ca +"input[name=\"user\" i]", // https://customerportal.mastercard.com/login +"input[name$=\"_username\" i]", // https://accounts.hindustantimes.com/?type=plain&ref=lm +"input[id=\"lmSsoinput\" i]", // bigcartel.com/login +"input[name=\"account_subdomain\" i]", // https://www.mydns.jp/members/ +"input[name=\"masterid\" i]", // https://giris.turkiye.gov.tr +"input[name=\"tridField\" i]", // https://membernetprb2c.b2clogin.com +"input[id=\"signInName\" i]", // https://www.w3.org/accounts/request +"input[id=\"w3c_accountsbundle_accountrequeststep1_login\" i]", "input[id=\"username\" i]", "input[name=\"_user\" i]", "input[name=\"login_username\" i]", // https://www.flytap.com/ +"input[name^=\"login-user-account\" i]", // https://www.sanitas.es +"input[id=\"loginusuario\" i]", // https://www.guardiacivil.es/administracion/login.html +"input[name=\"usuario\" i]", // https://m.bintercanarias.com/ +"input[id=\"UserLoginFormUsername\" i]", // https://id.docker.com/login +"input[id=\"nw_username\" i]", // https://appleid.apple.com/es/sign-in (needed for all languages) +"input[can-field=\"accountName\"]", "input[placeholder^=\"username\" i]"]; +const password = ["input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i]):not([name*=card i]):not([name*=cvv i])", // DDG's CloudSave feature https://emanuele.duckduckgo.com/settings 'input.js-cloudsave-phrase']; const cardName = "\ninput[autocomplete=\"cc-name\" i],\ninput[autocomplete=\"ccname\" i],\ninput[name=\"ccname\" i],\ninput[name=\"cc-name\" i],\ninput[name=\"ppw-accountHolderName\" i],\ninput[id*=cardname i],\ninput[id*=card-name i],\ninput[id*=card_name i]"; const cardNumber = "\ninput[autocomplete=\"cc-number\" i],\ninput[autocomplete=\"ccnumber\" i],\ninput[autocomplete=\"cardnumber\" i],\ninput[autocomplete=\"card-number\" i],\ninput[name=\"ccnumber\" i],\ninput[name=\"cc-number\" i],\ninput[name*=card i][name*=number i],\ninput[name*=cardnumber i],\ninput[id*=cardnumber i],\ninput[id*=card-number i],\ninput[id*=card_number i]"; const cardSecurityCode = "\ninput[autocomplete=\"cc-csc\" i],\ninput[autocomplete=\"csc\" i],\ninput[autocomplete=\"cc-cvc\" i],\ninput[autocomplete=\"cvc\" i],\ninput[name=\"cvc\" i],\ninput[name=\"cc-cvc\" i],\ninput[name=\"cc-csc\" i],\ninput[name=\"csc\" i],\ninput[name*=security i][name*=code i]"; -const expirationMonth = "\n[autocomplete=\"cc-exp-month\" i],\n[autocomplete=\"cc_exp_month\" i],\n[name=\"ccmonth\" i],\n[name=\"ppw-expirationDate_month\" i],\n[name=cardExpiryMonth i],\n[name*=ExpDate_Month i],\n[name*=expiration i][name*=month i],\n[id*=expiration i][id*=month i],\n[name*=cc-exp-month i],\n[name*=cc_exp_month i]"; -const expirationYear = "\n[autocomplete=\"cc-exp-year\" i],\n[autocomplete=\"cc_exp_year\" i],\n[name=\"ccyear\" i],\n[name=\"ppw-expirationDate_year\" i],\n[name=cardExpiryYear i],\n[name*=ExpDate_Year i],\n[name*=expiration i][name*=year i],\n[id*=expiration i][id*=year i],\n[name*=cc-exp-year i],\n[name*=cc_exp_year i]"; +const expirationMonth = "\n[autocomplete=\"cc-exp-month\" i],\n[autocomplete=\"cc_exp_month\" i],\n[name=\"ccmonth\" i],\n[name=\"ppw-expirationDate_month\" i],\n[name=cardExpiryMonth i],\n[name*=ExpDate_Month i],\n[name*=expiration i][name*=month i],\n[id*=expiration i][id*=month i],\n[name*=cc-exp-month i],\n[name*=\"card_exp-month\" i],\n[name*=cc_exp_month i]"; +const expirationYear = "\n[autocomplete=\"cc-exp-year\" i],\n[autocomplete=\"cc_exp_year\" i],\n[name=\"ccyear\" i],\n[name=\"ppw-expirationDate_year\" i],\n[name=cardExpiryYear i],\n[name*=ExpDate_Year i],\n[name*=expiration i][name*=year i],\n[id*=expiration i][id*=year i],\n[name*=\"cc-exp-year\" i],\n[name*=\"card_exp-year\" i],\n[name*=cc_exp_year i]"; const expiration = "\n[autocomplete=\"cc-exp\" i],\n[name=\"cc-exp\" i],\n[name=\"exp-date\" i],\n[name=\"expirationDate\" i],\ninput[id*=expiration i]"; const firstName = "\n[name*=fname i], [autocomplete*=given-name i],\n[name*=firstname i], [autocomplete*=firstname i],\n[name*=first-name i], [autocomplete*=first-name i],\n[name*=first_name i], [autocomplete*=first_name i],\n[name*=givenname i], [autocomplete*=givenname i],\n[name*=given-name i],\n[name*=given_name i], [autocomplete*=given_name i],\n[name*=forename i], [autocomplete*=forename i]"; const middleName = "\n[name*=mname i], [autocomplete*=additional-name i],\n[name*=middlename i], [autocomplete*=middlename i],\n[name*=middle-name i], [autocomplete*=middle-name i],\n[name*=middle_name i], [autocomplete*=middle_name i],\n[name*=additionalname i], [autocomplete*=additionalname i],\n[name*=additional-name i],\n[name*=additional_name i], [autocomplete*=additional_name i]"; const lastName = "\n[name=lname], [autocomplete*=family-name i],\n[name*=lastname i], [autocomplete*=lastname i],\n[name*=last-name i], [autocomplete*=last-name i],\n[name*=last_name i], [autocomplete*=last_name i],\n[name*=familyname i], [autocomplete*=familyname i],\n[name*=family-name i],\n[name*=family_name i], [autocomplete*=family_name i],\n[name*=surname i], [autocomplete*=surname i]"; -const fullName = "\n[name=name], [autocomplete=name],\n[name*=fullname i], [autocomplete*=fullname i],\n[name*=full-name i], [autocomplete*=full-name i],\n[name*=full_name i], [autocomplete*=full_name i],\n[name*=your-name i], [autocomplete*=your-name i]"; +const fullName = "\n[autocomplete=name],\n[name*=fullname i], [autocomplete*=fullname i],\n[name*=full-name i], [autocomplete*=full-name i],\n[name*=full_name i], [autocomplete*=full_name i],\n[name*=your-name i], [autocomplete*=your-name i]"; const phone = "\n[name*=phone i]:not([name*=extension i]):not([name*=type i]):not([name*=country i]),\n[name*=mobile i]:not([name*=type i]),\n[autocomplete=tel],\n[autocomplete=\"tel-national\"],\n[placeholder*=\"phone number\" i]"; -const addressStreet1 = "\n[name=address i], [autocomplete=street-address i], [autocomplete=address-line1 i],\n[name=street i],\n[name=ppw-line1 i], [name*=addressLine1 i]"; +const addressStreet = "\n[name=address i], [autocomplete=street-address i], [autocomplete=address-line1 i],\n[name=street i],\n[name=ppw-line1 i], [name*=addressLine1 i]"; const addressStreet2 = "\n[name=address2 i], [autocomplete=address-line2 i],\n[name=ppw-line2 i], [name*=addressLine2 i]"; const addressCity = "\n[name=city i], [autocomplete=address-level2 i],\n[name=ppw-city i], [name*=addressCity i]"; const addressProvince = "\n[name=province i], [name=state i], [autocomplete=address-level1 i]"; @@ -10867,46 +11072,30 @@ const addressCountryCode = ["[name=country i], [autocomplete=country i],\n [ const birthdayDay = "\n[name=bday-day i],\n[name*=birthday_day i], [name*=birthday-day i],\n[name=date_of_birth_day i], [name=date-of-birth-day i],\n[name^=birthdate_d i], [name^=birthdate-d i],\n[aria-label=\"birthday\" i][placeholder=\"day\" i]"; const birthdayMonth = "\n[name=bday-month i],\n[name*=birthday_month i], [name*=birthday-month i],\n[name=date_of_birth_month i], [name=date-of-birth-month i],\n[name^=birthdate_m i], [name^=birthdate-m i],\nselect[name=\"mm\" i]"; const birthdayYear = "\n[name=bday-year i],\n[name*=birthday_year i], [name*=birthday-year i],\n[name=date_of_birth_year i], [name=date-of-birth-year i],\n[name^=birthdate_y i], [name^=birthdate-y i],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]"; -const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user i]"), "input[name=username i]", // fix for `aa.com` -"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login -"input[name=\"userid\" i]", "input[id=\"userid\" i]", "input[name=\"user_id\" i]", "input[name=\"user-id\" i]", "input[id=\"login-id\" i]", "input[id=\"login_id\" i]", "input[id=\"loginid\" i]", "input[name=\"login\" i]", "input[name=accountname i]", "input[autocomplete=username i]", "input[name*=accountid i]", "input[name=\"j_username\" i]", "input[id=\"j_username\" i]", // https://account.uwindsor.ca/login -"input[name=\"uwinid\" i]", // livedoor.com -"input[name=\"livedoor_id\" i]", // https://login.oracle.com/mysso/signon.jsp?request_id= -"input[name=\"ssousername\" i]", // https://secure.nsandi.com/ -"input[name=\"j_userlogin_pwd\" i]", // https://freelance.habr.com/users/sign_up -"input[name=\"user[login]\" i]", // https://weblogin.utoronto.ca -"input[name=\"user\" i]", // https://customerportal.mastercard.com/login -"input[name$=\"_username\" i]", // https://accounts.hindustantimes.com/?type=plain&ref=lm -"input[id=\"lmSsoinput\" i]", // bigcartel.com/login -"input[name=\"account_subdomain\" i]", // https://www.mydns.jp/members/ -"input[name=\"masterid\" i]", // https://giris.turkiye.gov.tr -"input[name=\"tridField\" i]", // https://membernetprb2c.b2clogin.com -"input[id=\"signInName\" i]", // https://www.w3.org/accounts/request -"input[id=\"w3c_accountsbundle_accountrequeststep1_login\" i]", "input[id=\"username\" i]", "input[name=\"_user\" i]", "input[name=\"login_username\" i]", // https://www.flytap.com/ -"input[name^=\"login-user-account\" i]", "input[placeholder^=\"username\" i]"]; // todo: these are still used directly right now, mostly in scanForInputs -// todo: ensure these can be set via configuration - -// Exported here for now, to be moved to configuration later -// eslint-disable-next-line camelcase -const __secret_do_not_use = { - GENERIC_TEXT_FIELD, - SUBMIT_BUTTON_SELECTOR, - FORM_INPUTS_SELECTOR, - email: email, - password, +const selectors = { + // Generic + genericTextField, + submitButtonSelector, + formInputsSelector, + safeUniversalSelector, + // Credentials + emailAddress, username, + password, + // Credit Card cardName, cardNumber, cardSecurityCode, expirationMonth, expirationYear, expiration, + // Identities firstName, middleName, lastName, fullName, phone, - addressStreet1, + addressStreet, addressStreet2, addressCity, addressProvince, @@ -10916,7 +11105,7 @@ const __secret_do_not_use = { birthdayMonth, birthdayYear }; -exports.__secret_do_not_use = __secret_do_not_use; +exports.selectors = selectors; },{}],37:[function(require,module,exports){ "use strict"; @@ -11104,7 +11293,7 @@ class InContextSignup { }; if (options.shouldHideTooltip) { - this.device.removeAutofillUIFromPage(); + this.device.removeAutofillUIFromPage('Email Protection in-context signup dismissed.'); this.device.deviceApi.notify(new _deviceApiCalls.CloseAutofillParentCall(null)); } @@ -11555,8 +11744,6 @@ exports.createScanner = createScanner; var _Form = require("./Form/Form.js"); -var _selectorsCss = require("./Form/selectors-css.js"); - var _constants = require("./constants.js"); var _matching = require("./Form/matching.js"); @@ -11575,9 +11762,10 @@ const { /** * @typedef {{ * forms: Map; - * init(): ()=> void; + * init(): (reason, ...rest)=> void; * enqueue(elements: (HTMLElement|Document)[]): void; * findEligibleInputs(context): Scanner; + * matching: import("./Form/matching").Matching; * options: ScannerOptions; * }} Scanner * @@ -11628,6 +11816,10 @@ class DefaultScanner { /** @type {boolean} A flag to indicate the whole page will be re-scanned */ + /** @type {boolean} Indicates whether we called stopScanning */ + + /** @type {import("./Form/matching").Matching} matching */ + /** * @param {import("./DeviceInterface/InterfacePrototype").default} device * @param {ScannerOptions} options @@ -11645,6 +11837,10 @@ class DefaultScanner { _defineProperty(this, "rescanAll", false); + _defineProperty(this, "stopped", false); + + _defineProperty(this, "matching", void 0); + _defineProperty(this, "mutObs", new MutationObserver(mutationList => { /** @type {HTMLElement[]} */ if (this.rescanAll) { @@ -11689,11 +11885,13 @@ class DefaultScanner { * Call this to scan once and then watch for changes. * * Call the returned function to remove listeners. - * @returns {() => void} + * @returns {(reason: string, ...rest) => void} */ init() { + var _this = this; + if (this.device.globalConfig.isExtension) { this.device.deviceApi.notify(new _deviceApiCalls.AddDebugFlagCall({ flag: 'autofill' @@ -11709,20 +11907,12 @@ class DefaultScanner { setTimeout(() => this.scanAndObserve(), delay); } - return () => { - var _this$device$activeFo; - - const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers - - clearTimeout(this.debounceTimer); - this.mutObs.disconnect(); - this.forms.forEach(form => { - form.resetAllInputs(); - form.removeAllDecorations(); - }); - this.forms.clear(); // Bring the user back to the input they were interacting with + return function (reason) { + for (var _len = arguments.length, rest = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + rest[_key - 1] = arguments[_key]; + } - activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + _this.stopScanner(reason, ...rest); }; } /** @@ -11733,9 +11923,10 @@ class DefaultScanner { scanAndObserve() { var _window$performance, _window$performance$m, _window$performance2, _window$performance2$; - (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'scanner:init:start'); + (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : (_window$performance$m = _window$performance.mark) === null || _window$performance$m === void 0 ? void 0 : _window$performance$m.call(_window$performance, 'initial_scanner:init:start'); this.findEligibleInputs(document); - (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'scanner:init:end'); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : (_window$performance2$ = _window$performance2.mark) === null || _window$performance2$ === void 0 ? void 0 : _window$performance2$.call(_window$performance2, 'initial_scanner:init:end'); + (0, _autofillUtils.logPerformance)('initial_scanner'); this.mutObs.observe(document.documentElement, { childList: true, subtree: true @@ -11754,12 +11945,13 @@ class DefaultScanner { return this; } - if ('matches' in context && (_context$matches = context.matches) !== null && _context$matches !== void 0 && _context$matches.call(context, _selectorsCss.FORM_INPUTS_SELECTOR)) { + if ('matches' in context && (_context$matches = context.matches) !== null && _context$matches !== void 0 && _context$matches.call(context, this.matching.cssSelector('formInputsSelector'))) { this.addInput(context); } else { - const inputs = context.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); + const inputs = context.querySelectorAll(this.matching.cssSelector('formInputsSelector')); if (inputs.length > this.options.maxInputsPerPage) { + this.stopScanner('Too many input fields in the given context, stop scanning', context); return this; } @@ -11768,6 +11960,38 @@ class DefaultScanner { return this; } + /** + * Stops scanning, switches off the mutation observer and clears all forms + * @param {string} reason + * @param {...any} rest + */ + + + stopScanner(reason) { + var _this$device$activeFo; + + this.stopped = true; + + if ((0, _autofillUtils.shouldLog)()) { + for (var _len2 = arguments.length, rest = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + rest[_key2 - 1] = arguments[_key2]; + } + + console.log(reason, ...rest); + } + + const activeInput = (_this$device$activeFo = this.device.activeForm) === null || _this$device$activeFo === void 0 ? void 0 : _this$device$activeFo.activeInput; // remove Dax, listeners, timers, and observers + + clearTimeout(this.debounceTimer); + this.changedElements.clear(); + this.mutObs.disconnect(); + this.forms.forEach(form => { + form.destroy(); + }); + this.forms.clear(); // Bring the user back to the input they were interacting with + + activeInput === null || activeInput === void 0 ? void 0 : activeInput.focus(); + } /** * @param {HTMLElement|HTMLInputElement|HTMLSelectElement} input * @returns {HTMLFormElement|HTMLElement} @@ -11776,10 +12000,13 @@ class DefaultScanner { getParentForm(input) { if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { - // Use input.form unless it encloses most of the DOM - // In that case we proceed to identify more precise wrappers - if (input.form && !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { - return input.form; + if (input.form) { + // Use input.form unless it encloses most of the DOM + // In that case we proceed to identify more precise wrappers + if (this.forms.has(input.form) || // If we've added the form we've already checked that it's not a page wrapper + !(0, _autofillUtils.isFormLikelyToBeUsedAsPageWrapper)(input.form)) { + return input.form; + } } } @@ -11796,8 +12023,8 @@ class DefaultScanner { } element = element.parentElement; - const inputs = element.querySelectorAll(_selectorsCss.FORM_INPUTS_SELECTOR); - const buttons = element.querySelectorAll(_selectorsCss.SUBMIT_BUTTON_SELECTOR); // If we find a button or another input, we assume that's our form + const inputs = element.querySelectorAll(this.matching.cssSelector('formInputsSelector')); + const buttons = element.querySelectorAll(this.matching.cssSelector('submitButtonSelector')); // If we find a button or another input, we assume that's our form if (inputs.length > 1 || buttons.length) { // found related input, return common ancestor @@ -11813,28 +12040,55 @@ class DefaultScanner { addInput(input) { - const parentForm = this.getParentForm(input); // Note that el.contains returns true for el itself + if (this.stopped) return; + const parentForm = this.getParentForm(input); + + if (parentForm instanceof HTMLFormElement && this.forms.has(parentForm)) { + var _this$forms$get; + + // We've met the form, add the input + (_this$forms$get = this.forms.get(parentForm)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + return; + } // Check if the forms we've seen are either disconnected, + // or are parent/child of the currently-found form + + + let previouslyFoundParent, childForm; + + for (const [formEl] of this.forms) { + // Remove disconnected forms to avoid leaks + if (!formEl.isConnected) { + this.forms.delete(formEl); + continue; + } - const previouslyFoundParent = [...this.forms.keys()].find(form => form.contains(parentForm)); + if (formEl.contains(parentForm)) { + previouslyFoundParent = formEl; + break; + } + + if (parentForm.contains(formEl)) { + childForm = formEl; + break; + } + } if (previouslyFoundParent) { if (parentForm instanceof HTMLFormElement && parentForm !== previouslyFoundParent) { // If we had a prior parent but this is an explicit form, the previous was a false positive this.forms.delete(previouslyFoundParent); } else { - var _this$forms$get; + var _this$forms$get2; // If we've already met the form or a descendant, add the input - (_this$forms$get = this.forms.get(previouslyFoundParent)) === null || _this$forms$get === void 0 ? void 0 : _this$forms$get.addInput(input); + (_this$forms$get2 = this.forms.get(previouslyFoundParent)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.addInput(input); } } else { // if this form is an ancestor of an existing form, remove that before adding this - const childForm = [...this.forms.keys()].find(form => parentForm.contains(form)); - if (childForm) { - var _this$forms$get2; + var _this$forms$get3; - (_this$forms$get2 = this.forms.get(childForm)) === null || _this$forms$get2 === void 0 ? void 0 : _this$forms$get2.destroy(); + (_this$forms$get3 = this.forms.get(childForm)) === null || _this$forms$get3 === void 0 ? void 0 : _this$forms$get3.destroy(); this.forms.delete(childForm); } // Only add the form if below the limit of forms per page @@ -11842,9 +12096,7 @@ class DefaultScanner { if (this.forms.size < this.options.maxFormsPerPage) { this.forms.set(parentForm, new _Form.Form(parentForm, input, this.device, this.matching, this.shouldAutoprompt)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The page has too many forms, stop adding them.'); - } + this.stopScanner('The page has too many forms, stop adding them.'); } } } @@ -11870,9 +12122,14 @@ class DefaultScanner { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { + var _window$performance3, _window$performance3$, _window$performance4, _window$performance4$; + + (_window$performance3 = window.performance) === null || _window$performance3 === void 0 ? void 0 : (_window$performance3$ = _window$performance3.mark) === null || _window$performance3$ === void 0 ? void 0 : _window$performance3$.call(_window$performance3, 'scanner:init:start'); this.processChangedElements(); this.changedElements.clear(); this.rescanAll = false; + (_window$performance4 = window.performance) === null || _window$performance4 === void 0 ? void 0 : (_window$performance4$ = _window$performance4.mark) === null || _window$performance4$ === void 0 ? void 0 : _window$performance4$.call(_window$performance4, 'scanner:init:end'); + (0, _autofillUtils.logPerformance)('scanner'); }, this.options.debounceTimePeriod); } /** @@ -11913,7 +12170,7 @@ function createScanner(device, scannerOptions) { }); } -},{"./Form/Form.js":25,"./Form/matching.js":35,"./Form/selectors-css.js":36,"./autofill-utils.js":55,"./constants.js":58,"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],44:[function(require,module,exports){ +},{"./Form/Form.js":25,"./Form/matching.js":35,"./autofill-utils.js":55,"./constants.js":58,"./deviceApiCalls/__generated__/deviceApiCalls.js":59}],44:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -14044,14 +14301,16 @@ Object.defineProperty(exports, "__esModule", { }); exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0; exports.escapeXML = escapeXML; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getText = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; exports.isPotentiallyViewable = void 0; exports.isValidTLD = isValidTLD; +exports.logPerformance = logPerformance; exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = void 0; exports.shouldLog = shouldLog; +exports.shouldLogPerformance = shouldLogPerformance; exports.truncateFromMiddle = truncateFromMiddle; exports.wasAutofilledByChrome = void 0; exports.whenIdle = whenIdle; @@ -14126,6 +14385,10 @@ const isAutofillEnabledFromProcessedConfig = processedConfig => { const site = processedConfig.site; if (site.isBroken || !site.enabledFeatures.includes('autofill')) { + if (shouldLog()) { + console.log('⚠️ Autofill disabled by remote config'); + } + return false; } @@ -14138,6 +14401,10 @@ const isIncontextSignupEnabledFromProcessedConfig = processedConfig => { const site = processedConfig.site; if (site.isBroken || !site.enabledFeatures.includes('incontextSignup')) { + if (shouldLog()) { + console.log('⚠️ In-context signup disabled by remote config'); + } + return false; } @@ -14430,26 +14697,28 @@ function escapeXML(str) { }; return String(str).replace(/[&"'<>/]/g, m => replacements[m]); } - -const SUBMIT_BUTTON_REGEX = /submit|send|confirm|save|continue|next|sign|log.?([io])n|buy|purchase|check.?out|subscribe|donate/i; -const SUBMIT_BUTTON_UNLIKELY_REGEX = /facebook|twitter|google|apple|cancel|password|show|toggle|reveal|hide|print/i; /** * Determines if an element is likely to be a submit button * @param {HTMLElement} el A button, input, anchor or other element with role=button + * @param {import("./Form/matching").Matching} matching * @return {boolean} */ -const isLikelyASubmitButton = el => { - const text = getText(el); + +const isLikelyASubmitButton = (el, matching) => { + var _matching$getDDGMatch, _matching$getDDGMatch2, _matching$getDDGMatch3; + + const text = getTextShallow(el); const ariaLabel = el.getAttribute('aria-label') || ''; const dataTestId = el.getAttribute('data-test-id') || ''; - return (el.getAttribute('type') === 'submit' || // is explicitly set as "submit" - el.getAttribute('name') === 'submit' || // is called "submit" - /primary|submit/i.test(el.className) || // has high-signal submit classes - /submit/i.test(dataTestId) || SUBMIT_BUTTON_REGEX.test(text) || // has high-signal text + if ((el.getAttribute('type') === 'submit' || // is explicitly set as "submit" + el.getAttribute('name') === 'submit') && // is called "submit" + !((_matching$getDDGMatch = matching.getDDGMatcherRegex('submitButtonUnlikelyRegex')) !== null && _matching$getDDGMatch !== void 0 && _matching$getDDGMatch.test(text + ' ' + ariaLabel))) return true; + return (/primary|submit/i.test(el.className) || // has high-signal submit classes + /submit/i.test(dataTestId) || ((_matching$getDDGMatch2 = matching.getDDGMatcherRegex('submitButtonRegex')) === null || _matching$getDDGMatch2 === void 0 ? void 0 : _matching$getDDGMatch2.test(text)) || // has high-signal text el.offsetHeight * el.offsetWidth >= 10000 && !/secondary/i.test(el.className) // it's a large element 250x40px ) && el.offsetHeight * el.offsetWidth >= 2000 && // it's not a very small button like inline links and such - !SUBMIT_BUTTON_UNLIKELY_REGEX.test(text + ' ' + ariaLabel); + !((_matching$getDDGMatch3 = matching.getDDGMatcherRegex('submitButtonUnlikelyRegex')) !== null && _matching$getDDGMatch3 !== void 0 && _matching$getDDGMatch3.test(text + ' ' + ariaLabel)); }; /** * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form @@ -14469,26 +14738,39 @@ const buttonMatchesFormType = (el, formObj) => { return true; } }; + +exports.buttonMatchesFormType = buttonMatchesFormType; +const buttonInputTypes = ['submit', 'button']; /** - * Get the text of an element - * @param {Element} el + * Get the text of an element, one level deep max + * @param {Node} el * @returns {string} */ - -exports.buttonMatchesFormType = buttonMatchesFormType; - -const getText = el => { +const getTextShallow = el => { // for buttons, we don't care about descendants, just get the whole text as is // this is important in order to give proper attribution of the text to the button if (el instanceof HTMLButtonElement) return (0, _matching.removeExcessWhitespace)(el.textContent); - if (el instanceof HTMLInputElement && ['submit', 'button'].includes(el.type)) return el.value; - if (el instanceof HTMLInputElement && el.type === 'image') { - return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + if (el instanceof HTMLInputElement) { + if (buttonInputTypes.includes(el.type)) { + return el.value; + } + + if (el.type === 'image') { + return (0, _matching.removeExcessWhitespace)(el.alt || el.value || el.title || el.name); + } + } + + let text = ''; + + for (const childNode of el.childNodes) { + if (childNode instanceof Text) { + text += ' ' + childNode.textContent; + } } - return (0, _matching.removeExcessWhitespace)(Array.from(el.childNodes).reduce((text, child) => child instanceof Text ? text + ' ' + child.textContent : text, '')); + return (0, _matching.removeExcessWhitespace)(text); }; /** * Check if hostname is a local address @@ -14497,7 +14779,7 @@ const getText = el => { */ -exports.getText = getText; +exports.getTextShallow = getTextShallow; function isLocalNetwork() { let hostname = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.hostname; @@ -14532,7 +14814,7 @@ const wasAutofilledByChrome = input => { } }; /** - * Checks if we should log debug info to the console + * Checks if we should log form analysis debug info to the console * @returns {boolean} */ @@ -14540,15 +14822,44 @@ const wasAutofilledByChrome = input => { exports.wasAutofilledByChrome = wasAutofilledByChrome; function shouldLog() { + return readDebugSetting('ddg-autofill-debug'); +} +/** + * Checks if we should log performance info to the console + * @returns {boolean} + */ + + +function shouldLogPerformance() { + return readDebugSetting('ddg-autofill-perf'); +} +/** + * Check if a sessionStorage item is set to 'true' + * @param setting + * @returns {boolean} + */ + + +function readDebugSetting(setting) { // sessionStorage throws in invalid schemes like data: and file: try { var _window$sessionStorag; - return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem('ddg-autofill-debug')) === 'true'; + return ((_window$sessionStorag = window.sessionStorage) === null || _window$sessionStorag === void 0 ? void 0 : _window$sessionStorag.getItem(setting)) === 'true'; } catch (e) { return false; } } + +function logPerformance(markName) { + if (shouldLogPerformance()) { + var _window$performance, _window$performance2; + + const measurement = (_window$performance = window.performance) === null || _window$performance === void 0 ? void 0 : _window$performance.measure("".concat(markName, ":init"), "".concat(markName, ":init:start"), "".concat(markName, ":init:end")); + console.log("".concat(markName, " took ").concat(Math.round(measurement === null || measurement === void 0 ? void 0 : measurement.duration), "ms")); + (_window$performance2 = window.performance) === null || _window$performance2 === void 0 ? void 0 : _window$performance2.clearMarks(); + } +} /** * * @param {Function} callback @@ -14743,7 +15054,7 @@ exports.constants = void 0; const constants = { ATTR_INPUT_TYPE: 'data-ddg-inputType', ATTR_AUTOFILL: 'data-ddg-autofill', - TEXT_LENGTH_CUTOFF: 50, + TEXT_LENGTH_CUTOFF: 100, MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, diff --git a/node_modules/@duckduckgo/autofill/dist/shared-credentials.json b/node_modules/@duckduckgo/autofill/dist/shared-credentials.json index b97fc00eb4f7..59ef6c5be6fb 100644 --- a/node_modules/@duckduckgo/autofill/dist/shared-credentials.json +++ b/node_modules/@duckduckgo/autofill/dist/shared-credentials.json @@ -247,6 +247,12 @@ "ing.com" ] }, + { + "shared": [ + "instagram.com", + "threads.net" + ] + }, { "from": [ "letsdeel.com" diff --git a/package-lock.json b/package-lock.json index 1c764917233c..9ead75df3620 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@duckduckgo/autoconsent": "^5.1.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#8.2.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#8.4.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#4.36.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#1.4.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1692081744" @@ -64,7 +64,7 @@ "integrity": "sha512-/ZUdNt+FLhtT40f53Pl/TwOLX1Rr4vCyzgDXQjtXBHF7vSaQJLRdkDkiEm4P24HAxNbg+WGeleJUiIEyQgfp2A==" }, "node_modules/@duckduckgo/autofill": { - "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#20a6aeddbd86b43fd83c42aa45fdd9ec6db0e0f7", + "resolved": "git+ssh://git@github.com/duckduckgo/duckduckgo-autofill.git#f3eccad8647fdba2b5d180a02a0513c61375b8fb", "hasInstallScript": true, "license": "Apache-2.0" }, diff --git a/package.json b/package.json index c7a551a5f26b..765bb97ca7db 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@duckduckgo/autoconsent": "^5.1.0", - "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#8.2.0", + "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#8.4.0", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#4.36.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#1.4.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1692081744"