From 22becb63dbe4cd18da91d4054fcb2abed5f2b1b2 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Thu, 19 Oct 2023 14:47:22 +0200 Subject: [PATCH 1/9] Improve skipping search forms Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 6 ++-- dist/autofill.js | 6 ++-- .../__generated__/compiled-matching-config.js | 6 ++-- src/Form/matching-config/selectors-css.js | 17 +++++----- src/Form/test-cases/asana_search.html | 32 ++++++++++++++++++- .../Resources/assets/autofill-debug.js | 6 ++-- swift-package/Resources/assets/autofill.js | 6 ++-- 7 files changed, 55 insertions(+), 24 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 5875c2c2a..a94821974 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -12026,12 +12026,12 @@ const matchingConfiguration = exports.matchingConfiguration = { strategies: { cssSelector: { selectors: { - genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])', + genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])', submitButtonSelector: 'input[type=submit], input[type=button], input[type=image], button:not([role=switch]):not([role=link]), [role=button], a[href="#"][id*=button i], a[href="#"][id*=btn i]', - formInputsSelector: 'input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]), [autocomplete=username], select', + formInputsSelector: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month]),[autocomplete=username],select', safeUniversalSelector: '*:not(select):not(option):not(script):not(noscript):not(style):not(br)', emailAddress: 'input:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=""][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]), input[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]), input:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=""][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=email], input[type=text][aria-label*=email i]:not([aria-label*=search i]), input:not([type])[aria-label*=email i]:not([aria-label*=search i]), input[name=username][type=email], input[autocomplete=username][type=email], input[autocomplete=username][placeholder*=email i], input[autocomplete=email],input[name="mail_tel" i],input[value=email i]', - username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', + username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', password: 'input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i]):not([name*=card i]):not([name*=cvv i]),input.js-cloudsave-phrase', cardName: 'input[autocomplete="cc-name" i], input[autocomplete="ccname" i], input[name="ccname" i], input[name="cc-name" i], input[name="ppw-accountHolderName" i], input[id*=cardname i], input[id*=card-name i], input[id*=card_name i]', cardNumber: 'input[autocomplete="cc-number" i], input[autocomplete="ccnumber" i], input[autocomplete="cardnumber" i], input[autocomplete="card-number" i], input[name="ccnumber" i], input[name="cc-number" i], input[name*=card i][name*=number i], input[name*=cardnumber i], input[id*=cardnumber i], input[id*=card-number i], input[id*=card_number i]', diff --git a/dist/autofill.js b/dist/autofill.js index f8f022768..9bb2db807 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -8081,12 +8081,12 @@ const matchingConfiguration = exports.matchingConfiguration = { strategies: { cssSelector: { selectors: { - genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])', + genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])', submitButtonSelector: 'input[type=submit], input[type=button], input[type=image], button:not([role=switch]):not([role=link]), [role=button], a[href="#"][id*=button i], a[href="#"][id*=btn i]', - formInputsSelector: 'input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]), [autocomplete=username], select', + formInputsSelector: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month]),[autocomplete=username],select', safeUniversalSelector: '*:not(select):not(option):not(script):not(noscript):not(style):not(br)', emailAddress: 'input:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=""][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]), input[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]), input:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=""][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=email], input[type=text][aria-label*=email i]:not([aria-label*=search i]), input:not([type])[aria-label*=email i]:not([aria-label*=search i]), input[name=username][type=email], input[autocomplete=username][type=email], input[autocomplete=username][placeholder*=email i], input[autocomplete=email],input[name="mail_tel" i],input[value=email i]', - username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', + username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', password: 'input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i]):not([name*=card i]):not([name*=cvv i]),input.js-cloudsave-phrase', cardName: 'input[autocomplete="cc-name" i], input[autocomplete="ccname" i], input[name="ccname" i], input[name="cc-name" i], input[name="ppw-accountHolderName" i], input[id*=cardname i], input[id*=card-name i], input[id*=card_name i]', cardNumber: 'input[autocomplete="cc-number" i], input[autocomplete="ccnumber" i], input[autocomplete="cardnumber" i], input[autocomplete="card-number" i], input[name="ccnumber" i], input[name="cc-number" i], input[name*=card i][name*=number i], input[name*=cardnumber i], input[id*=cardnumber i], input[id*=card-number i], input[id*=card_number i]', diff --git a/src/Form/matching-config/__generated__/compiled-matching-config.js b/src/Form/matching-config/__generated__/compiled-matching-config.js index 363cf4522..bb768c2a7 100644 --- a/src/Form/matching-config/__generated__/compiled-matching-config.js +++ b/src/Form/matching-config/__generated__/compiled-matching-config.js @@ -203,12 +203,12 @@ const matchingConfiguration = { strategies: { cssSelector: { selectors: { - genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])', + genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])', submitButtonSelector: 'input[type=submit], input[type=button], input[type=image], button:not([role=switch]):not([role=link]), [role=button], a[href="#"][id*=button i], a[href="#"][id*=btn i]', - formInputsSelector: 'input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]), [autocomplete=username], select', + formInputsSelector: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month]),[autocomplete=username],select', safeUniversalSelector: '*:not(select):not(option):not(script):not(noscript):not(style):not(br)', emailAddress: 'input:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=""][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]), input[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]), input:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=""][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=email], input[type=text][aria-label*=email i]:not([aria-label*=search i]), input:not([type])[aria-label*=email i]:not([aria-label*=search i]), input[name=username][type=email], input[autocomplete=username][type=email], input[autocomplete=username][placeholder*=email i], input[autocomplete=email],input[name="mail_tel" i],input[value=email i]', - username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', + username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', password: 'input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i]):not([name*=card i]):not([name*=cvv i]),input.js-cloudsave-phrase', cardName: 'input[autocomplete="cc-name" i], input[autocomplete="ccname" i], input[name="ccname" i], input[name="cc-name" i], input[name="ppw-accountHolderName" i], input[id*=cardname i], input[id*=card-name i], input[id*=card_name i]', cardNumber: 'input[autocomplete="cc-number" i], input[autocomplete="ccnumber" i], input[autocomplete="cardnumber" i], input[autocomplete="card-number" i], input[name="ccnumber" i], input[name="cc-number" i], input[name*=card i][name*=number i], input[name*=cardnumber i], input[id*=cardnumber i], input[id*=card-number i], input[id*=card_number i]', diff --git a/src/Form/matching-config/selectors-css.js b/src/Form/matching-config/selectors-css.js index 1ee3d08c7..e567afdf7 100644 --- a/src/Form/matching-config/selectors-css.js +++ b/src/Form/matching-config/selectors-css.js @@ -1,7 +1,12 @@ -const formInputsSelector = ` -input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]), -[autocomplete=username], -select` +// We've seen non-standard types like 'user'. This selector should get them, too +const genericTextField = ` +input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])` + +const formInputsSelector = [ + genericTextField, + '[autocomplete=username]', + 'select' +] const submitButtonSelector = ` input[type=submit], @@ -14,10 +19,6 @@ a[href="#"][id*=btn i]` const safeUniversalSelector = '*:not(select):not(option):not(script):not(noscript):not(style):not(br)' -// We've seen non-standard types like 'user'. This selector should get them, too -const genericTextField = ` -input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])` - const emailAddress = [ ` input:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), diff --git a/src/Form/test-cases/asana_search.html b/src/Form/test-cases/asana_search.html index 133e9f19a..78e472264 100644 --- a/src/Form/test-cases/asana_search.html +++ b/src/Form/test-cases/asana_search.html @@ -1,2 +1,32 @@ -
+
+ + +
diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 5875c2c2a..a94821974 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -12026,12 +12026,12 @@ const matchingConfiguration = exports.matchingConfiguration = { strategies: { cssSelector: { selectors: { - genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])', + genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])', submitButtonSelector: 'input[type=submit], input[type=button], input[type=image], button:not([role=switch]):not([role=link]), [role=button], a[href="#"][id*=button i], a[href="#"][id*=btn i]', - formInputsSelector: 'input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]), [autocomplete=username], select', + formInputsSelector: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month]),[autocomplete=username],select', safeUniversalSelector: '*:not(select):not(option):not(script):not(noscript):not(style):not(br)', emailAddress: 'input:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=""][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]), input[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]), input:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=""][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=email], input[type=text][aria-label*=email i]:not([aria-label*=search i]), input:not([type])[aria-label*=email i]:not([aria-label*=search i]), input[name=username][type=email], input[autocomplete=username][type=email], input[autocomplete=username][placeholder*=email i], input[autocomplete=email],input[name="mail_tel" i],input[value=email i]', - username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', + username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', password: 'input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i]):not([name*=card i]):not([name*=cvv i]),input.js-cloudsave-phrase', cardName: 'input[autocomplete="cc-name" i], input[autocomplete="ccname" i], input[name="ccname" i], input[name="cc-name" i], input[name="ppw-accountHolderName" i], input[id*=cardname i], input[id*=card-name i], input[id*=card_name i]', cardNumber: 'input[autocomplete="cc-number" i], input[autocomplete="ccnumber" i], input[autocomplete="cardnumber" i], input[autocomplete="card-number" i], input[name="ccnumber" i], input[name="cc-number" i], input[name*=card i][name*=number i], input[name*=cardnumber i], input[id*=cardnumber i], input[id*=card-number i], input[id*=card_number i]', diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index f8f022768..9bb2db807 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -8081,12 +8081,12 @@ const matchingConfiguration = exports.matchingConfiguration = { strategies: { cssSelector: { selectors: { - genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])', + genericTextField: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])', submitButtonSelector: 'input[type=submit], input[type=button], input[type=image], button:not([role=switch]):not([role=link]), [role=button], a[href="#"][id*=button i], a[href="#"][id*=btn i]', - formInputsSelector: 'input:not([type=submit]):not([type=button]):not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=search]):not([type=reset]):not([type=image]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]), [autocomplete=username], select', + formInputsSelector: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month]),[autocomplete=username],select', safeUniversalSelector: '*:not(select):not(option):not(script):not(noscript):not(style):not(br)', emailAddress: 'input:not([type])[name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=""][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([type=tel]), input[type=text][name*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=title i]):not([name*=tab i]):not([name*=code i]), input:not([type])[placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]):not([name*=code i]), input[type=text][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=""][placeholder*=email i]:not([placeholder*=search i]):not([placeholder*=filter i]):not([placeholder*=subject i]), input[type=email], input[type=text][aria-label*=email i]:not([aria-label*=search i]), input:not([type])[aria-label*=email i]:not([aria-label*=search i]), input[name=username][type=email], input[autocomplete=username][type=email], input[autocomplete=username][placeholder*=email i], input[autocomplete=email],input[name="mail_tel" i],input[value=email i]', - username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=file]):not([type=hidden]):not([type=month]):not([type=number]):not([type=radio]):not([type=range]):not([type=reset]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', + username: 'input:not([type=button]):not([type=checkbox]):not([type=color]):not([type=file]):not([type=hidden]):not([type=radio]):not([type=range]):not([type=reset]):not([type=image]):not([type=search]):not([type=submit]):not([type=time]):not([type=url]):not([type=week]):not([name^=fake i]):not([data-description^=dummy i]):not([name*=otp]):not([autocomplete="fake"]):not([placeholder^=search i]):not([type=date]):not([type=datetime-local]):not([type=datetime]):not([type=month])[autocomplete^=user i],input[name=username i],input[name="loginId" i],input[name="userid" i],input[id="userid" i],input[name="user_id" i],input[name="user-id" i],input[id="login-id" i],input[id="login_id" i],input[id="loginid" i],input[name="login" i],input[name=accountname i],input[autocomplete=username i],input[name*=accountid i],input[name="j_username" i],input[id="j_username" i],input[name="uwinid" i],input[name="livedoor_id" i],input[name="ssousername" i],input[name="j_userlogin_pwd" i],input[name="user[login]" i],input[name="user" i],input[name$="_username" i],input[id="lmSsoinput" i],input[name="account_subdomain" i],input[name="masterid" i],input[name="tridField" i],input[id="signInName" i],input[id="w3c_accountsbundle_accountrequeststep1_login" i],input[id="username" i],input[name="_user" i],input[name="login_username" i],input[name^="login-user-account" i],input[id="loginusuario" i],input[name="usuario" i],input[id="UserLoginFormUsername" i],input[id="nw_username" i],input[can-field="accountName"],input[placeholder^="username" i]', password: 'input[type=password]:not([autocomplete*=cc]):not([autocomplete=one-time-code]):not([name*=answer i]):not([name*=mfa i]):not([name*=tin i]):not([name*=card i]):not([name*=cvv i]),input.js-cloudsave-phrase', cardName: 'input[autocomplete="cc-name" i], input[autocomplete="ccname" i], input[name="ccname" i], input[name="cc-name" i], input[name="ppw-accountHolderName" i], input[id*=cardname i], input[id*=card-name i], input[id*=card_name i]', cardNumber: 'input[autocomplete="cc-number" i], input[autocomplete="ccnumber" i], input[autocomplete="cardnumber" i], input[autocomplete="card-number" i], input[name="ccnumber" i], input[name="cc-number" i], input[name*=card i][name*=number i], input[name*=cardnumber i], input[id*=cardnumber i], input[id*=card-number i], input[id*=card_number i]', From 30373b74d15dfdb422b522fafda2535597f1910f Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 23 Oct 2023 14:15:07 +0200 Subject: [PATCH 2/9] Limit the upward traversal when searching the form Signed-off-by: Emanuele Feliziani --- src/Scanner.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Scanner.js b/src/Scanner.js index 8457e0b55..bba74a765 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -194,9 +194,14 @@ class DefaultScanner { } } + /** + * Max number of nodes we want to traverse upwards, critical to avoid enclosing large portions of the DOM + * @type {number} + */ + let traversalLayerCount = 0 let element = input // traverse the DOM to search for related inputs - while (element.parentElement && element.parentElement !== document.documentElement) { + while (traversalLayerCount <= 5 && element.parentElement && element.parentElement !== document.documentElement) { // Avoid overlapping containers or forms const siblingForm = element.parentElement?.querySelector('form') if (siblingForm && siblingForm !== element) { @@ -212,6 +217,7 @@ class DefaultScanner { // found related input, return common ancestor return element } + traversalLayerCount++ } return input From e6c4970838fa15d2ee240988c2e0933396486771 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Mon, 23 Oct 2023 14:19:40 +0200 Subject: [PATCH 3/9] Fix type Signed-off-by: Emanuele Feliziani --- src/Scanner.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Scanner.js b/src/Scanner.js index bba74a765..c4da6f937 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -18,6 +18,7 @@ const { * findEligibleInputs(context): Scanner; * matching: import("./Form/matching").Matching; * options: ScannerOptions; + * stopScanner: (reason: string, ...rest: any) => void; * }} Scanner * * @typedef {{ @@ -151,7 +152,7 @@ class DefaultScanner { /** * Stops scanning, switches off the mutation observer and clears all forms * @param {string} reason - * @param {...any} rest + * @param {any} rest */ stopScanner (reason, ...rest) { this.stopped = true From 3e8d9a81299d9f3949ec7597b9e0cde94b8e490d Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Wed, 25 Oct 2023 17:46:17 +0200 Subject: [PATCH 4/9] Re-enable the mutation observer within the form Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 73 ++++++++++++++++--- dist/autofill.js | 73 ++++++++++++++++--- .../tests/mutating-form.macos.spec.js | 2 +- src/Form/Form.js | 58 +++++++++++++-- src/Form/Form.test.js | 2 +- src/Form/matching.js | 3 +- .../Resources/assets/autofill-debug.js | 73 ++++++++++++++++--- swift-package/Resources/assets/autofill.js | 73 ++++++++++++++++--- 8 files changed, 302 insertions(+), 55 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index a94821974..300a7a4c3 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9437,7 +9437,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, - MAX_INPUTS_PER_FORM + MAX_INPUTS_PER_FORM, + MAX_FORM_MUT_OBS_COUNT } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -9485,6 +9486,30 @@ class Form { if (!entry.isIntersecting) this.removeTooltip(); } }); + this.rescanCount = 0; + this.mutObsConfig = { + childList: true, + subtree: true + }; + this.mutObs = new MutationObserver(records => { + const anythingRemoved = records.some(record => record.removedNodes.length > 0); + if (anythingRemoved) { + if (!this.form.isConnected) { + this.destroy(); + return; + } + // Must check for inputs because a parent may be removed and not show up in record.removedNodes + if ([...this.inputs.all].some(input => !input.isConnected)) { + // ADD COMMENT + this.mutObs.disconnect(); + // If any known input has been removed from the DOM, reanalyze the whole form + window.requestIdleCallback(() => { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); + }); + } + } + }); // This ensures we fire the handler again if the form is changed this.addListener(form, 'input', () => { @@ -9703,6 +9728,12 @@ class Form { * Resets our input scoring and starts from scratch */ recategorizeAllInputs() { + // If the form mutates too much, disconnect to avoid performance issues + if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + this.mutObs.disconnect(); + return; + } + this.rescanCount++; this.initialScanComplete = false; this.removeAllDecorations(); this.forgetAllInputs(); @@ -9721,11 +9752,13 @@ class Form { } // This removes all listeners to avoid memory leaks and weird behaviours destroy() { + this.mutObs.disconnect(); this.removeAllDecorations(); this.removeTooltip(); this.forgetAllInputs(); this.matching.clear(); this.intObs = null; + this.device.scanner.forms.delete(this.form); } categorizeInputs() { const selector = this.matching.cssSelector('formInputsSelector'); @@ -9740,12 +9773,17 @@ class Form { if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, bailing.'); - } + // This is rather extreme, but better safe than sorry + this.device.scanner.stopScanner('The form has too many inputs, bailing.'); + return; } } this.initialScanComplete = true; + + // Observe only if the container isn't the body, to avoid performance overloads + if (this.form !== document.body) { + this.mutObs.observe(this.form, this.mutObsConfig); + } } get submitButtons() { const selector = this.matching.cssSelector('submitButtonSelector'); @@ -9798,10 +9836,14 @@ class Form { // If the form has too many inputs, destroy everything to avoid performance issues if (this.inputs.all.size > MAX_INPUTS_PER_FORM) { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, destroying.'); - } - this.destroy(); + this.device.scanner.stopScanner('The form has too many inputs, destroying.'); + return this; + } + + // When new inputs are added after the initial scan, reanalyze the whole form + if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); return this; } @@ -13087,7 +13129,8 @@ function getInputSubtype(input) { */ const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - if (!string) return ''; + // The length check is extra safety to avoid trimming strings that would be discarded anyway + if (!string || string.length > TEXT_LENGTH_CUTOFF + 50) return ''; return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; @@ -13653,6 +13696,7 @@ const { * findEligibleInputs(context): Scanner; * matching: import("./Form/matching").Matching; * options: ScannerOptions; + * stopScanner: (reason: string, ...rest: any) => void; * }} Scanner * * @typedef {{ @@ -13794,7 +13838,7 @@ class DefaultScanner { /** * Stops scanning, switches off the mutation observer and clears all forms * @param {string} reason - * @param {...any} rest + * @param {any} rest */ stopScanner(reason) { this.stopped = true; @@ -13835,9 +13879,15 @@ class DefaultScanner { } } } + + /** + * Max number of nodes we want to traverse upwards, critical to avoid enclosing large portions of the DOM + * @type {number} + */ + let traversalLayerCount = 0; let element = input; // traverse the DOM to search for related inputs - while (element.parentElement && element.parentElement !== document.documentElement) { + while (traversalLayerCount <= 5 && element.parentElement && element.parentElement !== document.documentElement) { // Avoid overlapping containers or forms const siblingForm = element.parentElement?.querySelector('form'); if (siblingForm && siblingForm !== element) { @@ -13851,6 +13901,7 @@ class DefaultScanner { // found related input, return common ancestor return element; } + traversalLayerCount++; } return input; } diff --git a/dist/autofill.js b/dist/autofill.js index 9bb2db807..d927bd6b7 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5492,7 +5492,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, - MAX_INPUTS_PER_FORM + MAX_INPUTS_PER_FORM, + MAX_FORM_MUT_OBS_COUNT } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -5540,6 +5541,30 @@ class Form { if (!entry.isIntersecting) this.removeTooltip(); } }); + this.rescanCount = 0; + this.mutObsConfig = { + childList: true, + subtree: true + }; + this.mutObs = new MutationObserver(records => { + const anythingRemoved = records.some(record => record.removedNodes.length > 0); + if (anythingRemoved) { + if (!this.form.isConnected) { + this.destroy(); + return; + } + // Must check for inputs because a parent may be removed and not show up in record.removedNodes + if ([...this.inputs.all].some(input => !input.isConnected)) { + // ADD COMMENT + this.mutObs.disconnect(); + // If any known input has been removed from the DOM, reanalyze the whole form + window.requestIdleCallback(() => { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); + }); + } + } + }); // This ensures we fire the handler again if the form is changed this.addListener(form, 'input', () => { @@ -5758,6 +5783,12 @@ class Form { * Resets our input scoring and starts from scratch */ recategorizeAllInputs() { + // If the form mutates too much, disconnect to avoid performance issues + if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + this.mutObs.disconnect(); + return; + } + this.rescanCount++; this.initialScanComplete = false; this.removeAllDecorations(); this.forgetAllInputs(); @@ -5776,11 +5807,13 @@ class Form { } // This removes all listeners to avoid memory leaks and weird behaviours destroy() { + this.mutObs.disconnect(); this.removeAllDecorations(); this.removeTooltip(); this.forgetAllInputs(); this.matching.clear(); this.intObs = null; + this.device.scanner.forms.delete(this.form); } categorizeInputs() { const selector = this.matching.cssSelector('formInputsSelector'); @@ -5795,12 +5828,17 @@ class Form { if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, bailing.'); - } + // This is rather extreme, but better safe than sorry + this.device.scanner.stopScanner('The form has too many inputs, bailing.'); + return; } } this.initialScanComplete = true; + + // Observe only if the container isn't the body, to avoid performance overloads + if (this.form !== document.body) { + this.mutObs.observe(this.form, this.mutObsConfig); + } } get submitButtons() { const selector = this.matching.cssSelector('submitButtonSelector'); @@ -5853,10 +5891,14 @@ class Form { // If the form has too many inputs, destroy everything to avoid performance issues if (this.inputs.all.size > MAX_INPUTS_PER_FORM) { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, destroying.'); - } - this.destroy(); + this.device.scanner.stopScanner('The form has too many inputs, destroying.'); + return this; + } + + // When new inputs are added after the initial scan, reanalyze the whole form + if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); return this; } @@ -9142,7 +9184,8 @@ function getInputSubtype(input) { */ const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - if (!string) return ''; + // The length check is extra safety to avoid trimming strings that would be discarded anyway + if (!string || string.length > TEXT_LENGTH_CUTOFF + 50) return ''; return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; @@ -9708,6 +9751,7 @@ const { * findEligibleInputs(context): Scanner; * matching: import("./Form/matching").Matching; * options: ScannerOptions; + * stopScanner: (reason: string, ...rest: any) => void; * }} Scanner * * @typedef {{ @@ -9849,7 +9893,7 @@ class DefaultScanner { /** * Stops scanning, switches off the mutation observer and clears all forms * @param {string} reason - * @param {...any} rest + * @param {any} rest */ stopScanner(reason) { this.stopped = true; @@ -9890,9 +9934,15 @@ class DefaultScanner { } } } + + /** + * Max number of nodes we want to traverse upwards, critical to avoid enclosing large portions of the DOM + * @type {number} + */ + let traversalLayerCount = 0; let element = input; // traverse the DOM to search for related inputs - while (element.parentElement && element.parentElement !== document.documentElement) { + while (traversalLayerCount <= 5 && element.parentElement && element.parentElement !== document.documentElement) { // Avoid overlapping containers or forms const siblingForm = element.parentElement?.querySelector('form'); if (siblingForm && siblingForm !== element) { @@ -9906,6 +9956,7 @@ class DefaultScanner { // found related input, return common ancestor return element; } + traversalLayerCount++; } return input; } diff --git a/integration-test/tests/mutating-form.macos.spec.js b/integration-test/tests/mutating-form.macos.spec.js index 6dc29ad84..3451cdd37 100644 --- a/integration-test/tests/mutating-form.macos.spec.js +++ b/integration-test/tests/mutating-form.macos.spec.js @@ -8,7 +8,7 @@ import {test as base} from '@playwright/test' */ const test = base.extend({}) -test.describe.skip('Mutating form page', () => { +test.describe('Mutating form page', () => { async function applyScript (page) { await createAutofillScript() .replaceAll(macosContentScopeReplacements()) diff --git a/src/Form/Form.js b/src/Form/Form.js index 7b22a877b..f793dfcdd 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -26,7 +26,8 @@ import {constants} from '../constants.js' const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, - MAX_INPUTS_PER_FORM + MAX_INPUTS_PER_FORM, + MAX_FORM_MUT_OBS_COUNT } = constants class Form { @@ -76,6 +77,30 @@ class Form { } }) + this.rescanCount = 0 + this.mutObsConfig = { childList: true, subtree: true } + this.mutObs = new MutationObserver( + (records) => { + const anythingRemoved = records.some(record => record.removedNodes.length > 0) + if (anythingRemoved) { + if (!this.form.isConnected) { + this.destroy() + return + } + // Must check for inputs because a parent may be removed and not show up in record.removedNodes + if ([...this.inputs.all].some(input => !input.isConnected)) { + // ADD COMMENT + this.mutObs.disconnect() + // If any known input has been removed from the DOM, reanalyze the whole form + window.requestIdleCallback(() => { + this.formAnalyzer = new FormAnalyzer(this.form, input, this.matching) + this.recategorizeAllInputs() + }) + } + } + } + ) + // This ensures we fire the handler again if the form is changed this.addListener(form, 'input', () => { if (!this.isAutofilling) { @@ -309,6 +334,12 @@ class Form { * Resets our input scoring and starts from scratch */ recategorizeAllInputs () { + // If the form mutates too much, disconnect to avoid performance issues + if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + this.mutObs.disconnect() + return + } + this.rescanCount++ this.initialScanComplete = false this.removeAllDecorations() this.forgetAllInputs() @@ -327,11 +358,13 @@ class Form { } // This removes all listeners to avoid memory leaks and weird behaviours destroy () { + this.mutObs.disconnect() this.removeAllDecorations() this.removeTooltip() this.forgetAllInputs() this.matching.clear() this.intObs = null + this.device.scanner.forms.delete(this.form) } categorizeInputs () { @@ -347,12 +380,17 @@ class Form { if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)) } else { - if (shouldLog()) { - console.log('The form has too many inputs, bailing.') - } + // This is rather extreme, but better safe than sorry + this.device.scanner.stopScanner('The form has too many inputs, bailing.') + return } } this.initialScanComplete = true + + // Observe only if the container isn't the body, to avoid performance overloads + if (this.form !== document.body) { + this.mutObs.observe(this.form, this.mutObsConfig) + } } get submitButtons () { @@ -410,10 +448,14 @@ class Form { // If the form has too many inputs, destroy everything to avoid performance issues if (this.inputs.all.size > MAX_INPUTS_PER_FORM) { - if (shouldLog()) { - console.log('The form has too many inputs, destroying.') - } - this.destroy() + this.device.scanner.stopScanner('The form has too many inputs, destroying.') + return this + } + + // When new inputs are added after the initial scan, reanalyze the whole form + if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + this.formAnalyzer = new FormAnalyzer(this.form, input, this.matching) + this.recategorizeAllInputs() return this } diff --git a/src/Form/Form.test.js b/src/Form/Form.test.js index fb7e06c86..f2df479d3 100644 --- a/src/Form/Form.test.js +++ b/src/Form/Form.test.js @@ -380,7 +380,7 @@ describe('Form bails', () => { beforeEach(() => { document.body.innerHTML = '' }) - test('when it has too many fields', async () => { + test('when it has too many fields on load', async () => { const formEl = attachAndReturnGenericForm() for (let i = 0; i <= constants.MAX_INPUTS_PER_FORM + 10; i++) { const input = document.createElement('input') diff --git a/src/Form/matching.js b/src/Form/matching.js index 449cd778e..e5fdb6c1f 100644 --- a/src/Form/matching.js +++ b/src/Form/matching.js @@ -759,7 +759,8 @@ function getInputSubtype (input) { * @return {string} */ const removeExcessWhitespace = (string = '') => { - if (!string) return '' + // The length check is extra safety to avoid trimming strings that would be discarded anyway + if (!string || string.length > TEXT_LENGTH_CUTOFF + 50) return '' return (string) .replace(/\n/g, ' ') diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index a94821974..300a7a4c3 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9437,7 +9437,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, - MAX_INPUTS_PER_FORM + MAX_INPUTS_PER_FORM, + MAX_FORM_MUT_OBS_COUNT } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -9485,6 +9486,30 @@ class Form { if (!entry.isIntersecting) this.removeTooltip(); } }); + this.rescanCount = 0; + this.mutObsConfig = { + childList: true, + subtree: true + }; + this.mutObs = new MutationObserver(records => { + const anythingRemoved = records.some(record => record.removedNodes.length > 0); + if (anythingRemoved) { + if (!this.form.isConnected) { + this.destroy(); + return; + } + // Must check for inputs because a parent may be removed and not show up in record.removedNodes + if ([...this.inputs.all].some(input => !input.isConnected)) { + // ADD COMMENT + this.mutObs.disconnect(); + // If any known input has been removed from the DOM, reanalyze the whole form + window.requestIdleCallback(() => { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); + }); + } + } + }); // This ensures we fire the handler again if the form is changed this.addListener(form, 'input', () => { @@ -9703,6 +9728,12 @@ class Form { * Resets our input scoring and starts from scratch */ recategorizeAllInputs() { + // If the form mutates too much, disconnect to avoid performance issues + if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + this.mutObs.disconnect(); + return; + } + this.rescanCount++; this.initialScanComplete = false; this.removeAllDecorations(); this.forgetAllInputs(); @@ -9721,11 +9752,13 @@ class Form { } // This removes all listeners to avoid memory leaks and weird behaviours destroy() { + this.mutObs.disconnect(); this.removeAllDecorations(); this.removeTooltip(); this.forgetAllInputs(); this.matching.clear(); this.intObs = null; + this.device.scanner.forms.delete(this.form); } categorizeInputs() { const selector = this.matching.cssSelector('formInputsSelector'); @@ -9740,12 +9773,17 @@ class Form { if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, bailing.'); - } + // This is rather extreme, but better safe than sorry + this.device.scanner.stopScanner('The form has too many inputs, bailing.'); + return; } } this.initialScanComplete = true; + + // Observe only if the container isn't the body, to avoid performance overloads + if (this.form !== document.body) { + this.mutObs.observe(this.form, this.mutObsConfig); + } } get submitButtons() { const selector = this.matching.cssSelector('submitButtonSelector'); @@ -9798,10 +9836,14 @@ class Form { // If the form has too many inputs, destroy everything to avoid performance issues if (this.inputs.all.size > MAX_INPUTS_PER_FORM) { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, destroying.'); - } - this.destroy(); + this.device.scanner.stopScanner('The form has too many inputs, destroying.'); + return this; + } + + // When new inputs are added after the initial scan, reanalyze the whole form + if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); return this; } @@ -13087,7 +13129,8 @@ function getInputSubtype(input) { */ const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - if (!string) return ''; + // The length check is extra safety to avoid trimming strings that would be discarded anyway + if (!string || string.length > TEXT_LENGTH_CUTOFF + 50) return ''; return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; @@ -13653,6 +13696,7 @@ const { * findEligibleInputs(context): Scanner; * matching: import("./Form/matching").Matching; * options: ScannerOptions; + * stopScanner: (reason: string, ...rest: any) => void; * }} Scanner * * @typedef {{ @@ -13794,7 +13838,7 @@ class DefaultScanner { /** * Stops scanning, switches off the mutation observer and clears all forms * @param {string} reason - * @param {...any} rest + * @param {any} rest */ stopScanner(reason) { this.stopped = true; @@ -13835,9 +13879,15 @@ class DefaultScanner { } } } + + /** + * Max number of nodes we want to traverse upwards, critical to avoid enclosing large portions of the DOM + * @type {number} + */ + let traversalLayerCount = 0; let element = input; // traverse the DOM to search for related inputs - while (element.parentElement && element.parentElement !== document.documentElement) { + while (traversalLayerCount <= 5 && element.parentElement && element.parentElement !== document.documentElement) { // Avoid overlapping containers or forms const siblingForm = element.parentElement?.querySelector('form'); if (siblingForm && siblingForm !== element) { @@ -13851,6 +13901,7 @@ class DefaultScanner { // found related input, return common ancestor return element; } + traversalLayerCount++; } return input; } diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 9bb2db807..d927bd6b7 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5492,7 +5492,8 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, - MAX_INPUTS_PER_FORM + MAX_INPUTS_PER_FORM, + MAX_FORM_MUT_OBS_COUNT } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -5540,6 +5541,30 @@ class Form { if (!entry.isIntersecting) this.removeTooltip(); } }); + this.rescanCount = 0; + this.mutObsConfig = { + childList: true, + subtree: true + }; + this.mutObs = new MutationObserver(records => { + const anythingRemoved = records.some(record => record.removedNodes.length > 0); + if (anythingRemoved) { + if (!this.form.isConnected) { + this.destroy(); + return; + } + // Must check for inputs because a parent may be removed and not show up in record.removedNodes + if ([...this.inputs.all].some(input => !input.isConnected)) { + // ADD COMMENT + this.mutObs.disconnect(); + // If any known input has been removed from the DOM, reanalyze the whole form + window.requestIdleCallback(() => { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); + }); + } + } + }); // This ensures we fire the handler again if the form is changed this.addListener(form, 'input', () => { @@ -5758,6 +5783,12 @@ class Form { * Resets our input scoring and starts from scratch */ recategorizeAllInputs() { + // If the form mutates too much, disconnect to avoid performance issues + if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + this.mutObs.disconnect(); + return; + } + this.rescanCount++; this.initialScanComplete = false; this.removeAllDecorations(); this.forgetAllInputs(); @@ -5776,11 +5807,13 @@ class Form { } // This removes all listeners to avoid memory leaks and weird behaviours destroy() { + this.mutObs.disconnect(); this.removeAllDecorations(); this.removeTooltip(); this.forgetAllInputs(); this.matching.clear(); this.intObs = null; + this.device.scanner.forms.delete(this.form); } categorizeInputs() { const selector = this.matching.cssSelector('formInputsSelector'); @@ -5795,12 +5828,17 @@ class Form { if (foundInputs.length < MAX_INPUTS_PER_FORM) { foundInputs.forEach(input => this.addInput(input)); } else { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, bailing.'); - } + // This is rather extreme, but better safe than sorry + this.device.scanner.stopScanner('The form has too many inputs, bailing.'); + return; } } this.initialScanComplete = true; + + // Observe only if the container isn't the body, to avoid performance overloads + if (this.form !== document.body) { + this.mutObs.observe(this.form, this.mutObsConfig); + } } get submitButtons() { const selector = this.matching.cssSelector('submitButtonSelector'); @@ -5853,10 +5891,14 @@ class Form { // If the form has too many inputs, destroy everything to avoid performance issues if (this.inputs.all.size > MAX_INPUTS_PER_FORM) { - if ((0, _autofillUtils.shouldLog)()) { - console.log('The form has too many inputs, destroying.'); - } - this.destroy(); + this.device.scanner.stopScanner('The form has too many inputs, destroying.'); + return this; + } + + // When new inputs are added after the initial scan, reanalyze the whole form + if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); + this.recategorizeAllInputs(); return this; } @@ -9142,7 +9184,8 @@ function getInputSubtype(input) { */ const removeExcessWhitespace = function () { let string = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; - if (!string) return ''; + // The length check is extra safety to avoid trimming strings that would be discarded anyway + if (!string || string.length > TEXT_LENGTH_CUTOFF + 50) return ''; return string.replace(/\n/g, ' ').replace(/\s{2,}/g, ' ').trim(); }; @@ -9708,6 +9751,7 @@ const { * findEligibleInputs(context): Scanner; * matching: import("./Form/matching").Matching; * options: ScannerOptions; + * stopScanner: (reason: string, ...rest: any) => void; * }} Scanner * * @typedef {{ @@ -9849,7 +9893,7 @@ class DefaultScanner { /** * Stops scanning, switches off the mutation observer and clears all forms * @param {string} reason - * @param {...any} rest + * @param {any} rest */ stopScanner(reason) { this.stopped = true; @@ -9890,9 +9934,15 @@ class DefaultScanner { } } } + + /** + * Max number of nodes we want to traverse upwards, critical to avoid enclosing large portions of the DOM + * @type {number} + */ + let traversalLayerCount = 0; let element = input; // traverse the DOM to search for related inputs - while (element.parentElement && element.parentElement !== document.documentElement) { + while (traversalLayerCount <= 5 && element.parentElement && element.parentElement !== document.documentElement) { // Avoid overlapping containers or forms const siblingForm = element.parentElement?.querySelector('form'); if (siblingForm && siblingForm !== element) { @@ -9906,6 +9956,7 @@ class DefaultScanner { // found related input, return common ancestor return element; } + traversalLayerCount++; } return input; } From ccc5a344c38c919cb8be34f64bfb1475d1eae16d Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Wed, 25 Oct 2023 17:56:28 +0200 Subject: [PATCH 5/9] Rename constant Signed-off-by: Emanuele Feliziani --- integration-test/helpers/harness.js | 2 +- src/Form/Form.js | 6 +++--- src/constants.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-test/helpers/harness.js b/integration-test/helpers/harness.js index d19401866..b88474ba3 100644 --- a/integration-test/helpers/harness.js +++ b/integration-test/helpers/harness.js @@ -41,7 +41,7 @@ export async function withEmailProtectionExtensionSignedInAs (page, username) { * MAX_INPUTS_PER_PAGE: number, * MAX_FORMS_PER_PAGE: number, * MAX_INPUTS_PER_FORM: number, - * MAX_FORM_MUT_OBS_COUNT: number + * MAX_FORM_RESCANS: number * }} [p.constants] * @return {Promise} */ diff --git a/src/Form/Form.js b/src/Form/Form.js index f793dfcdd..15e57567b 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -27,7 +27,7 @@ const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, MAX_INPUTS_PER_FORM, - MAX_FORM_MUT_OBS_COUNT + MAX_FORM_RESCANS } = constants class Form { @@ -335,7 +335,7 @@ class Form { */ recategorizeAllInputs () { // If the form mutates too much, disconnect to avoid performance issues - if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + if (this.rescanCount >= MAX_FORM_RESCANS) { this.mutObs.disconnect() return } @@ -453,7 +453,7 @@ class Form { } // When new inputs are added after the initial scan, reanalyze the whole form - if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + if (this.initialScanComplete && this.rescanCount < MAX_FORM_RESCANS) { this.formAnalyzer = new FormAnalyzer(this.form, input, this.matching) this.recategorizeAllInputs() return this diff --git a/src/constants.js b/src/constants.js index f8ed71eb2..5c5fbcc8b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ export const constants = { MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, - MAX_FORM_MUT_OBS_COUNT: 50 + MAX_FORM_RESCANS: 50 } From f592a853e7a05cca152e37a1c3acbd21ffdce49a Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Wed, 25 Oct 2023 17:56:36 +0200 Subject: [PATCH 6/9] Add comment Signed-off-by: Emanuele Feliziani --- src/Form/Form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/Form.js b/src/Form/Form.js index 15e57567b..c88c3b2e9 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -89,7 +89,7 @@ class Form { } // Must check for inputs because a parent may be removed and not show up in record.removedNodes if ([...this.inputs.all].some(input => !input.isConnected)) { - // ADD COMMENT + // This is re-connected in recategorizeAllInputs, disconnecting here to avoid risk of re-work this.mutObs.disconnect() // If any known input has been removed from the DOM, reanalyze the whole form window.requestIdleCallback(() => { From 0e03a4457f490b4106bf456acbd2f4602bbed338 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Wed, 25 Oct 2023 17:56:45 +0200 Subject: [PATCH 7/9] Add compiled files Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 10 +++++----- dist/autofill.js | 10 +++++----- swift-package/Resources/assets/autofill-debug.js | 10 +++++----- swift-package/Resources/assets/autofill.js | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 300a7a4c3..03d6c7148 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9438,7 +9438,7 @@ const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, MAX_INPUTS_PER_FORM, - MAX_FORM_MUT_OBS_COUNT + MAX_FORM_RESCANS } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -9500,7 +9500,7 @@ class Form { } // Must check for inputs because a parent may be removed and not show up in record.removedNodes if ([...this.inputs.all].some(input => !input.isConnected)) { - // ADD COMMENT + // This is re-connected in recategorizeAllInputs, disconnecting here to avoid risk of re-work this.mutObs.disconnect(); // If any known input has been removed from the DOM, reanalyze the whole form window.requestIdleCallback(() => { @@ -9729,7 +9729,7 @@ class Form { */ recategorizeAllInputs() { // If the form mutates too much, disconnect to avoid performance issues - if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + if (this.rescanCount >= MAX_FORM_RESCANS) { this.mutObs.disconnect(); return; } @@ -9841,7 +9841,7 @@ class Form { } // When new inputs are added after the initial scan, reanalyze the whole form - if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + if (this.initialScanComplete && this.rescanCount < MAX_FORM_RESCANS) { this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); this.recategorizeAllInputs(); return this; @@ -16611,7 +16611,7 @@ const constants = exports.constants = { MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, - MAX_FORM_MUT_OBS_COUNT: 50 + MAX_FORM_RESCANS: 50 }; },{}],65:[function(require,module,exports){ diff --git a/dist/autofill.js b/dist/autofill.js index d927bd6b7..ec5240b4b 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5493,7 +5493,7 @@ const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, MAX_INPUTS_PER_FORM, - MAX_FORM_MUT_OBS_COUNT + MAX_FORM_RESCANS } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -5555,7 +5555,7 @@ class Form { } // Must check for inputs because a parent may be removed and not show up in record.removedNodes if ([...this.inputs.all].some(input => !input.isConnected)) { - // ADD COMMENT + // This is re-connected in recategorizeAllInputs, disconnecting here to avoid risk of re-work this.mutObs.disconnect(); // If any known input has been removed from the DOM, reanalyze the whole form window.requestIdleCallback(() => { @@ -5784,7 +5784,7 @@ class Form { */ recategorizeAllInputs() { // If the form mutates too much, disconnect to avoid performance issues - if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + if (this.rescanCount >= MAX_FORM_RESCANS) { this.mutObs.disconnect(); return; } @@ -5896,7 +5896,7 @@ class Form { } // When new inputs are added after the initial scan, reanalyze the whole form - if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + if (this.initialScanComplete && this.rescanCount < MAX_FORM_RESCANS) { this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); this.recategorizeAllInputs(); return this; @@ -12666,7 +12666,7 @@ const constants = exports.constants = { MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, - MAX_FORM_MUT_OBS_COUNT: 50 + MAX_FORM_RESCANS: 50 }; },{}],55:[function(require,module,exports){ diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 300a7a4c3..03d6c7148 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9438,7 +9438,7 @@ const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, MAX_INPUTS_PER_FORM, - MAX_FORM_MUT_OBS_COUNT + MAX_FORM_RESCANS } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -9500,7 +9500,7 @@ class Form { } // Must check for inputs because a parent may be removed and not show up in record.removedNodes if ([...this.inputs.all].some(input => !input.isConnected)) { - // ADD COMMENT + // This is re-connected in recategorizeAllInputs, disconnecting here to avoid risk of re-work this.mutObs.disconnect(); // If any known input has been removed from the DOM, reanalyze the whole form window.requestIdleCallback(() => { @@ -9729,7 +9729,7 @@ class Form { */ recategorizeAllInputs() { // If the form mutates too much, disconnect to avoid performance issues - if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + if (this.rescanCount >= MAX_FORM_RESCANS) { this.mutObs.disconnect(); return; } @@ -9841,7 +9841,7 @@ class Form { } // When new inputs are added after the initial scan, reanalyze the whole form - if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + if (this.initialScanComplete && this.rescanCount < MAX_FORM_RESCANS) { this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); this.recategorizeAllInputs(); return this; @@ -16611,7 +16611,7 @@ const constants = exports.constants = { MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, - MAX_FORM_MUT_OBS_COUNT: 50 + MAX_FORM_RESCANS: 50 }; },{}],65:[function(require,module,exports){ diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index d927bd6b7..ec5240b4b 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5493,7 +5493,7 @@ const { ATTR_AUTOFILL, ATTR_INPUT_TYPE, MAX_INPUTS_PER_FORM, - MAX_FORM_MUT_OBS_COUNT + MAX_FORM_RESCANS } = _constants.constants; class Form { /** @type {import("../Form/matching").Matching} */ @@ -5555,7 +5555,7 @@ class Form { } // Must check for inputs because a parent may be removed and not show up in record.removedNodes if ([...this.inputs.all].some(input => !input.isConnected)) { - // ADD COMMENT + // This is re-connected in recategorizeAllInputs, disconnecting here to avoid risk of re-work this.mutObs.disconnect(); // If any known input has been removed from the DOM, reanalyze the whole form window.requestIdleCallback(() => { @@ -5784,7 +5784,7 @@ class Form { */ recategorizeAllInputs() { // If the form mutates too much, disconnect to avoid performance issues - if (this.rescanCount >= MAX_FORM_MUT_OBS_COUNT) { + if (this.rescanCount >= MAX_FORM_RESCANS) { this.mutObs.disconnect(); return; } @@ -5896,7 +5896,7 @@ class Form { } // When new inputs are added after the initial scan, reanalyze the whole form - if (this.initialScanComplete && this.rescanCount < MAX_FORM_MUT_OBS_COUNT) { + if (this.initialScanComplete && this.rescanCount < MAX_FORM_RESCANS) { this.formAnalyzer = new _FormAnalyzer.default(this.form, input, this.matching); this.recategorizeAllInputs(); return this; @@ -12666,7 +12666,7 @@ const constants = exports.constants = { MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, - MAX_FORM_MUT_OBS_COUNT: 50 + MAX_FORM_RESCANS: 50 }; },{}],55:[function(require,module,exports){ From 7b89b5e814880c6b0589db0e512f25927f816fa8 Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Wed, 25 Oct 2023 18:05:08 +0200 Subject: [PATCH 8/9] Fix name in test harness Signed-off-by: Emanuele Feliziani --- integration-test/helpers/harness.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/helpers/harness.js b/integration-test/helpers/harness.js index b88474ba3..05e71d9a1 100644 --- a/integration-test/helpers/harness.js +++ b/integration-test/helpers/harness.js @@ -115,7 +115,7 @@ export function createAutofillScript () { MAX_INPUTS_PER_PAGE: 100, MAX_FORMS_PER_PAGE: 30, MAX_INPUTS_PER_FORM: 80, - MAX_FORM_MUT_OBS_COUNT: 50 + MAX_FORM_RESCANS: 50 } /** @type {ScriptBuilder} */ From f7cf4d47f288863b05b6887e901b27a965e2be7b Mon Sep 17 00:00:00 2001 From: Emanuele Feliziani Date: Wed, 25 Oct 2023 18:21:55 +0200 Subject: [PATCH 9/9] Add comment Signed-off-by: Emanuele Feliziani --- dist/autofill-debug.js | 1 + dist/autofill.js | 1 + src/Form/Form.js | 1 + swift-package/Resources/assets/autofill-debug.js | 1 + swift-package/Resources/assets/autofill.js | 1 + 5 files changed, 5 insertions(+) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 03d6c7148..cd79bd834 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9494,6 +9494,7 @@ class Form { this.mutObs = new MutationObserver(records => { const anythingRemoved = records.some(record => record.removedNodes.length > 0); if (anythingRemoved) { + // Ensure we destroy the form if it's removed from the DOM if (!this.form.isConnected) { this.destroy(); return; diff --git a/dist/autofill.js b/dist/autofill.js index ec5240b4b..a517dd179 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5549,6 +5549,7 @@ class Form { this.mutObs = new MutationObserver(records => { const anythingRemoved = records.some(record => record.removedNodes.length > 0); if (anythingRemoved) { + // Ensure we destroy the form if it's removed from the DOM if (!this.form.isConnected) { this.destroy(); return; diff --git a/src/Form/Form.js b/src/Form/Form.js index c88c3b2e9..9b771710b 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -83,6 +83,7 @@ class Form { (records) => { const anythingRemoved = records.some(record => record.removedNodes.length > 0) if (anythingRemoved) { + // Ensure we destroy the form if it's removed from the DOM if (!this.form.isConnected) { this.destroy() return diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 03d6c7148..cd79bd834 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9494,6 +9494,7 @@ class Form { this.mutObs = new MutationObserver(records => { const anythingRemoved = records.some(record => record.removedNodes.length > 0); if (anythingRemoved) { + // Ensure we destroy the form if it's removed from the DOM if (!this.form.isConnected) { this.destroy(); return; diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index ec5240b4b..a517dd179 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5549,6 +5549,7 @@ class Form { this.mutObs = new MutationObserver(records => { const anythingRemoved = records.some(record => record.removedNodes.length > 0); if (anythingRemoved) { + // Ensure we destroy the form if it's removed from the DOM if (!this.form.isConnected) { this.destroy(); return;