From 1bbe4e2818b9fc840d56056a732b33e6ee83a9e2 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:32:36 +0000 Subject: [PATCH] Update autofill to 10.0.3 (#4068) Task/Issue URL: https://app.asana.com/0/1206327382038426/1206327382038426 Autofill Release: https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/10.0.3 ## Description Updates Autofill to version [10.0.3](https://github.com/duckduckgo/duckduckgo-autofill/releases/tag/10.0.3). ### Autofill 10.0.3 release notes ## What's Changed * Use the default branch when checking out repos by @GioSensation in https://github.com/duckduckgo/duckduckgo-autofill/pull/444 * Password update flows by @GioSensation in https://github.com/duckduckgo/duckduckgo-autofill/pull/453 * Bump @types/jest from 29.5.5 to 29.5.11 by @dependabot in https://github.com/duckduckgo/duckduckgo-autofill/pull/439 * Update password-related json files (2023-12-21) by @daxmobile in https://github.com/duckduckgo/duckduckgo-autofill/pull/454 * Move test forms out of src by @GioSensation in https://github.com/duckduckgo/duckduckgo-autofill/pull/457 * Move integration tests around by @GioSensation in https://github.com/duckduckgo/duckduckgo-autofill/pull/455 * Fixes by @GioSensation in https://github.com/duckduckgo/duckduckgo-autofill/pull/467 * Update password-related json files (2024-01-11) by @daxmobile in https://github.com/duckduckgo/duckduckgo-autofill/pull/466 **Full Changelog**: https://github.com/duckduckgo/duckduckgo-autofill/compare/10.0.2...10.0.3 ## 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 | 237 ++++++++++++++---- .../@duckduckgo/autofill/dist/autofill.css | 12 +- .../@duckduckgo/autofill/dist/autofill.js | 237 ++++++++++++++---- .../autofill/dist/shared-credentials.json | 9 + package-lock.json | 4 +- package.json | 2 +- 6 files changed, 389 insertions(+), 112 deletions(-) diff --git a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js index ceb5cd6551e8..e242335e851c 100644 --- a/node_modules/@duckduckgo/autofill/dist/autofill-debug.js +++ b/node_modules/@duckduckgo/autofill/dist/autofill-debug.js @@ -6612,6 +6612,9 @@ module.exports={ "classmates.com": { "password-rules": "minlength: 6; maxlength: 20; allowed: lower, upper, digit, [!@#$%^&*];" }, + "clegc-gckey.gc.ca": { + "password-rules": "minlength: 8; maxlength: 16; required: lower, upper, digit;" + }, "clien.net": { "password-rules": "minlength: 5; required: lower, upper; required: digit;" }, @@ -6745,7 +6748,7 @@ module.exports={ "password-rules": "minlength: 8; max-consecutive: 3; required: lower; required: upper; required: digit; allowed: [-!@#$%^&*_+=`|(){}[:;,.?]];" }, "fidelity.com": { - "password-rules": "minlength: 6; maxlength: 20; required: lower; allowed: upper,digit,[!$%'()+,./:;=?@^_|~];" + "password-rules": "minlength: 6; maxlength: 20; required: lower; required: upper; required: digit; required: [!$%'()+,./:;=?@^_|~]; max-consecutive: 2;" }, "flysas.com": { "password-rules": "minlength: 8; maxlength: 14; required: lower; required: upper; required: digit; required: [-~!@#$%^&_+=`|(){}[:\"'<>,.?]];" @@ -6802,7 +6805,7 @@ module.exports={ "password-rules": "minlength: 8; maxlength: 15; required: lower; required: upper; required: digit; required: special;" }, "hotels.com": { - "password-rules": "minlength: 6; maxlength: 20; required: digit; allowed: lower, upper, [@$!#()&^*%];" + "password-rules": "minlength: 6; maxlength: 20; required: digit; required: [-~#@$%&!*_?^]; allowed: lower, upper;" }, "hotwire.com": { "password-rules": "minlength: 6; maxlength: 30; allowed: lower, upper, digit, [-~!@#$%^&*_+=`|(){}[:;\"'<>,.?]];" @@ -6822,6 +6825,9 @@ module.exports={ "hyresbostader.se": { "password-rules": "minlength: 6; maxlength: 20; required: lower, upper; required: digit;" }, + "ichunqiu.com": { + "password-rules": "minlength: 8; maxlength: 20; required: lower; required: upper; required: digit;" + }, "id.sonyentertainmentnetwork.com": { "password-rules": "minlength: 8; maxlength: 30; required: lower, upper; required: digit; allowed: [-!@#^&*=+;:];" }, @@ -6894,6 +6900,9 @@ module.exports={ "lg.com": { "password-rules": "minlength: 8; maxlength: 16; required: lower; required: upper; required: digit; allowed: [-!#$%&'()*+,.:;=?@[^_{|}~]];" }, + "linearity.io": { + "password-rules": "minlength: 8; required: lower; required: upper; required: digit; required: special;" + }, "live.com": { "password-rules": "minlength: 8; required: lower; required: upper; required: digit; allowed: [-@_#!&$`%*+()./,;~:{}|?>=<^'[]];" }, @@ -7218,6 +7227,9 @@ module.exports={ "vivo.com.br": { "password-rules": "maxlength: 6; max-consecutive: 3; allowed: digit;" }, + "volaris.com": { + "password-rules": "minlength: 8; maxlength: 16; required: lower; required: upper; required: digit; required: special;" + }, "wa.aaa.com": { "password-rules": "minlength: 8; maxlength: 16; required: lower; required: upper; required: digit; allowed: ascii-printable;" }, @@ -9020,7 +9032,7 @@ class InterfacePrototype { */ preAttachTooltip(topContextData, input, form) { // A list of checks to determine if we need to generate a password - const checks = [topContextData.inputType === 'credentials.password', this.settings.featureToggles.password_generation, form.isSignup]; + const checks = [topContextData.inputType === 'credentials.password.new', this.settings.featureToggles.password_generation]; // if all checks pass, generate and save a password if (checks.every(Boolean)) { @@ -9101,10 +9113,11 @@ class InterfacePrototype { /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios + * @param {Partial} [globalConfigOverrides] * @returns {InterfacePrototype} */ - static default() { - const globalConfig = (0, _config.createGlobalConfig)(); + static default(globalConfigOverrides) { + const globalConfig = (0, _config.createGlobalConfig)(globalConfigOverrides); const transport = (0, _transports.createTransport)(globalConfig); const deviceApi = new _index.DeviceApi(transport); const settings = _Settings.Settings.default(globalConfig, deviceApi); @@ -9503,7 +9516,8 @@ function initFormSubmissionsApi(forms, matching) { * @param {PointerEvent} event */ window.addEventListener('pointerdown', event => { - const matchingForm = [...forms.values()].find(form => { + const formsArray = [...forms.values()]; + const matchingForm = formsArray.find(form => { const btns = [...form.submitButtons]; // @ts-ignore if (btns.includes(event.target)) return true; @@ -9517,11 +9531,15 @@ function initFormSubmissionsApi(forms, matching) { // check if the click happened on a button const button = /** @type HTMLElement */event.target?.closest(selector); if (!button) return; + + // If the element we've found includes a form it can't be a button, it's a false match + const buttonIsAFalsePositive = formsArray.some(form => button?.contains(form.form)); + if (buttonIsAFalsePositive) return; const text = (0, _autofillUtils.getTextShallow)(button) || (0, _labelUtil.extractElementStrings)(button).join(' '); const hasRelevantText = (0, _autofillUtils.safeRegexTest)(matching.getDDGMatcherRegex('submitButtonRegex'), text); if (hasRelevantText && text.length < 25) { // check if there's a form with values - const filledForm = [...forms.values()].find(form => form.hasValues()); + const filledForm = formsArray.find(form => form.hasValues()); if (filledForm && (0, _autofillUtils.buttonMatchesFormType)( /** @type HTMLElement */button, filledForm)) { filledForm?.submitHandler('global pointerdown event + filled form'); } @@ -9531,7 +9549,7 @@ function initFormSubmissionsApi(forms, matching) { // https://app.asana.com/0/1198964220583541/1201650539303898/f if ( /** @type HTMLElement */event.target?.closest('#passwordNext button, #identifierNext button')) { // check if there's a form with values - const filledForm = [...forms.values()].find(form => form.hasValues()); + const filledForm = formsArray.find(form => form.hasValues()); filledForm?.submitHandler('global pointerdown event + google escape hatch'); } } @@ -9541,12 +9559,13 @@ function initFormSubmissionsApi(forms, matching) { * @type {PerformanceObserver} */ const observer = new PerformanceObserver(list => { + const formsArray = [...forms.values()]; const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`? ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && (0, _autofillUtils.safeRegexTest)(/login|sign-in|signin/, entry.name)); if (!entries.length) return; - const filledForm = [...forms.values()].find(form => form.hasValues()); - const focusedForm = [...forms.values()].find(form => form.hasFocus()); + const filledForm = formsArray.find(form => form.hasValues()); + const focusedForm = formsArray.find(form => form.hasFocus()); // If a form is still focused the user is still typing: do nothing if (focusedForm) return; filledForm?.submitHandler('performance observer'); @@ -9663,6 +9682,7 @@ var _inputStyles = require("./inputStyles.js"); var _inputTypeConfig = require("./inputTypeConfig.js"); var _formatters = require("./formatters.js"); var _constants = require("../constants.js"); +var _Credentials = require("../InputTypes/Credentials.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const { ATTR_AUTOFILL, @@ -9828,8 +9848,8 @@ class Form { creditCards: {}, identities: {} }); - if (formValues.credentials.password && !formValues.credentials.username && !formValues.identities.emailAddress) { - // If we have a password but no username, let's search further + if (!formValues.credentials.username && !formValues.identities.emailAddress) { + // If we could find no username, let's search further const hiddenFields = /** @type [HTMLInputElement] */[...this.form.querySelectorAll('input[type=hidden]')]; const probableField = hiddenFields.find(field => { const regex = new RegExp('email|' + this.matching.getDDGMatcherRegex('username')?.source); @@ -10322,19 +10342,12 @@ class Form { // Do not autofill if it's disabled or readonly to avoid potential breakage if (!(0, _inputTypeConfig.canBeInteractedWith)(input)) return; - // @ts-ignore - const activeInputSubtype = (0, _matching.getInputSubtype)(this.activeInput); - const inputSubtype = (0, _matching.getInputSubtype)(input); - const isEmailAutofill = activeInputSubtype === 'emailAddress' && inputSubtype === 'emailAddress'; - - // Don't override values for identities, unless it's the current input or we're autofilling email - if (dataType === 'identities' && - // only for identities + // Don't override values the user provided, unless it's the focused input or we're autofilling creditCards + if (dataType !== 'creditCards' && + // creditCards always override, the others only when we're focusing the input input.nodeName !== 'SELECT' && input.value !== '' && // if the input is not empty - this.activeInput !== input && - // and this is not the active input - !isEmailAutofill // and we're not auto-filling email + this.activeInput !== input // and this is not the active input ) return; // do not overwrite the value // If the value is already there, just return @@ -10383,7 +10396,20 @@ class Form { autofillData = (0, _formatters.getCountryName)(input, data); } if (autofillData) { - this.autofillInput(input, autofillData, dataType); + const variant = (0, _matching.getInputVariant)(input); + if (!variant) { + return this.autofillInput(input, autofillData, dataType); + } + + // Fields with a variant should only be filled when fill is initiated from the same variant. + // This ensures we don't overwrite the current password when filling a + // generated password in password update forms. + if (variant === 'new' && _Credentials.AUTOGENERATED_KEY in data) { + return this.autofillInput(input, autofillData, dataType); + } + if (variant === 'current' && !(_Credentials.AUTOGENERATED_KEY in data)) { + return this.autofillInput(input, autofillData, dataType); + } } }, dataType); this.isAutofilling = false; @@ -10467,7 +10493,7 @@ class Form { } exports.Form = Form; -},{"../autofill-utils.js":61,"../constants.js":64,"./FormAnalyzer.js":34,"./formatters.js":36,"./inputStyles.js":37,"./inputTypeConfig.js":38,"./matching.js":43}],34:[function(require,module,exports){ +},{"../InputTypes/Credentials.js":45,"../autofill-utils.js":61,"../constants.js":64,"./FormAnalyzer.js":34,"./formatters.js":36,"./inputStyles.js":37,"./inputTypeConfig.js":38,"./matching.js":43}],34:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -10690,6 +10716,7 @@ class FormAnalyzer { 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, this.matching); + let shouldFlip = false; if (likelyASubmit) { this.form.querySelectorAll('input[type=submit], button[type=submit]').forEach(submit => { // If there is another element marked as submit and this is not, flip back to false @@ -10697,12 +10724,17 @@ class FormAnalyzer { likelyASubmit = false; } }); + } else { + // Here we don't think this is a submit, so if there is another submit in the form, flip the score + const thereIsASubmitButton = Boolean(this.form.querySelector('input[type=submit], button[type=submit]')); + shouldFlip = thereIsASubmitButton; } - const strength = likelyASubmit ? 20 : 2; + const strength = likelyASubmit ? 20 : 4; this.updateSignal({ string, strength, - signalType: `submit: ${string}` + signalType: `button: ${string}`, + shouldFlip }); return; } @@ -10802,9 +10834,11 @@ class FormAnalyzer { // Match form textContent against common cc fields (includes hidden labels) const textMatches = formEl.textContent?.match(/(credit|payment).?card(.?number)?|ccv|security.?code|cvv|cvc|csc/ig); + // De-dupe matches to avoid counting the same element more than once + const deDupedMatches = new Set(textMatches?.map(match => match.toLowerCase())); // We check for more than one to minimise false positives - this._isCCForm = Boolean(textMatches && textMatches.length > 1); + this._isCCForm = Boolean(textMatches && deDupedMatches.size > 1); return this._isCCForm; } } @@ -11898,15 +11932,25 @@ const inputTypeConfig = { } = _ref3; if (!canBeInteractedWith(input)) return ''; if (device.settings.featureToggles.inlineIcon_credentials) { + const subtype = (0, _matching.getInputSubtype)(input); + const variant = (0, _matching.getInputVariant)(input); + if (subtype === 'password' && variant === 'new') { + return ddgPasswordIcons.ddgPasswordGenIconBase; + } return ddgPasswordIcons.ddgPasswordIconBase; } return ''; }, - getIconFilled: (_input, _ref4) => { + getIconFilled: (input, _ref4) => { let { device } = _ref4; if (device.settings.featureToggles.inlineIcon_credentials) { + const subtype = (0, _matching.getInputSubtype)(input); + const variant = (0, _matching.getInputVariant)(input); + if (subtype === 'password' && variant === 'new') { + return ddgPasswordIcons.ddgPasswordGenIconFilled; + } return ddgPasswordIcons.ddgPasswordIconFilled; } return ''; @@ -11918,18 +11962,20 @@ const inputTypeConfig = { isHybrid, device } = _ref5; - // if we are on a 'login' page, check if we have data to autofill the field - if (isLogin || isHybrid) { - return canBeAutofilled(input, device); - } + const subtype = (0, _matching.getInputSubtype)(input); + const variant = (0, _matching.getInputVariant)(input); - // at this point, it's not a 'login' form, so we could offer to provide a password + // Check first for password generation and the password.new scoring if (device.settings.featureToggles.password_generation) { - const subtype = (0, _matching.getInputSubtype)(input); - if (subtype === 'password') { + if (subtype === 'password' && variant === 'new') { return canBeInteractedWith(input); } } + + // if we are on a 'login' page, check if we have data to autofill the field + if (isLogin || isHybrid || variant === 'current') { + return canBeAutofilled(input, device); + } return false; }, dataType: 'Credentials', @@ -12337,7 +12383,7 @@ const matchingConfiguration = exports.matchingConfiguration = { ddgMatcher: { matchers: { unknown: { - match: /search|filter|subject|title|captcha|mfa|2fa|two factor|one-time|otp|cerca|filtr|oggetto|titolo|(due|più) fattori|suche|filtern|betreff|zoeken|filter|onderwerp|titel|chercher|filtrer|objet|titre|authentification multifacteur|double authentification|à usage unique|busca|busqueda|filtra|dos pasos|un solo uso|sök|filter|ämne|multifaktorsautentisering|tvåfaktorsautentisering|två.?faktor|engångs/iu, + match: /search|filter|subject|title|captcha|mfa|2fa|(two|2).?factor|one-time|otp|cerca|filtr|oggetto|titolo|(due|2|più).?fattori|suche|filtern|betreff|zoeken|filter|onderwerp|titel|chercher|filtrer|objet|titre|authentification multifacteur|double authentification|à usage unique|busca|busqueda|filtra|dos pasos|un solo uso|sök|filter|ämne|multifaktorsautentisering|tvåfaktorsautentisering|två.?faktor|engångs/iu, skip: /phone|mobile|email|password/iu }, emailAddress: { @@ -12350,10 +12396,16 @@ const matchingConfiguration = exports.matchingConfiguration = { skip: /email|one-time|error|hint/iu, forceUnknown: /captcha|mfa|2fa|two factor|otp|pin/iu }, + newPassword: { + match: /new|re.?(enter|type)|repeat|update|reset/iu + }, + currentPassword: { + match: /current|old|previous|expired|existing/iu + }, username: { match: /(user|account|log(i|o)n|net)((.)?(name|i.?d.?|log(i|o)n).?)?(.?((or|\/).+|\*|:)( required)?)?$|(nome|id|login).?utente|(nome|id) (dell.)?account|codice cliente|nutzername|anmeldename|gebruikersnaam|nom d.utilisateur|identifiant|pseudo|usuari|cuenta|identificador|apodo|\bdni\b|\bnie\b| del? documento|documento de identidad|användarnamn|kontonamn|användar-id/iu, skip: /phone/iu, - forceUnknown: /search|policy/iu + forceUnknown: /search|policy|choose a user\b/iu }, cardName: { match: /(card.*name|name.*card)|(card.*holder|holder.*card)|(card.*owner|owner.*card)/iu @@ -12436,10 +12488,10 @@ const matchingConfiguration = exports.matchingConfiguration = { match: /(birth.*year|year.*birth)/iu }, loginRegex: { - match: /sign(ing)?.?in(?!g)|log.?(i|o)n|log.?out|unsubscri|(forgot(ten)?|reset) (your )?password|password (forgotten|lost)|mfa-submit-form|unlock|logged in as|entra|accedi|accesso|resetta password|password dimenticata|dimenticato la password|recuper[ao] password|(ein|aus)loggen|anmeld(eformular|ung|efeld)|abmelden|passwort (vergessen|verloren)|zugang| zugangsformular|einwahl|inloggen|se (dé)?connecter|(dé)?connexion|récupérer ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|clave(?! su)|olvidó su (clave|contraseña)|.*sesión|conect(arse|ado)|conéctate|acce(de|so)|entrar|logga (in|ut)|avprenumerera|avregistrera|glömt lösenord|återställ lösenord/iu + match: /sign(ing)?.?[io]n(?!g)|log.?[io]n|log.?out|unsubscri|(forgot(ten)?|reset) (your )?password|password (forgotten|lost)|mfa-submit-form|unlock|logged in as|entra|accedi|accesso|resetta password|password dimenticata|dimenticato la password|recuper[ao] password|(ein|aus)loggen|anmeld(eformular|ung|efeld)|abmelden|passwort (vergessen|verloren)|zugang| zugangsformular|einwahl|inloggen|se (dé)?connecter|(dé)?connexion|récupérer ((mon|ton|votre|le) )?mot de passe|mot de passe (oublié|perdu)|clave(?! su)|olvidó su (clave|contraseña)|.*sesión|conect(arse|ado)|conéctate|acce(de|so)|entrar|logga (in|ut)|avprenumerera|avregistrera|glömt lösenord|återställ lösenord/iu }, 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|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|registrier(ung|en)|profil (anlegen|erstellen)| nachrichten|verteiler|neukunde|neuer (kunde|benutzer|nutzer)|passwort wiederholen|anmeldeseite|nieuwsbrief|aanmaken|profiel|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|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|solicitar|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo|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/iu + match: /sign(ing)?.?up|join|\bregist(er|ration)|newsletter|\bsubscri(be|ption)|contact|create|start|enroll|settings|preferences|profile|update|checkout|purchase|buy|order|schedule|estimate|request|new.?customer|(confirm|re.?(type|enter)|repeat) password|password confirm|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|registrier(ung|en)|profil (anlegen|erstellen)| nachrichten|verteiler|neukunde|neuer (kunde|benutzer|nutzer)|passwort wiederholen|anmeldeseite|nieuwsbrief|aanmaken|profiel|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|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|solicitar|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo|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/iu }, conservativeSignupRegex: { match: /sign.?up|join|register|enroll|(create|new).+account|newsletter|subscri(be|ption)|settings|preferences|profile|update|iscri(viti|zione)|registra(ti|zione)|(?:nuovo|crea(?:zione)?) account|contatt(?:ac)?i|sottoscriv|sottoscrizione|impostazioni|preferenze|aggiorna|anmeld(en|ung)|registrier(en|ung)|neukunde|neuer (kunde|benutzer|nutzer)|registreren|eigenschappen|profiel|bijwerken|s.inscrire|inscription|s.abonner|abonnement|préférences|profil|créer un compte|regis(trarse|tro)|regístrate|inscr(ibirse|ipción|íbete)|crea(r cuenta)?|nueva cuenta|nuevo (cliente|usuario)|preferencias|perfil|lista de correo|registrer(a|ing)|(nytt|öppna) konto|nyhetsbrev|prenumer(era|ation)|kontakt|skapa|starta|inställningar|min (sida|kundvagn)|uppdatera/iu @@ -12451,10 +12503,10 @@ const matchingConfiguration = exports.matchingConfiguration = { match: / with | con | mit | met | avec /iu }, submitButtonRegex: { - match: /submit|send|confirm|save|continue|next|sign|log.?([io])n|buy|purchase|check.?out|subscribe|donate|invia|conferma|salva|continua|entra|acced|accesso|compra|paga|sottoscriv|registra|dona|senden|\bja\b|bestätigen|weiter|nächste|kaufen|bezahlen|spenden|versturen|verzenden|opslaan|volgende|koop|kopen|voeg toe|aanmelden|envoyer|confirmer|sauvegarder|continuer|suivant|signer|connexion|acheter|payer|s.abonner|donner|enviar|confirmar|registrarse|continuar|siguiente|comprar|donar|skicka|bekräfta|spara|fortsätt|nästa|logga in|köp|handla|till kassan|registrera|donera/iu + match: /submit|send|confirm|save|continue|next|sign|log.?([io])n|buy|purchase|check.?out|subscribe|donate|update|\bset\b|invia|conferma|salva|continua|entra|acced|accesso|compra|paga|sottoscriv|registra|dona|senden|\bja\b|bestätigen|weiter|nächste|kaufen|bezahlen|spenden|versturen|verzenden|opslaan|volgende|koop|kopen|voeg toe|aanmelden|envoyer|confirmer|sauvegarder|continuer|suivant|signer|connexion|acheter|payer|s.abonner|donner|enviar|confirmar|registrarse|continuar|siguiente|comprar|donar|skicka|bekräfta|spara|fortsätt|nästa|logga in|köp|handla|till kassan|registrera|donera/iu }, submitButtonUnlikelyRegex: { - match: /facebook|twitter|google|apple|cancel|password|show|toggle|reveal|hide|print|back|already|annulla|mostra|nascondi|stampa|indietro|già|abbrechen|passwort|zeigen|verbergen|drucken|zurück|annuleer|wachtwoord|toon|vorige|annuler|mot de passe|montrer|cacher|imprimer|retour|déjà|anular|cancelar|imprimir|cerrar|avbryt|lösenord|visa|dölj|skirv ut|tillbaka|redan/iu + match: /facebook|twitter|google|apple|cancel|show|toggle|reveal|hide|print|back|already|annulla|mostra|nascondi|stampa|indietro|già|abbrechen|passwort|zeigen|verbergen|drucken|zurück|annuleer|wachtwoord|toon|vorige|annuler|mot de passe|montrer|cacher|imprimer|retour|déjà|anular|cancelar|imprimir|cerrar|avbryt|lösenord|visa|dölj|skirv ut|tillbaka|redan/iu } } }, @@ -12627,9 +12679,11 @@ exports.createMatching = createMatching; exports.getInputMainType = exports.getExplicitLabelsText = void 0; exports.getInputSubtype = getInputSubtype; exports.getInputType = getInputType; +exports.getInputVariant = getInputVariant; exports.getMainTypeFromType = getMainTypeFromType; exports.getRelatedText = void 0; exports.getSubtypeFromType = getSubtypeFromType; +exports.getVariantFromType = getVariantFromType; exports.removeExcessWhitespace = exports.matchInPlaceholderAndLabels = void 0; var _constants = require("../constants.js"); var _labelUtil = require("./label-util.js"); @@ -12887,12 +12941,18 @@ class Matching { if (['password', 'text'].includes(input.type) && input.name !== 'email' && // pcsretirement.com, improper use of the for attribute input.name !== 'Username') { - return 'credentials.password'; + return this.inferPasswordVariant(input, opts); } } - if (this.subtypeFromMatchers('emailAddress', input) && this.isInputLargeEnough('emailAddress', input)) { + if (this.subtypeFromMatchers('emailAddress', input)) { + if (!this.isInputLargeEnough('emailAddress', input)) { + if ((0, _autofillUtils.shouldLog)()) { + console.log('Field matched for Email Address, but discarded because too small when scanned'); + } + return 'unknown'; + } if (opts.isLogin || opts.isHybrid) { - // TODO: Being this support back in the future + // TODO: Bring this support back in the future // https://app.asana.com/0/1198964220583541/1204686960531034/f // Show identities when supported and there are no credentials // if (opts.supportsIdentitiesAutofill && !opts.hasCredentials) { @@ -13016,6 +13076,40 @@ class Matching { return undefined; } + /** + * Returns the password type string including the variant + * @param {HTMLInputElement} input + * @param opts + * @returns {'credentials.password.new'|'credentials.password.current'} + */ + inferPasswordVariant(input, opts) { + // Check attributes first + // This is done mainly to ensure coverage for all languages, since attributes are usually in English + const attrsToCheck = [input.autocomplete, input.name, input.id]; + if (opts.isSignup && attrsToCheck.some(str => (0, _autofillUtils.safeRegexTest)(/new.?password|password.?new/i, str))) { + return 'credentials.password.new'; + } + if ((opts.isLogin || opts.isHybrid) && attrsToCheck.some(str => (0, _autofillUtils.safeRegexTest)(/(current|old|previous).?password|password.?(current|old|previous)/i, str))) { + return 'credentials.password.current'; + } + + // Check strings using the usual DDG matcher + const newPasswordMatch = this.execDDGMatcher('newPassword'); + if (newPasswordMatch.matched) { + return 'credentials.password.new'; + } + const currentPasswordMatch = this.execDDGMatcher('currentPassword'); + if (currentPasswordMatch.matched) { + return 'credentials.password.current'; + } + + // Otherwise, rely on the passed form type + if (opts.isLogin || opts.isHybrid) { + return 'credentials.password.current'; + } + return 'credentials.password.new'; + } + /** * CSS selector matching just leverages the `.matches` method on elements * @@ -13218,6 +13312,7 @@ class Matching { } /** + * Only used for testing * @param {HTMLInputElement|HTMLSelectElement} input * @param {HTMLElement} form * @returns {Matching} @@ -13309,7 +13404,10 @@ function isValidCreditCardSubtype(supportedType) { } /** @typedef {supportedCredentialsSubtypes[number]} SupportedCredentialsSubTypes */ -const supportedCredentialsSubtypes = /** @type {const} */['password', 'username']; +const supportedCredentialsSubtypes = /** @type {const} */['password', 'password.new', 'password.current', 'username']; + +/** @typedef {supportedVariants[number]} SupportedVariants */ +const supportedVariants = /** @type {const} */['new', 'current']; /** * @param {SupportedTypes | any} supportedType @@ -13335,6 +13433,17 @@ function getSubtypeFromType(type) { return validType ? subType : 'unknown'; } +/** + * Retrieves the variant + * @param {SupportedTypes | string} type + * @returns {SupportedVariants | ''} + */ +function getVariantFromType(type) { + const variant = type?.split('.')[2]; + const validVariant = isValidVariant(variant); + return validVariant ? variant : ''; +} + /** * @param {SupportedSubTypes | any} supportedSubType * @returns {supportedSubType is SupportedSubTypes} @@ -13351,6 +13460,14 @@ function isValidSupportedType(supportedType) { return supportedTypes.includes(supportedType); } +/** + * @param {SupportedVariants | any} supportedVariant + * @returns {supportedVariant is SupportedVariants} + */ +function isValidVariant(supportedVariant) { + return supportedVariants.includes(supportedVariant); +} + /** * Retrieves the input subtype * @param {HTMLInputElement|Element} input @@ -13361,6 +13478,16 @@ function getInputSubtype(input) { return getSubtypeFromType(type); } +/** + * Retrieves the input variant + * @param {HTMLInputElement|Element} input + * @returns {SupportedVariants | ''} + */ +function getInputVariant(input) { + const type = getInputType(input); + return getVariantFromType(type); +} + /** * Remove whitespace of more than 2 in a row and trim the string * @param {string | null} string @@ -13368,9 +13495,10 @@ function getInputSubtype(input) { */ const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + string = string?.trim() || ''; // The length check is extra safety to avoid trimming strings that would be discarded anyway if (!string || string.length > TEXT_LENGTH_CUTOFF + 50) return ''; - return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); + return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' '); }; /** @@ -14699,7 +14827,7 @@ class DataHTMLTooltip extends _HTMLTooltip.default { // Only show manage Manage… when it's topAutofill, the provider is unlocked, and it's not just EmailProtection const shouldShowManageButton = isTopAutofill && items.some(item => !['personalAddress', 'privateAddress', _Credentials.PROVIDER_LOCKED].includes(item.id())); const topClass = wrapperClass || ''; - const dataTypeClass = `tooltip__button--data--${config.type}`; + const dataTypeClass = `tooltip__button--data--${config.type}${this.variant ? '__' + this.variant : ''}`; this.shadow.innerHTML = ` ${css}