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