summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/NewPasswordModel.sys.mjs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/passwordmgr/NewPasswordModel.sys.mjs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/passwordmgr/NewPasswordModel.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/NewPasswordModel.sys.mjs681
1 files changed, 681 insertions, 0 deletions
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<HTMLElement>) 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 <td>, and, if so, check the textContent of the containing <tr>
+ if (parentElement.tagName === "TD" && parentElement.parentElement) {
+ // TODO: How bad is the assumption that the <tr> won't be the parent of the <td>?
+ return regex.test(parentElement.parentElement.textContent);
+ }
+
+ // Check if the input is in a <dd>, and, if so, check the textContent of the preceding <dt>
+ 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),
+};