From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- .../passwordmgr/NewPasswordModel.sys.mjs | 681 +++++++++++++++++++++ 1 file changed, 681 insertions(+) create mode 100644 toolkit/components/passwordmgr/NewPasswordModel.sys.mjs (limited to 'toolkit/components/passwordmgr/NewPasswordModel.sys.mjs') diff --git a/toolkit/components/passwordmgr/NewPasswordModel.sys.mjs b/toolkit/components/passwordmgr/NewPasswordModel.sys.mjs new file mode 100644 index 0000000000..142b2e1662 --- /dev/null +++ b/toolkit/components/passwordmgr/NewPasswordModel.sys.mjs @@ -0,0 +1,681 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Machine learning model for identifying new password input elements + * using Fathom. + */ + +import { + dom, + element, + out, + rule, + ruleset, + score, + type, + utils, + clusters, +} from "resource://gre/modules/third_party/fathom/fathom.mjs"; + +let { identity, isVisible, min, setDefault } = utils; +let { euclidean } = clusters; + +/** + * ----- Start of model ----- + * + * Everything below this comment up to the "End of model" comment is copied from: + * https://github.com/mozilla-services/fathom-login-forms/blob/78d4bf8f301b5aa6d62c06b45e826a0dd9df1afa/new-password/rulesets.js#L14-L613 + * Deviations from that file: + * - Remove import statements, instead using ``ChromeUtils.defineModuleGetter`` and destructuring assignments above. + * - Set ``DEVELOPMENT`` constant to ``false``. + */ + +// Whether this is running in the Vectorizer, rather than in-application, in a +// privileged Chrome context +const DEVELOPMENT = false; + +// Run me with confidence cutoff = 0.75. +const coefficients = { + new: [ + ["hasNewLabel", 2.9195094108581543], + ["hasConfirmLabel", 2.1672143936157227], + ["hasCurrentLabel", -2.1813206672668457], + ["closestLabelMatchesNew", 2.965045213699341], + ["closestLabelMatchesConfirm", 2.698647975921631], + ["closestLabelMatchesCurrent", -2.147423505783081], + ["hasNewAriaLabel", 2.8312134742736816], + ["hasConfirmAriaLabel", 1.5153108835220337], + ["hasCurrentAriaLabel", -4.368860244750977], + ["hasNewPlaceholder", 1.4374250173568726], + ["hasConfirmPlaceholder", 1.717592477798462], + ["hasCurrentPlaceholder", -1.9401700496673584], + ["forgotPasswordInFormLinkTextContent", -0.6736700534820557], + ["forgotPasswordInFormLinkHref", -1.3025357723236084], + ["forgotPasswordInFormLinkTitle", -2.9019577503204346], + ["forgotInFormLinkTextContent", -1.2455425262451172], + ["forgotInFormLinkHref", 0.4884686768054962], + ["forgotPasswordInFormButtonTextContent", -0.8015769720077515], + ["forgotPasswordOnPageLinkTextContent", 0.04422328248620033], + ["forgotPasswordOnPageLinkHref", -1.0331494808197021], + ["forgotPasswordOnPageLinkTitle", -0.08798415213823318], + ["forgotPasswordOnPageButtonTextContent", -1.5396910905838013], + ["elementAttrsMatchNew", 2.8492355346679688], + ["elementAttrsMatchConfirm", 1.9043376445770264], + ["elementAttrsMatchCurrent", -2.056903839111328], + ["elementAttrsMatchPassword1", 1.5833512544631958], + ["elementAttrsMatchPassword2", 1.3928000926971436], + ["elementAttrsMatchLogin", 1.738782525062561], + ["formAttrsMatchRegister", 2.1345033645629883], + ["formHasRegisterAction", 1.9337323904037476], + ["formButtonIsRegister", 3.0930404663085938], + ["formAttrsMatchLogin", -0.5816961526870728], + ["formHasLoginAction", -0.18886367976665497], + ["formButtonIsLogin", -2.332860231399536], + ["hasAutocompleteCurrentPassword", -0.029974736273288727], + ["formHasRememberMeCheckbox", 0.8600837588310242], + ["formHasRememberMeLabel", 0.06663893908262253], + ["formHasNewsletterCheckbox", -1.4851698875427246], + ["formHasNewsletterLabel", 2.416919231414795], + ["closestHeaderAboveIsLoginy", -2.0047383308410645], + ["closestHeaderAboveIsRegistery", 2.19451642036438], + ["nextInputIsConfirmy", 2.5344431400299072], + ["formHasMultipleVisibleInput", 2.81270694732666], + ["firstFieldInFormWithThreePasswordFields", -2.8964080810546875], + ], +}; + +const biases = [["new", -1.3525885343551636]]; + +const passwordStringRegex = + /password|passwort|رمز عبور|mot de passe|パスワード|비밀번호|암호|wachtwoord|senha|Пароль|parol|密码|contraseña|heslo|كلمة السر|kodeord|Κωδικός|pass code|Kata sandi|hasło|รหัสผ่าน|Şifre/i; +const passwordAttrRegex = /pw|pwd|passwd|pass/i; +const newStringRegex = + /new|erstellen|create|choose|設定|신규|Créer|Nouveau|baru|nouă|nieuw/i; +const newAttrRegex = /new/i; +const confirmStringRegex = + /wiederholen|wiederholung|confirm|repeat|confirmation|verify|retype|repite|確認|の確認|تکرار|re-enter|확인|bevestigen|confirme|Повторите|tassyklamak|再次输入|ještě jednou|gentag|re-type|confirmar|Répéter|conferma|Repetaţi|again|reenter|再入力|재입력|Ulangi|Bekræft/i; +const confirmAttrRegex = /confirm|retype/i; +const currentAttrAndStringRegex = + /current|old|aktuelles|derzeitiges|当前|Atual|actuel|curentă|sekarang/i; +const forgotStringRegex = + /vergessen|vergeten|forgot|oublié|dimenticata|Esqueceu|esqueci|Забыли|忘记|找回|Zapomenuté|lost|忘れた|忘れられた|忘れの方|재설정|찾기|help|فراموشی| را فراموش کرده اید|Восстановить|Unuttu|perdus|重新設定|reset|recover|change|remind|find|request|restore|trouble/i; +const forgotHrefRegex = + /forgot|reset|recover|change|lost|remind|find|request|restore/i; +const password1Regex = + /pw1|pwd1|pass1|passwd1|password1|pwone|pwdone|passone|passwdone|passwordone|pwfirst|pwdfirst|passfirst|passwdfirst|passwordfirst/i; +const password2Regex = + /pw2|pwd2|pass2|passwd2|password2|pwtwo|pwdtwo|passtwo|passwdtwo|passwordtwo|pwsecond|pwdsecond|passsecond|passwdsecond|passwordsecond/i; +const loginRegex = + /login|log in|log on|log-on|Войти|sign in|sigin|sign\/in|sign-in|sign on|sign-on|ورود|登录|Přihlásit se|Přihlaste|Авторизоваться|Авторизация|entrar|ログイン|로그인|inloggen|Συνδέσου|accedi|ログオン|Giriş Yap|登入|connecter|connectez-vous|Connexion|Вход/i; +const loginFormAttrRegex = + /login|log in|log on|log-on|sign in|sigin|sign\/in|sign-in|sign on|sign-on/i; +const registerStringRegex = + /create[a-zA-Z\s]+account|activate[a-zA-Z\s]+account|Zugang anlegen|Angaben prüfen|Konto erstellen|register|sign up|ثبت نام|登録|注册|cadastr|Зарегистрироваться|Регистрация|Bellige alynmak|تسجيل|ΕΓΓΡΑΦΗΣ|Εγγραφή|Créer mon compte|Créer un compte|Mendaftar|가입하기|inschrijving|Zarejestruj się|Deschideți un cont|Создать аккаунт|ร่วม|Üye Ol|registr|new account|ساخت حساب کاربری|Schrijf je|S'inscrire/i; +const registerActionRegex = + /register|signup|sign-up|create-account|account\/create|join|new_account|user\/create|sign\/up|membership\/create/i; +const registerFormAttrRegex = + /signup|join|register|regform|registration|new_user|AccountCreate|create_customer|CreateAccount|CreateAcct|create-account|reg-form|newuser|new-reg|new-form|new_membership/i; +const rememberMeAttrRegex = + /remember|auto_login|auto-login|save_mail|save-mail|ricordami|manter|mantenha|savelogin|auto login/i; +const rememberMeStringRegex = + /remember me|keep me logged in|keep me signed in|save email address|save id|stay signed in|ricordami|次回からログオンIDの入力を省略する|メールアドレスを保存する|を保存|아이디저장|아이디 저장|로그인 상태 유지|lembrar|manter conectado|mantenha-me conectado|Запомни меня|запомнить меня|Запомните меня|Не спрашивать в следующий раз|下次自动登录|记住我/i; +const newsletterStringRegex = /newsletter|ニュースレター/i; +const passwordStringAndAttrRegex = new RegExp( + passwordStringRegex.source + "|" + passwordAttrRegex.source, + "i" +); + +function makeRuleset(coeffs, biases) { + // HTMLElement => (selector => Array) nested map to cache querySelectorAll calls. + let elementToSelectors; + // We want to clear the cache each time the model is executed to get the latest DOM snapshot + // for each classification. + function clearCache() { + // WeakMaps do not have a clear method + elementToSelectors = new WeakMap(); + } + + function hasLabelMatchingRegex(element, regex) { + // Check element.labels + const labels = element.labels; + // TODO: Should I be concerned with multiple labels? + if (labels !== null && labels.length) { + return regex.test(labels[0].textContent); + } + + // Check element.aria-labelledby + let labelledBy = element.getAttribute("aria-labelledby"); + if (labelledBy !== null) { + labelledBy = labelledBy + .split(" ") + .map(id => element.getRootNode().getElementById(id)) + .filter(el => el); + if (labelledBy.length === 1) { + return regex.test(labelledBy[0].textContent); + } else if (labelledBy.length > 1) { + return regex.test( + min(labelledBy, node => euclidean(node, element)).textContent + ); + } + } + + const parentElement = element.parentElement; + // Bug 1634819: element.parentElement is null if element.parentNode is a ShadowRoot + if (!parentElement) { + return false; + } + // Check if the input is in a , and, if so, check the textContent of the containing + if (parentElement.tagName === "TD" && parentElement.parentElement) { + // TODO: How bad is the assumption that the won't be the parent of the ? + return regex.test(parentElement.parentElement.textContent); + } + + // Check if the input is in a
, and, if so, check the textContent of the preceding
+ if ( + parentElement.tagName === "DD" && + // previousElementSibling can be null + parentElement.previousElementSibling + ) { + return regex.test(parentElement.previousElementSibling.textContent); + } + return false; + } + + function closestLabelMatchesRegex(element, regex) { + const previousElementSibling = element.previousElementSibling; + if ( + previousElementSibling !== null && + previousElementSibling.tagName === "LABEL" + ) { + return regex.test(previousElementSibling.textContent); + } + + const nextElementSibling = element.nextElementSibling; + if (nextElementSibling !== null && nextElementSibling.tagName === "LABEL") { + return regex.test(nextElementSibling.textContent); + } + + const closestLabelWithinForm = closestSelectorElementWithinElement( + element, + element.form, + "label" + ); + return containsRegex( + regex, + closestLabelWithinForm, + closestLabelWithinForm => closestLabelWithinForm.textContent + ); + } + + function containsRegex(regex, thingOrNull, thingToString = identity) { + return thingOrNull !== null && regex.test(thingToString(thingOrNull)); + } + + function closestSelectorElementWithinElement( + toElement, + withinElement, + querySelector + ) { + if (withinElement !== null) { + let nodeList = Array.from(withinElement.querySelectorAll(querySelector)); + if (nodeList.length) { + return min(nodeList, node => euclidean(node, toElement)); + } + } + return null; + } + + function hasAriaLabelMatchingRegex(element, regex) { + return containsRegex(regex, element.getAttribute("aria-label")); + } + + function hasPlaceholderMatchingRegex(element, regex) { + return containsRegex(regex, element.getAttribute("placeholder")); + } + + function testRegexesAgainstAnchorPropertyWithinElement( + property, + element, + ...regexes + ) { + return hasSomeMatchingPredicateForSelectorWithinElement( + element, + "a", + anchor => { + const propertyValue = anchor[property]; + return regexes.every(regex => regex.test(propertyValue)); + } + ); + } + + function testFormButtonsAgainst(element, stringRegex) { + const form = element.form; + if (form !== null) { + let inputs = Array.from( + form.querySelectorAll("input[type=submit],input[type=button]") + ); + inputs = inputs.filter(input => { + return stringRegex.test(input.value); + }); + if (inputs.length) { + return true; + } + + return hasSomeMatchingPredicateForSelectorWithinElement( + form, + "button", + button => { + return ( + stringRegex.test(button.value) || + stringRegex.test(button.textContent) || + stringRegex.test(button.id) || + stringRegex.test(button.title) + ); + } + ); + } + return false; + } + + function hasAutocompleteCurrentPassword(fnode) { + return fnode.element.autocomplete === "current-password"; + } + + // Check cache before calling querySelectorAll on element + function getElementDescendants(element, selector) { + // Use the element to look up the selector map: + const selectorToDescendants = setDefault( + elementToSelectors, + element, + () => new Map() + ); + + // Use the selector to grab the descendants: + return setDefault(selectorToDescendants, selector, () => + Array.from(element.querySelectorAll(selector)) + ); + } + + /** + * Return whether the form element directly after this one looks like a + * confirm-password input. + */ + function nextInputIsConfirmy(fnode) { + const form = fnode.element.form; + const me = fnode.element; + if (form !== null) { + let afterMe = false; + for (const formEl of form.elements) { + if (formEl === me) { + afterMe = true; + } else if (afterMe) { + if ( + formEl.type === "password" && + !formEl.disabled && + formEl.getAttribute("aria-hidden") !== "true" + ) { + // Now we're looking at a passwordy, visible input[type=password] + // directly after me. + return elementAttrsMatchRegex(formEl, confirmAttrRegex); + // We could check other confirmy smells as well. Balance accuracy + // against time and complexity. + } + // We look only at the very next element, so we may be thrown off by + // Hide buttons and such. + break; + } + } + } + return false; + } + + /** + * Returns true when the number of visible input found in the form is over + * the given threshold. + * + * Since the idea in the signal is based on the fact that registration pages + * often have multiple inputs, this rule only selects inputs whose type is + * either email, password, text, tel or empty, which are more likely a input + * field for users to fill their information. + */ + function formHasMultipleVisibleInput(element, selector, threshold) { + let form = element.form; + if (!form) { + // For password fields that don't have an associated form, we apply a heuristic + // to find a "form" for it. The heuristic works as follow: + // 1. Locate the closest preceding input. + // 2. Find the lowest common ancestor of the password field and the closet + // preceding input. + // 3. Assume the common ancestor is the "form" of the password input. + const previous = closestElementAbove(element, selector); + if (!previous) { + return false; + } + form = findLowestCommonAncestor(previous, element); + if (!form) { + return false; + } + } + const inputs = Array.from(form.querySelectorAll(selector)); + for (const input of inputs) { + // don't need to check visibility for the element we're testing against + if (element === input || isVisible(input)) { + threshold--; + if (threshold === 0) { + return true; + } + } + } + return false; + } + + /** + * Returns true when there are three password fields in the form and the passed + * element is the first one. + * + * The signal is based on that change-password forms with 3 password fields often + * have the "current password", "new password", and "confirm password" pattern. + */ + function firstFieldInFormWithThreePasswordFields(fnode) { + const element = fnode.element; + const form = element.form; + if (form) { + let elements = form.querySelectorAll( + "input[type=password]:not([disabled], [aria-hidden=true])" + ); + // Only care forms with three password fields. If there are more than three password + // fields found, probably we include some hidden fields, so just ignore it. + if (elements.length == 3 && elements[0] == element) { + return true; + } + } + return false; + } + + function hasSomeMatchingPredicateForSelectorWithinElement( + element, + selector, + matchingPredicate + ) { + if (element === null) { + return false; + } + const elements = getElementDescendants(element, selector); + return elements.some(matchingPredicate); + } + + function textContentMatchesRegexes(element, ...regexes) { + const textContent = element.textContent; + return regexes.every(regex => regex.test(textContent)); + } + + function closestHeaderAboveMatchesRegex(element, regex) { + const closestHeader = closestElementAbove( + element, + "h1,h2,h3,h4,h5,h6,div[class*=heading],div[class*=header],div[class*=title],legend" + ); + if (closestHeader !== null) { + return regex.test(closestHeader.textContent); + } + return false; + } + + function closestElementAbove(element, selector) { + let elements = Array.from(element.ownerDocument.querySelectorAll(selector)); + for (let i = elements.length - 1; i >= 0; --i) { + if ( + element.compareDocumentPosition(elements[i]) & + Node.DOCUMENT_POSITION_PRECEDING + ) { + return elements[i]; + } + } + return null; + } + + function findLowestCommonAncestor(elementA, elementB) { + // Walk up the ancestor chain of both elements and compare whether the + // ancestors in the depth are the same. If they are not the same, the + // ancestor in the previous run is the lowest common ancestor. + function getAncestorChain(element) { + let ancestors = []; + let p = element.parentNode; + while (p) { + ancestors.push(p); + p = p.parentNode; + } + return ancestors; + } + + let aAncestors = getAncestorChain(elementA); + let bAncestors = getAncestorChain(elementB); + let posA = aAncestors.length - 1; + let posB = bAncestors.length - 1; + for (; posA >= 0 && posB >= 0; posA--, posB--) { + if (aAncestors[posA] != bAncestors[posB]) { + return aAncestors[posA + 1]; + } + } + return null; + } + + function elementAttrsMatchRegex(element, regex) { + if (element !== null) { + return ( + regex.test(element.id) || + regex.test(element.name) || + regex.test(element.className) + ); + } + return false; + } + + /** + * Let us compactly represent a collection of rules that all take a single + * type with no .when() clause and have only a score() call on the right-hand + * side. + */ + function* simpleScoringRulesTakingType(inType, ruleMap) { + for (const [name, scoringCallback] of Object.entries(ruleMap)) { + yield rule(type(inType), score(scoringCallback), { name }); + } + } + + return ruleset( + [ + rule( + DEVELOPMENT + ? dom( + "input[type=password]:not([disabled], [aria-hidden=true])" + ).when(isVisible) + : element("input"), + type("new").note(clearCache) + ), + ...simpleScoringRulesTakingType("new", { + hasNewLabel: fnode => + hasLabelMatchingRegex(fnode.element, newStringRegex), + hasConfirmLabel: fnode => + hasLabelMatchingRegex(fnode.element, confirmStringRegex), + hasCurrentLabel: fnode => + hasLabelMatchingRegex(fnode.element, currentAttrAndStringRegex), + closestLabelMatchesNew: fnode => + closestLabelMatchesRegex(fnode.element, newStringRegex), + closestLabelMatchesConfirm: fnode => + closestLabelMatchesRegex(fnode.element, confirmStringRegex), + closestLabelMatchesCurrent: fnode => + closestLabelMatchesRegex(fnode.element, currentAttrAndStringRegex), + hasNewAriaLabel: fnode => + hasAriaLabelMatchingRegex(fnode.element, newStringRegex), + hasConfirmAriaLabel: fnode => + hasAriaLabelMatchingRegex(fnode.element, confirmStringRegex), + hasCurrentAriaLabel: fnode => + hasAriaLabelMatchingRegex(fnode.element, currentAttrAndStringRegex), + hasNewPlaceholder: fnode => + hasPlaceholderMatchingRegex(fnode.element, newStringRegex), + hasConfirmPlaceholder: fnode => + hasPlaceholderMatchingRegex(fnode.element, confirmStringRegex), + hasCurrentPlaceholder: fnode => + hasPlaceholderMatchingRegex(fnode.element, currentAttrAndStringRegex), + forgotPasswordInFormLinkTextContent: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "textContent", + fnode.element.form, + passwordStringRegex, + forgotStringRegex + ), + forgotPasswordInFormLinkHref: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "href", + fnode.element.form, + passwordStringAndAttrRegex, + forgotHrefRegex + ), + forgotPasswordInFormLinkTitle: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "title", + fnode.element.form, + passwordStringRegex, + forgotStringRegex + ), + forgotInFormLinkTextContent: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "textContent", + fnode.element.form, + forgotStringRegex + ), + forgotInFormLinkHref: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "href", + fnode.element.form, + forgotHrefRegex + ), + forgotPasswordInFormButtonTextContent: fnode => + hasSomeMatchingPredicateForSelectorWithinElement( + fnode.element.form, + "button", + button => + textContentMatchesRegexes( + button, + passwordStringRegex, + forgotStringRegex + ) + ), + forgotPasswordOnPageLinkTextContent: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "textContent", + fnode.element.ownerDocument, + passwordStringRegex, + forgotStringRegex + ), + forgotPasswordOnPageLinkHref: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "href", + fnode.element.ownerDocument, + passwordStringAndAttrRegex, + forgotHrefRegex + ), + forgotPasswordOnPageLinkTitle: fnode => + testRegexesAgainstAnchorPropertyWithinElement( + "title", + fnode.element.ownerDocument, + passwordStringRegex, + forgotStringRegex + ), + forgotPasswordOnPageButtonTextContent: fnode => + hasSomeMatchingPredicateForSelectorWithinElement( + fnode.element.ownerDocument, + "button", + button => + textContentMatchesRegexes( + button, + passwordStringRegex, + forgotStringRegex + ) + ), + elementAttrsMatchNew: fnode => + elementAttrsMatchRegex(fnode.element, newAttrRegex), + elementAttrsMatchConfirm: fnode => + elementAttrsMatchRegex(fnode.element, confirmAttrRegex), + elementAttrsMatchCurrent: fnode => + elementAttrsMatchRegex(fnode.element, currentAttrAndStringRegex), + elementAttrsMatchPassword1: fnode => + elementAttrsMatchRegex(fnode.element, password1Regex), + elementAttrsMatchPassword2: fnode => + elementAttrsMatchRegex(fnode.element, password2Regex), + elementAttrsMatchLogin: fnode => + elementAttrsMatchRegex(fnode.element, loginRegex), + formAttrsMatchRegister: fnode => + elementAttrsMatchRegex(fnode.element.form, registerFormAttrRegex), + formHasRegisterAction: fnode => + containsRegex( + registerActionRegex, + fnode.element.form, + form => form.action + ), + formButtonIsRegister: fnode => + testFormButtonsAgainst(fnode.element, registerStringRegex), + formAttrsMatchLogin: fnode => + elementAttrsMatchRegex(fnode.element.form, loginFormAttrRegex), + formHasLoginAction: fnode => + containsRegex(loginRegex, fnode.element.form, form => form.action), + formButtonIsLogin: fnode => + testFormButtonsAgainst(fnode.element, loginRegex), + hasAutocompleteCurrentPassword, + formHasRememberMeCheckbox: fnode => + hasSomeMatchingPredicateForSelectorWithinElement( + fnode.element.form, + "input[type=checkbox]", + checkbox => + rememberMeAttrRegex.test(checkbox.id) || + rememberMeAttrRegex.test(checkbox.name) + ), + formHasRememberMeLabel: fnode => + hasSomeMatchingPredicateForSelectorWithinElement( + fnode.element.form, + "label", + label => rememberMeStringRegex.test(label.textContent) + ), + formHasNewsletterCheckbox: fnode => + hasSomeMatchingPredicateForSelectorWithinElement( + fnode.element.form, + "input[type=checkbox]", + checkbox => + checkbox.id.includes("newsletter") || + checkbox.name.includes("newsletter") + ), + formHasNewsletterLabel: fnode => + hasSomeMatchingPredicateForSelectorWithinElement( + fnode.element.form, + "label", + label => newsletterStringRegex.test(label.textContent) + ), + closestHeaderAboveIsLoginy: fnode => + closestHeaderAboveMatchesRegex(fnode.element, loginRegex), + closestHeaderAboveIsRegistery: fnode => + closestHeaderAboveMatchesRegex(fnode.element, registerStringRegex), + nextInputIsConfirmy, + formHasMultipleVisibleInput: fnode => + formHasMultipleVisibleInput( + fnode.element, + "input[type=email],input[type=password],input[type=text],input[type=tel]", + 3 + ), + firstFieldInFormWithThreePasswordFields, + }), + rule(type("new"), out("new")), + ], + coeffs, + biases + ); +} + +/* + * ----- End of model ----- + */ + +export const NewPasswordModel = { + type: "new", + rules: makeRuleset([...coefficients.new], biases), +}; -- cgit v1.2.3