1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/**
6 * Machine learning model for identifying new password input elements
7 * using Fathom.
8 */
9
10const EXPORTED_SYMBOLS = ["NewPasswordModel"];
11
12ChromeUtils.defineModuleGetter(
13  this,
14  "fathom",
15  "resource://gre/modules/third_party/fathom/fathom.jsm"
16);
17
18const {
19  dom,
20  element,
21  out,
22  rule,
23  ruleset,
24  score,
25  type,
26  utils: { identity, isVisible, min, setDefault },
27  clusters: { euclidean },
28} = fathom;
29
30/**
31 * ----- Start of model -----
32 *
33 * Everything below this comment up to the "End of model" comment is copied from:
34 * https://github.com/mozilla-services/fathom-login-forms/blob/78d4bf8f301b5aa6d62c06b45e826a0dd9df1afa/new-password/rulesets.js#L14-L613
35 * Deviations from that file:
36 *   - Remove import statements, instead using ``ChromeUtils.defineModuleGetter`` and destructuring assignments above.
37 *   - Set ``DEVELOPMENT`` constant to ``false``.
38 */
39
40// Whether this is running in the Vectorizer, rather than in-application, in a
41// privileged Chrome context
42const DEVELOPMENT = false;
43
44// Run me with confidence cutoff = 0.75.
45const coefficients = {
46  new: [
47    ["hasNewLabel", 2.9195094108581543],
48    ["hasConfirmLabel", 2.1672143936157227],
49    ["hasCurrentLabel", -2.1813206672668457],
50    ["closestLabelMatchesNew", 2.965045213699341],
51    ["closestLabelMatchesConfirm", 2.698647975921631],
52    ["closestLabelMatchesCurrent", -2.147423505783081],
53    ["hasNewAriaLabel", 2.8312134742736816],
54    ["hasConfirmAriaLabel", 1.5153108835220337],
55    ["hasCurrentAriaLabel", -4.368860244750977],
56    ["hasNewPlaceholder", 1.4374250173568726],
57    ["hasConfirmPlaceholder", 1.717592477798462],
58    ["hasCurrentPlaceholder", -1.9401700496673584],
59    ["forgotPasswordInFormLinkTextContent", -0.6736700534820557],
60    ["forgotPasswordInFormLinkHref", -1.3025357723236084],
61    ["forgotPasswordInFormLinkTitle", -2.9019577503204346],
62    ["forgotInFormLinkTextContent", -1.2455425262451172],
63    ["forgotInFormLinkHref", 0.4884686768054962],
64    ["forgotPasswordInFormButtonTextContent", -0.8015769720077515],
65    ["forgotPasswordOnPageLinkTextContent", 0.04422328248620033],
66    ["forgotPasswordOnPageLinkHref", -1.0331494808197021],
67    ["forgotPasswordOnPageLinkTitle", -0.08798415213823318],
68    ["forgotPasswordOnPageButtonTextContent", -1.5396910905838013],
69    ["elementAttrsMatchNew", 2.8492355346679688],
70    ["elementAttrsMatchConfirm", 1.9043376445770264],
71    ["elementAttrsMatchCurrent", -2.056903839111328],
72    ["elementAttrsMatchPassword1", 1.5833512544631958],
73    ["elementAttrsMatchPassword2", 1.3928000926971436],
74    ["elementAttrsMatchLogin", 1.738782525062561],
75    ["formAttrsMatchRegister", 2.1345033645629883],
76    ["formHasRegisterAction", 1.9337323904037476],
77    ["formButtonIsRegister", 3.0930404663085938],
78    ["formAttrsMatchLogin", -0.5816961526870728],
79    ["formHasLoginAction", -0.18886367976665497],
80    ["formButtonIsLogin", -2.332860231399536],
81    ["hasAutocompleteCurrentPassword", -0.029974736273288727],
82    ["formHasRememberMeCheckbox", 0.8600837588310242],
83    ["formHasRememberMeLabel", 0.06663893908262253],
84    ["formHasNewsletterCheckbox", -1.4851698875427246],
85    ["formHasNewsletterLabel", 2.416919231414795],
86    ["closestHeaderAboveIsLoginy", -2.0047383308410645],
87    ["closestHeaderAboveIsRegistery", 2.19451642036438],
88    ["nextInputIsConfirmy", 2.5344431400299072],
89    ["formHasMultipleVisibleInput", 2.81270694732666],
90    ["firstFieldInFormWithThreePasswordFields", -2.8964080810546875],
91  ],
92};
93
94const biases = [["new", -1.3525885343551636]];
95
96const passwordStringRegex = /password|passwort|رمز عبور|mot de passe|パスワード|비밀번호|암호|wachtwoord|senha|Пароль|parol|密码|contraseña|heslo|كلمة السر|kodeord|Κωδικός|pass code|Kata sandi|hasło|รหัสผ่าน|Şifre/i;
97const passwordAttrRegex = /pw|pwd|passwd|pass/i;
98const newStringRegex = /new|erstellen|create|choose|設定|신규|Créer|Nouveau|baru|nouă|nieuw/i;
99const newAttrRegex = /new/i;
100const 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;
101const confirmAttrRegex = /confirm|retype/i;
102const currentAttrAndStringRegex = /current|old|aktuelles|derzeitiges|当前|Atual|actuel|curentă|sekarang/i;
103const forgotStringRegex = /vergessen|vergeten|forgot|oublié|dimenticata|Esqueceu|esqueci|Забыли|忘记|找回|Zapomenuté|lost|忘れた|忘れられた|忘れの方|재설정|찾기|help|فراموشی| را فراموش کرده اید|Восстановить|Unuttu|perdus|重新設定|reset|recover|change|remind|find|request|restore|trouble/i;
104const forgotHrefRegex = /forgot|reset|recover|change|lost|remind|find|request|restore/i;
105const password1Regex = /pw1|pwd1|pass1|passwd1|password1|pwone|pwdone|passone|passwdone|passwordone|pwfirst|pwdfirst|passfirst|passwdfirst|passwordfirst/i;
106const password2Regex = /pw2|pwd2|pass2|passwd2|password2|pwtwo|pwdtwo|passtwo|passwdtwo|passwordtwo|pwsecond|pwdsecond|passsecond|passwdsecond|passwordsecond/i;
107const 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;
108const loginFormAttrRegex = /login|log in|log on|log-on|sign in|sigin|sign\/in|sign-in|sign on|sign-on/i;
109const 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;
110const registerActionRegex = /register|signup|sign-up|create-account|account\/create|join|new_account|user\/create|sign\/up|membership\/create/i;
111const 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;
112const rememberMeAttrRegex = /remember|auto_login|auto-login|save_mail|save-mail|ricordami|manter|mantenha|savelogin|auto login/i;
113const 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;
114const newsletterStringRegex = /newsletter|ニュースレター/i;
115const passwordStringAndAttrRegex = new RegExp(
116  passwordStringRegex.source + "|" + passwordAttrRegex.source,
117  "i"
118);
119
120function makeRuleset(coeffs, biases) {
121  // HTMLElement => (selector => Array<HTMLElement>) nested map to cache querySelectorAll calls.
122  let elementToSelectors;
123  // We want to clear the cache each time the model is executed to get the latest DOM snapshot
124  // for each classification.
125  function clearCache() {
126    // WeakMaps do not have a clear method
127    elementToSelectors = new WeakMap();
128  }
129
130  function hasLabelMatchingRegex(element, regex) {
131    // Check element.labels
132    const labels = element.labels;
133    // TODO: Should I be concerned with multiple labels?
134    if (labels !== null && labels.length) {
135      return regex.test(labels[0].textContent);
136    }
137
138    // Check element.aria-labelledby
139    let labelledBy = element.getAttribute("aria-labelledby");
140    if (labelledBy !== null) {
141      labelledBy = labelledBy
142        .split(" ")
143        .map(id => element.getRootNode().getElementById(id))
144        .filter(el => el);
145      if (labelledBy.length === 1) {
146        return regex.test(labelledBy[0].textContent);
147      } else if (labelledBy.length > 1) {
148        return regex.test(
149          min(labelledBy, node => euclidean(node, element)).textContent
150        );
151      }
152    }
153
154    const parentElement = element.parentElement;
155    // Bug 1634819: element.parentElement is null if element.parentNode is a ShadowRoot
156    if (!parentElement) {
157      return false;
158    }
159    // Check if the input is in a <td>, and, if so, check the textContent of the containing <tr>
160    if (parentElement.tagName === "TD" && parentElement.parentElement) {
161      // TODO: How bad is the assumption that the <tr> won't be the parent of the <td>?
162      return regex.test(parentElement.parentElement.textContent);
163    }
164
165    // Check if the input is in a <dd>, and, if so, check the textContent of the preceding <dt>
166    if (
167      parentElement.tagName === "DD" &&
168      // previousElementSibling can be null
169      parentElement.previousElementSibling
170    ) {
171      return regex.test(parentElement.previousElementSibling.textContent);
172    }
173    return false;
174  }
175
176  function closestLabelMatchesRegex(element, regex) {
177    const previousElementSibling = element.previousElementSibling;
178    if (
179      previousElementSibling !== null &&
180      previousElementSibling.tagName === "LABEL"
181    ) {
182      return regex.test(previousElementSibling.textContent);
183    }
184
185    const nextElementSibling = element.nextElementSibling;
186    if (nextElementSibling !== null && nextElementSibling.tagName === "LABEL") {
187      return regex.test(nextElementSibling.textContent);
188    }
189
190    const closestLabelWithinForm = closestSelectorElementWithinElement(
191      element,
192      element.form,
193      "label"
194    );
195    return containsRegex(
196      regex,
197      closestLabelWithinForm,
198      closestLabelWithinForm => closestLabelWithinForm.textContent
199    );
200  }
201
202  function containsRegex(regex, thingOrNull, thingToString = identity) {
203    return thingOrNull !== null && regex.test(thingToString(thingOrNull));
204  }
205
206  function closestSelectorElementWithinElement(
207    toElement,
208    withinElement,
209    querySelector
210  ) {
211    if (withinElement !== null) {
212      let nodeList = Array.from(withinElement.querySelectorAll(querySelector));
213      if (nodeList.length) {
214        return min(nodeList, node => euclidean(node, toElement));
215      }
216    }
217    return null;
218  }
219
220  function hasAriaLabelMatchingRegex(element, regex) {
221    return containsRegex(regex, element.getAttribute("aria-label"));
222  }
223
224  function hasPlaceholderMatchingRegex(element, regex) {
225    return containsRegex(regex, element.getAttribute("placeholder"));
226  }
227
228  function testRegexesAgainstAnchorPropertyWithinElement(
229    property,
230    element,
231    ...regexes
232  ) {
233    return hasSomeMatchingPredicateForSelectorWithinElement(
234      element,
235      "a",
236      anchor => {
237        const propertyValue = anchor[property];
238        return regexes.every(regex => regex.test(propertyValue));
239      }
240    );
241  }
242
243  function testFormButtonsAgainst(element, stringRegex) {
244    const form = element.form;
245    if (form !== null) {
246      let inputs = Array.from(
247        form.querySelectorAll("input[type=submit],input[type=button]")
248      );
249      inputs = inputs.filter(input => {
250        return stringRegex.test(input.value);
251      });
252      if (inputs.length) {
253        return true;
254      }
255
256      return hasSomeMatchingPredicateForSelectorWithinElement(
257        form,
258        "button",
259        button => {
260          return (
261            stringRegex.test(button.value) ||
262            stringRegex.test(button.textContent) ||
263            stringRegex.test(button.id) ||
264            stringRegex.test(button.title)
265          );
266        }
267      );
268    }
269    return false;
270  }
271
272  function hasAutocompleteCurrentPassword(fnode) {
273    return fnode.element.autocomplete === "current-password";
274  }
275
276  // Check cache before calling querySelectorAll on element
277  function getElementDescendants(element, selector) {
278    // Use the element to look up the selector map:
279    const selectorToDescendants = setDefault(
280      elementToSelectors,
281      element,
282      () => new Map()
283    );
284
285    // Use the selector to grab the descendants:
286    return setDefault(
287      selectorToDescendants, // eslint-disable-line prettier/prettier
288      selector,
289      () => Array.from(element.querySelectorAll(selector))
290    );
291  }
292
293  /**
294   * Return whether the form element directly after this one looks like a
295   * confirm-password input.
296   */
297  function nextInputIsConfirmy(fnode) {
298    const form = fnode.element.form;
299    const me = fnode.element;
300    if (form !== null) {
301      let afterMe = false;
302      for (const formEl of form.elements) {
303        if (formEl === me) {
304          afterMe = true;
305        } else if (afterMe) {
306          if (
307            formEl.type === "password" &&
308            !formEl.disabled &&
309            formEl.getAttribute("aria-hidden") !== "true"
310          ) {
311            // Now we're looking at a passwordy, visible input[type=password]
312            // directly after me.
313            return elementAttrsMatchRegex(formEl, confirmAttrRegex);
314            // We could check other confirmy smells as well. Balance accuracy
315            // against time and complexity.
316          }
317          // We look only at the very next element, so we may be thrown off by
318          // Hide buttons and such.
319          break;
320        }
321      }
322    }
323    return false;
324  }
325
326  /**
327   * Returns true when the number of visible input found in the form is over
328   * the given threshold.
329   *
330   * Since the idea in the signal is based on the fact that registration pages
331   * often have multiple inputs, this rule only selects inputs whose type is
332   * either email, password, text, tel or empty, which are more likely a input
333   * field for users to fill their information.
334   */
335  function formHasMultipleVisibleInput(element, selector, threshold) {
336    let form = element.form;
337    if (!form) {
338      // For password fields that don't have an associated form, we apply a heuristic
339      // to find a "form" for it. The heuristic works as follow:
340      // 1. Locate the closest preceding input.
341      // 2. Find the lowest common ancestor of the password field and the closet
342      //    preceding input.
343      // 3. Assume the common ancestor is the "form" of the password input.
344      const previous = closestElementAbove(element, selector);
345      if (!previous) {
346        return false;
347      }
348      form = findLowestCommonAncestor(previous, element);
349      if (!form) {
350        return false;
351      }
352    }
353    const inputs = Array.from(form.querySelectorAll(selector));
354    for (const input of inputs) {
355      // don't need to check visibility for the element we're testing against
356      if (element === input || isVisible(input)) {
357        threshold--;
358        if (threshold === 0) {
359          return true;
360        }
361      }
362    }
363    return false;
364  }
365
366  /**
367   * Returns true when there are three password fields in the form and the passed
368   * element is the first one.
369   *
370   * The signal is based on that change-password forms with 3 password fields often
371   * have the "current password", "new password", and "confirm password" pattern.
372   */
373  function firstFieldInFormWithThreePasswordFields(fnode) {
374    const element = fnode.element;
375    const form = element.form;
376    if (form) {
377      let elements = form.querySelectorAll(
378        "input[type=password]:not([disabled], [aria-hidden=true])"
379      );
380      // Only care forms with three password fields. If there are more than three password
381      // fields found, probably we include some hidden fields, so just ignore it.
382      if (elements.length == 3 && elements[0] == element) {
383        return true;
384      }
385    }
386    return false;
387  }
388
389  function hasSomeMatchingPredicateForSelectorWithinElement(
390    element,
391    selector,
392    matchingPredicate
393  ) {
394    if (element === null) {
395      return false;
396    }
397    const elements = getElementDescendants(element, selector);
398    return elements.some(matchingPredicate);
399  }
400
401  function textContentMatchesRegexes(element, ...regexes) {
402    const textContent = element.textContent;
403    return regexes.every(regex => regex.test(textContent));
404  }
405
406  function closestHeaderAboveMatchesRegex(element, regex) {
407    const closestHeader = closestElementAbove(
408      element,
409      "h1,h2,h3,h4,h5,h6,div[class*=heading],div[class*=header],div[class*=title],legend"
410    );
411    if (closestHeader !== null) {
412      return regex.test(closestHeader.textContent);
413    }
414    return false;
415  }
416
417  function closestElementAbove(element, selector) {
418    let elements = Array.from(element.ownerDocument.querySelectorAll(selector));
419    for (let i = elements.length - 1; i >= 0; --i) {
420      if (
421        element.compareDocumentPosition(elements[i]) &
422        Node.DOCUMENT_POSITION_PRECEDING
423      ) {
424        return elements[i];
425      }
426    }
427    return null;
428  }
429
430  function findLowestCommonAncestor(elementA, elementB) {
431    // Walk up the ancestor chain of both elements and compare whether the
432    // ancestors in the depth are the same. If they are not the same, the
433    // ancestor in the previous run is the lowest common ancestor.
434    function getAncestorChain(element) {
435      let ancestors = [];
436      let p = element.parentNode;
437      while (p) {
438        ancestors.push(p);
439        p = p.parentNode;
440      }
441      return ancestors;
442    }
443
444    let aAncestors = getAncestorChain(elementA);
445    let bAncestors = getAncestorChain(elementB);
446    let posA = aAncestors.length - 1;
447    let posB = bAncestors.length - 1;
448    for (; posA >= 0 && posB >= 0; posA--, posB--) {
449      if (aAncestors[posA] != bAncestors[posB]) {
450        return aAncestors[posA + 1];
451      }
452    }
453    return null;
454  }
455
456  function elementAttrsMatchRegex(element, regex) {
457    if (element !== null) {
458      return (
459        regex.test(element.id) ||
460        regex.test(element.name) ||
461        regex.test(element.className)
462      );
463    }
464    return false;
465  }
466
467  /**
468   * Let us compactly represent a collection of rules that all take a single
469   * type with no .when() clause and have only a score() call on the right-hand
470   * side.
471   */
472  function* simpleScoringRulesTakingType(inType, ruleMap) {
473    for (const [name, scoringCallback] of Object.entries(ruleMap)) {
474      yield rule(type(inType), score(scoringCallback), { name });
475    }
476  }
477
478  return ruleset(
479    [
480      rule(
481        DEVELOPMENT
482          ? dom(
483              "input[type=password]:not([disabled], [aria-hidden=true])"
484            ).when(isVisible)
485          : element("input"),
486        type("new").note(clearCache)
487      ),
488      ...simpleScoringRulesTakingType("new", {
489        hasNewLabel: fnode =>
490          hasLabelMatchingRegex(fnode.element, newStringRegex),
491        hasConfirmLabel: fnode =>
492          hasLabelMatchingRegex(fnode.element, confirmStringRegex),
493        hasCurrentLabel: fnode =>
494          hasLabelMatchingRegex(fnode.element, currentAttrAndStringRegex),
495        closestLabelMatchesNew: fnode =>
496          closestLabelMatchesRegex(fnode.element, newStringRegex),
497        closestLabelMatchesConfirm: fnode =>
498          closestLabelMatchesRegex(fnode.element, confirmStringRegex),
499        closestLabelMatchesCurrent: fnode =>
500          closestLabelMatchesRegex(fnode.element, currentAttrAndStringRegex),
501        hasNewAriaLabel: fnode =>
502          hasAriaLabelMatchingRegex(fnode.element, newStringRegex),
503        hasConfirmAriaLabel: fnode =>
504          hasAriaLabelMatchingRegex(fnode.element, confirmStringRegex),
505        hasCurrentAriaLabel: fnode =>
506          hasAriaLabelMatchingRegex(fnode.element, currentAttrAndStringRegex),
507        hasNewPlaceholder: fnode =>
508          hasPlaceholderMatchingRegex(fnode.element, newStringRegex),
509        hasConfirmPlaceholder: fnode =>
510          hasPlaceholderMatchingRegex(fnode.element, confirmStringRegex),
511        hasCurrentPlaceholder: fnode =>
512          hasPlaceholderMatchingRegex(fnode.element, currentAttrAndStringRegex),
513        forgotPasswordInFormLinkTextContent: fnode =>
514          testRegexesAgainstAnchorPropertyWithinElement(
515            "textContent",
516            fnode.element.form,
517            passwordStringRegex,
518            forgotStringRegex
519          ),
520        forgotPasswordInFormLinkHref: fnode =>
521          testRegexesAgainstAnchorPropertyWithinElement(
522            "href",
523            fnode.element.form,
524            passwordStringAndAttrRegex,
525            forgotHrefRegex
526          ),
527        forgotPasswordInFormLinkTitle: fnode =>
528          testRegexesAgainstAnchorPropertyWithinElement(
529            "title",
530            fnode.element.form,
531            passwordStringRegex,
532            forgotStringRegex
533          ),
534        forgotInFormLinkTextContent: fnode =>
535          testRegexesAgainstAnchorPropertyWithinElement(
536            "textContent",
537            fnode.element.form,
538            forgotStringRegex
539          ),
540        forgotInFormLinkHref: fnode =>
541          testRegexesAgainstAnchorPropertyWithinElement(
542            "href",
543            fnode.element.form,
544            forgotHrefRegex
545          ),
546        forgotPasswordInFormButtonTextContent: fnode =>
547          hasSomeMatchingPredicateForSelectorWithinElement(
548            fnode.element.form,
549            "button",
550            button =>
551              textContentMatchesRegexes(
552                button,
553                passwordStringRegex,
554                forgotStringRegex
555              )
556          ),
557        forgotPasswordOnPageLinkTextContent: fnode =>
558          testRegexesAgainstAnchorPropertyWithinElement(
559            "textContent",
560            fnode.element.ownerDocument,
561            passwordStringRegex,
562            forgotStringRegex
563          ),
564        forgotPasswordOnPageLinkHref: fnode =>
565          testRegexesAgainstAnchorPropertyWithinElement(
566            "href",
567            fnode.element.ownerDocument,
568            passwordStringAndAttrRegex,
569            forgotHrefRegex
570          ),
571        forgotPasswordOnPageLinkTitle: fnode =>
572          testRegexesAgainstAnchorPropertyWithinElement(
573            "title",
574            fnode.element.ownerDocument,
575            passwordStringRegex,
576            forgotStringRegex
577          ),
578        forgotPasswordOnPageButtonTextContent: fnode =>
579          hasSomeMatchingPredicateForSelectorWithinElement(
580            fnode.element.ownerDocument,
581            "button",
582            button =>
583              textContentMatchesRegexes(
584                button,
585                passwordStringRegex,
586                forgotStringRegex
587              )
588          ),
589        elementAttrsMatchNew: fnode =>
590          elementAttrsMatchRegex(fnode.element, newAttrRegex),
591        elementAttrsMatchConfirm: fnode =>
592          elementAttrsMatchRegex(fnode.element, confirmAttrRegex),
593        elementAttrsMatchCurrent: fnode =>
594          elementAttrsMatchRegex(fnode.element, currentAttrAndStringRegex),
595        elementAttrsMatchPassword1: fnode =>
596          elementAttrsMatchRegex(fnode.element, password1Regex),
597        elementAttrsMatchPassword2: fnode =>
598          elementAttrsMatchRegex(fnode.element, password2Regex),
599        elementAttrsMatchLogin: fnode =>
600          elementAttrsMatchRegex(fnode.element, loginRegex),
601        formAttrsMatchRegister: fnode =>
602          elementAttrsMatchRegex(fnode.element.form, registerFormAttrRegex),
603        formHasRegisterAction: fnode =>
604          containsRegex(
605            registerActionRegex,
606            fnode.element.form,
607            form => form.action
608          ),
609        formButtonIsRegister: fnode =>
610          testFormButtonsAgainst(fnode.element, registerStringRegex),
611        formAttrsMatchLogin: fnode =>
612          elementAttrsMatchRegex(fnode.element.form, loginFormAttrRegex),
613        formHasLoginAction: fnode =>
614          containsRegex(loginRegex, fnode.element.form, form => form.action),
615        formButtonIsLogin: fnode =>
616          testFormButtonsAgainst(fnode.element, loginRegex),
617        hasAutocompleteCurrentPassword,
618        formHasRememberMeCheckbox: fnode =>
619          hasSomeMatchingPredicateForSelectorWithinElement(
620            fnode.element.form,
621            "input[type=checkbox]",
622            checkbox =>
623              rememberMeAttrRegex.test(checkbox.id) ||
624              rememberMeAttrRegex.test(checkbox.name)
625          ),
626        formHasRememberMeLabel: fnode =>
627          hasSomeMatchingPredicateForSelectorWithinElement(
628            fnode.element.form,
629            "label",
630            label => rememberMeStringRegex.test(label.textContent)
631          ),
632        formHasNewsletterCheckbox: fnode =>
633          hasSomeMatchingPredicateForSelectorWithinElement(
634            fnode.element.form,
635            "input[type=checkbox]",
636            checkbox =>
637              checkbox.id.includes("newsletter") ||
638              checkbox.name.includes("newsletter")
639          ),
640        formHasNewsletterLabel: fnode =>
641          hasSomeMatchingPredicateForSelectorWithinElement(
642            fnode.element.form,
643            "label",
644            label => newsletterStringRegex.test(label.textContent)
645          ),
646        closestHeaderAboveIsLoginy: fnode =>
647          closestHeaderAboveMatchesRegex(fnode.element, loginRegex),
648        closestHeaderAboveIsRegistery: fnode =>
649          closestHeaderAboveMatchesRegex(fnode.element, registerStringRegex),
650        nextInputIsConfirmy,
651        formHasMultipleVisibleInput: fnode =>
652          formHasMultipleVisibleInput(
653            fnode.element,
654            "input[type=email],input[type=password],input[type=text],input[type=tel]",
655            3
656          ),
657        firstFieldInFormWithThreePasswordFields,
658      }),
659      rule(type("new"), out("new")),
660    ],
661    coeffs,
662    biases
663  );
664}
665
666/*
667 * ----- End of model -----
668 */
669
670this.NewPasswordModel = {
671  type: "new",
672  rules: makeRuleset([...coefficients.new], biases),
673};
674