From 47b2ed5059857c5cf55fd3717e5f17d3366af3a6 Mon Sep 17 00:00:00 2001 From: svedge Date: Thu, 10 Sep 2026 11:03:15 +0500 Subject: [PATCH 1/6] New. ContactEncoder. Add option to exclude selected contact data. --- cleantalk.php | 5 + css/cleantalk-admin-settings-page.min.css | 2 +- css/src/cleantalk-admin-settings-page.css | 4 + inc/cleantalk-settings.php | 23 +++- .../ContactsEncoder/ContactsEncoder.php | 30 +++++ .../PluginSettingsPage/SettingsField.php | 6 +- lib/Cleantalk/ApbctWP/State.php | 1 + .../ContactsEncoder/ContactsEncoder.php | 8 ++ .../Common/ContactsEncoder/Dto/Params.php | 7 ++ .../Exclusions/ExclusionsService.php | 114 ++++++++++++++++++ .../Exclusions/TestExclusionsService.php | 27 +++++ .../ContactsEncoder/TestContactsEncoder.php | 101 ++++++++++++++++ 12 files changed, 325 insertions(+), 3 deletions(-) diff --git a/cleantalk.php b/cleantalk.php index 30dda1f11..1cad49eb8 100644 --- a/cleantalk.php +++ b/cleantalk.php @@ -3171,6 +3171,11 @@ function apbctGetContactsEncoder() $contacts_encoder_params->obfuscation_text = $apbct->settings['data__email_decoder_obfuscation_custom_text']; $contacts_encoder_params->do_encode_emails = (int)$apbct->settings['data__email_decoder_encode_email_addresses']; $contacts_encoder_params->do_encode_phones = (int)$apbct->settings['data__email_decoder_encode_phone_numbers']; + $contacts_encoder_params->excluded_strings = \Cleantalk\Common\ContactsEncoder\Exclusions\ExclusionsService::parseExcludedStrings( + isset($apbct->settings['data__email_decoder_excluded_strings']) + ? $apbct->settings['data__email_decoder_excluded_strings'] + : '' + ); return ContactsEncoder::getInstance($contacts_encoder_params); } diff --git a/css/cleantalk-admin-settings-page.min.css b/css/cleantalk-admin-settings-page.min.css index eaa208fd8..dbba03e03 100644 --- a/css/cleantalk-admin-settings-page.min.css +++ b/css/cleantalk-admin-settings-page.min.css @@ -1 +1 @@ -.apbct_settings-page{padding-right:10px}.apbct_settings-field_content,.cleantalk_link-auto,.cleantalk_link-manual,.ct-warning-test-failed,.ct_rate_block,.ct_settings_banner,i.animate-spin{display:inline-block}#apbctTopWarning{margin-bottom:5px}#apbctTopWarning h3{margin:10px 0 5px 0}#apbctTopWarning h4{margin:10px}#apbctTopWarning h4 span{margin-top:5px}.apbct_settings-subtitle{position:relative;top:-15px;margin:0}.apbct_settings-field_wrapper{margin:15px 0}.apbct_settings-field_wrapper--sub{margin-left:30px!important}.apbct_settings__label{margin-right:10px;font-size:17px;vertical-align:text-bottom}.apbct_settings-field_content{display:inline-block}.apbct_settings-field_content--radio,.apbct_settings-field_wrapper>.apbct_settings-field_description{width:70%;padding-bottom:10px}.apbct_settings-field_title--select,.apbct_settings-field_title--textarea{margin:0;width:210px;padding-right:10px;padding-bottom:10px;font-size:14px;vertical-align:top}.apbct_settings-field_title--radio{display:inline-block;margin:0;width:210px;padding-right:10px;font-size:14px;vertical-align:top}.apbct_input_text{min-width:255px;width:400px}.apbct_settings-long_description---show:hover{color:#aaa;cursor:pointer}.apbct_setting_textarea{min-width:300px}.cleantalk_link{text-decoration:none;font-size:13px;line-height:26px;margin:10px 0 0 0;padding:0 10px 1px;cursor:pointer;border-width:1px;border-style:solid;-webkit-appearance:none;white-space:nowrap;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.cleantalk_link-auto{background:#ccc;border-color:#999;-webkit-box-shadow:inset 0 1px 0 rgba(200,200,200,.5),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(200,200,200,.5),0 1px 0 rgba(0,0,0,.15);color:#000;display:inline-block;height:28px;-webkit-border-radius:2px;border-radius:2px}.cleantalk_link-auto:hover{color:#fff}.cleantalk_link-auto.cleantalk_link_text_center{text-align:center}.cleantalk_link-manual{background:#2ea2cc;border-color:#0074a2;-webkit-box-shadow:inset 0 1px 0 rgba(120,200,230,.5),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(120,200,230,.5),0 1px 0 rgba(0,0,0,.15);color:#fff;display:inline-block;-webkit-border-radius:3px;border-radius:3px;text-align:center}.cleantalk_link-manual:hover{color:#000}.cleantalk_link[disabled=disabled]{background:#5d9db5;color:#000}.apbct_status_icon{vertical-align:text-bottom;margin:0 5px 0 8px}.apbct-icon-upload-cloud{margin-left:3px}a.ct_support_link{color:#666;margin-right:.5em;font-size:10pt;font-weight:400}.ct-warning-test-failed{display:inline-block;position:relative;padding:5px;margin:4px;border:3px solid #f03232;border-radius:5px;background-color:#ffc8c8}.ct_settings_banner{text-align:right;display:inline-block;width:100%;margin:1em 0;vertical-align:top}#ct_translate_plugin{margin-left:0}.ct_rate_block{display:inline-block;width:370px;margin-right:3em;padding:.8em .8em 15px .8em;text-align:center;border:1px dashed #666}#ct_translate_plugin .apbct_button_rate{margin-bottom:10px}.apbct_long_desc{position:absolute;background:#5a5a5a;min-width:80px;min-height:80px;max-width:500px;padding:10px;color:#fff;z-index:10}.apbct_long_desc a{color:#78c8e6}i.animate-spin{-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear;display:inline-block;margin:0 auto;font-size:25px;line-height:20px;color:#78c8e6;margin:25px}@keyframes spin{to{transform:rotate(359deg)}}.apbct_long_desc__cancel{position:absolute;top:5px;right:5px;color:rgba(255,255,255,.5)}.apbct_long_desc__cancel:hover{color:#fff}.apbct_long_desc__angle{position:absolute;top:5px;left:-17px;width:10px;height:10px;background:#5a5a5a;-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg);-webkit-transform-origin:100% 100%;-ms-transform-origin:100% 100%;transform-origin:100% 100%}.apbct_long_desc__title{color:#78c8e6;margin:0}.--hide{display:none}.apbct_preloader_button{height:15px;margin-left:5px;vertical-align:text-top;display:none}.--invisible{visibility:hidden}.apbct_preloader{height:1em;width:1em;margin-left:.5em;display:inline-block}.key_changed_sync{font-size:1.5em;line-height:2em;vertical-align:middle}.key_changed_success{font-size:1.5em;line-height:2em;vertical-align:middle}.--upper-case{text-transform:uppercase}#cleantalk-modal-content{text-align:center}#cleantalk-modal-content>*{display:block;width:100%}.cleantalk-confirm-modal_header{font-size:15px;font-weight:500}.cleantalk-confirm-modal_text-block{padding:10px 0}.cleantalk-confirm-modal_buttons-block{display:flex!important;gap:10px;justify-content:center}button.ct_support_link{border:none;background:0 0;color:#666;text-decoration:underline;cursor:pointer}.apbct-btn-as-link{padding:0;margin:0;vertical-align:baseline;color:gray;border:0 none;border-bottom:1px solid;cursor:pointer}#apbct-account-email[contenteditable=true]{padding:6px;background-color:#fff;border:1px solid #ccc}button[value=save_changes]:disabled{color:#2271b1;border-color:#2271b1;background:#f6f7f7}#apbct_settings__advanced_settings{position:relative}#apbct_settings__advanced_settings_inner{width:70%}#apbct_hidden_section_nav{position:absolute;top:0;right:0;width:200px}#apbct_hidden_section_nav ul{z-index:9999;width:100%;top:40px}.apbct_settings__position_main_save_button{text-align:right;margin-right:40px}#apbct_settings__after_advanced_settings{margin-bottom:20px;width:70%}#apbct_settings__button_section{position:static;z-index:9999;padding-bottom:40px;bottom:0}.apbct_highlighted{outline-offset:5px;outline-color:red;outline-width:3px!important;outline-style:solid!important}.apbct_notice_inner{display:flex;margin-top:10px!important}.apbct_notification__advanced_settings{margin:20px 0;font-weight:400}.apbct_settings_top_info__div{float:right;padding:0 20px 0 0;font-size:13px;position:relative}.apbct_settings_top_info__p{padding:0;margin:2px}.apbct_settings_top_info__btn{display:inline-flex;gap:10px}.apbct_hidden_section_nav_mob_btn{display:none;background-image:url(images/menu.svg);background-repeat:no-repeat;background-size:contain;background-position-y:center}.apbct_hidden_section_nav_mob_btn-close{display:none}.apbct_settings__footer{background:#c2c2c2;position:absolute;left:0;padding-bottom:30px;padding-left:21px;z-index:-1}.apbct_settings_top_info__sub_btn{display:flex;flex-wrap:wrap;gap:10px;margin:15px 0}.apbct_settings__key_line{display:flex;align-items:center;justify-content:flex-start;flex-direction:row}.apbct_settings__key_line__elements{margin-right:10px}.apbct_setting__key_line__buttons{margin:0}.apbct_setting__key_line__get_key_manual_chunk{margin:10px 0 10px 0;border:1px solid #d3d3d3;border-radius:3px;padding:10px;background:#fff}.apbct_settings__key_line__service_block{margin-left:10px}#apbct_settings__public_offer{margin-top:10px;display:flex;align-items:center}#cleantalk_apikey_wrapper{max-width:65%}.apcbt_contact_data_encoder__line{max-width:70%;display:flex;flex-wrap:wrap;flex-direction:row}.apbct_warning_red_point{display:inline-block;width:4px;height:4px;background:red;border-radius:100%;margin:0 10px 3px 3px;vertical-align:middle;position:relative}.apbct_warning_red_point::after{content:'';position:absolute;left:50%;top:50%;width:12px;height:12px;border:1.5px solid rgba(255,2,2,.65);border-radius:50%;animation:apbct_pulsate 1.2s ease-out infinite;opacity:.7;pointer-events:none;transform:translate(-50%,-50%)}#apbct_summary_and_support{display:flex;flex-wrap:wrap;width:-webkit-fill-available;border:1px solid #ddd;border-radius:3px;padding:10px;background:#ededed;margin-top:4em}#apbct_summary_and_support-left_side{width:69%;padding:0 20px 0 0}#apbct_summary_and_support-right_side{width:31%;padding:0 20px;border-left:1px solid #dcdcde}#apbct_summary_and_support-sides_wrap{display:flex;flex-direction:row;margin:10px 0 10px;width:100%}.apbct_summary_and_support-side_header{margin:.5em 0}.apbct_summary_and_support-inner_header{margin:2em 0 .5em;font-size:medium}.apbct_summary_and_support-inner_header::after{content:'';display:block;width:100%;height:1px;background:linear-gradient(90deg,#d3d3d357 0,rgba(211,211,211,.14) 10%,transparent 100%);margin-top:5px}.apbct_summary_and_support-support_buttons_wrapper{display:flex;flex-direction:column}#apbct_summary_and_support-create_user_button{display:inline-flex;flex-direction:row;align-items:center;justify-content:center}.apbct_summary_and_support-user_creation_result{background:#eee;margin-top:10px;padding:5px;border:1px solid #d3d3d3;display:none}.apbct_summary-list_of_items{display:flex;flex-direction:column}.apbct_summary_list_item{margin-bottom:2px}.apbct-green{color:green}.apbct-red{color:red}#apbct_negative_reports_table{width:100%;border-collapse:collapse;margin:10px 5px}#apbct_sending_report_div{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;flex-wrap:nowrap}#apbct_sending_report_disabled_notice{margin-right:5px}.apbct-real-user-popup-img{align-self:start;margin:0!important;height:1.1em;vertical-align:initial}@keyframes apbct_pulsate{0%{transform:translate(-50%,-50%) scale(1);opacity:.7}100%{transform:translate(-50%,-50%) scale(1.5);opacity:0}}@media (max-width:768px){.apbct_settings_top_info__btn{display:inline-grid;width:100%;gap:5px}#apbct_settings__advanced_settings_inner,#apbct_settings__after_advanced_settings{width:100%}.apbct_settings-field_content--radio,.apbct_settings-field_wrapper>.apbct_settings-field_description{width:100%}.apbct_settings_top_info__sub_btn .apbct_bottom_links--left,.apbct_settings_top_info__sub_btn .apbct_bottom_links--other{margin-left:0;margin-right:0}#apbct_hidden_section_nav ul{display:none;background-color:#dadee3bd;margin:0;padding:10px;border-radius:3px;right:0;width:auto}#apbct_hidden_section_nav{right:0;min-width:167px}.apbct_setting_radio::before{height:6px!important;width:6px!important;margin:4px!important}.apbct_setting_checkbox::before{height:15px!important;width:15px!important;margin:0 -.05rem!important}.apbct_setting_checkbox,.apbct_setting_radio{height:16px!important;width:16px!important}.apbct_hidden_section_nav_mob_btn{display:block;width:32px;height:28px;right:20px;top:20px;position:fixed}.apbct_hidden_section_nav_mob_btn-close{display:block;position:relative;float:right;width:20px;height:22px;top:-5px;right:-5px;background-image:url(images/cancel.svg);background-repeat:no-repeat;background-size:contain;background-position-y:center;text-align:center}.apbct_long_desc{left:-2%!important;max-width:95%;margin-top:10%}.apbct_long_desc__angle{display:none}.ct_rate_block{max-width:240px;margin-right:20px}.apbct_settings__key_line{display:flex;align-items:flex-start;flex-direction:column;width:100%}.apbct_settings__key_line__elements{margin:5px 0 5px 0}#apbct_setting_apikey,.apbct_setting__key_line__buttons,.apbct_settings__key_line__elements{width:100%}#cleantalk_apikey_wrapper{max-width:100%}#apbct_settings__button_section{position:fixed;right:20px}.apcbt_contact_data_encoder__line{max-width:100%;margin-top:5px}#apbct_summary_and_support-right_side{border-top:1px solid #dcdcde;border-left:0;margin:35px 0 0;padding:0;width:-webkit-fill-available}#apbct_summary_and_support-left_side{border-top:1px solid #dcdcde;width:-webkit-fill-available}#apbct_summary_and_support-sides_wrap{flex-direction:column}}.apbct-trial-renew-fullpage{display:flex;position:fixed;width:stretch;top:0;bottom:0;overflow-y:auto;justify-content:center;align-items:flex-start;font-family:Inter,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;color:#444;background-color:#f0f0f0;z-index:99}.apbct-trial-renew-fullpage-notice{padding:32px 64px 64px 64px!important;margin-top:5em!important;margin-bottom:5em!important}.apbct-trial-renew-fullpage-body{margin-top:32px;margin-left:32px}.apbct-banner-header{display:flex;justify-content:space-between}.apbct-trial-renew-fullpage-body .apbct-banner-button-wrapper{display:flex;margin:0}.apbct-trial-renew-fullpage-image{flex:1 1 0;min-width:0;max-width:576px;height:auto;object-fit:contain;margin:auto 0;align-self:flex-start}.apbct-banner-desc-block-row{line-height:22px}.apbct-banner-red-point{display:inline-block;width:8px;height:8px;background-color:#bb3323;border-radius:50%;margin-right:8px;vertical-align:middle}.apbct-banner-error{background-color:#fff!important;box-shadow:0 4px 15px #84848440!important;border-left-color:#bb3323!important}.apbct-banner-background-container{display:grid;max-width:644px;gap:16px;padding:32px;background-color:#f3f6f9;border-radius:8px}.apbct-banner-desc-block-title{font-size:18px;font-weight:700;line-height:22px;color:#0a0a0a;margin-bottom:8px}.apbct-banner-stat-value{margin-bottom:0;color:#444;vertical-align:middle}.apbct-banner-desc-block-text{font-size:14px;line-height:22px;color:#717182;margin-left:12px;margin-top:0;vertical-align:middle}.apbct-banner-desc-blocks{display:grid;gap:16px;flex:1 1 0;min-width:0}.apbct-banner-desc-blocks-content{display:flex;gap:72px;flex-wrap:wrap;justify-content:center;margin:32px 0}.apbct-banner-check{vertical-align:middle}.apbct-trial-renew-upgrade{display:none;text-align:center}.apbct-trial-renew-upgrade-link{font-size:16px;line-height:22px;letter-spacing:0;text-decoration:underline solid 0 #026e88!important;text-underline-offset:25%;color:#026e88!important}@media (max-width:1366px){.apbct-banner-header{display:grid;gap:16px}.apbct-banner-desc-blocks-content{display:grid;gap:38px;place-items:center}.apbct-trial-renew-upgrade{display:block}}@media (max-width:1200px){.apbct-banner-desc-blocks-content{gap:32px}}@media (max-width:782px){.apbct-trial-renew-fullpage{padding-top:50px;padding-bottom:50px}.apbct-trial-renew-fullpage-notice{padding:24px 24px 32px 24px!important}.apbct-trial-renew-fullpage-body{margin-top:20px;margin-left:0}.apbct-banner-desc-blocks-content{gap:24px}.apbct-banner-background-container{padding:20px}.apbct-trial-renew-fullpage-image{width:100%}}@media (max-width:480px){.apbct-trial-renew-fullpage-notice{padding:16px 12px 24px 12px!important}.apbct-banner-background-container{padding:16px}.apbct-banner-desc-blocks-content{gap:16px}.apbct-banner-desc-block-title{font-size:16px}} \ No newline at end of file +.apbct_settings-page{padding-right:10px}.apbct_settings-field_content,.cleantalk_link-auto,.cleantalk_link-manual,.ct-warning-test-failed,.ct_rate_block,.ct_settings_banner,i.animate-spin{display:inline-block}#apbctTopWarning{margin-bottom:5px}#apbctTopWarning h3{margin:10px 0 5px 0}#apbctTopWarning h4{margin:10px}#apbctTopWarning h4 span{margin-top:5px}.apbct_settings-subtitle{position:relative;top:-15px;margin:0}.apbct_settings-field_wrapper{margin:15px 0}.apbct_settings-field_wrapper--sub{margin-left:30px!important}.apbct_settings__label{margin-right:10px;font-size:17px;vertical-align:text-bottom}.apbct_settings-field_content{display:inline-block}.apbct_settings-field_content--radio,.apbct_settings-field_wrapper>.apbct_settings-field_description{width:70%;padding-bottom:10px}.apbct_settings-field_title--select,.apbct_settings-field_title--textarea{margin:0;width:210px;padding-right:10px;padding-bottom:10px;font-size:14px;vertical-align:top}.apbct_settings-field_title--with-help{width:auto;white-space:nowrap}.apbct_settings-field_title--radio{display:inline-block;margin:0;width:210px;padding-right:10px;font-size:14px;vertical-align:top}.apbct_input_text{min-width:255px;width:400px}.apbct_settings-long_description---show:hover{color:#aaa;cursor:pointer}.apbct_setting_textarea{min-width:300px}.cleantalk_link{text-decoration:none;font-size:13px;line-height:26px;margin:10px 0 0 0;padding:0 10px 1px;cursor:pointer;border-width:1px;border-style:solid;-webkit-appearance:none;white-space:nowrap;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.cleantalk_link-auto{background:#ccc;border-color:#999;-webkit-box-shadow:inset 0 1px 0 rgba(200,200,200,.5),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(200,200,200,.5),0 1px 0 rgba(0,0,0,.15);color:#000;display:inline-block;height:28px;-webkit-border-radius:2px;border-radius:2px}.cleantalk_link-auto:hover{color:#fff}.cleantalk_link-auto.cleantalk_link_text_center{text-align:center}.cleantalk_link-manual{background:#2ea2cc;border-color:#0074a2;-webkit-box-shadow:inset 0 1px 0 rgba(120,200,230,.5),0 1px 0 rgba(0,0,0,.15);box-shadow:inset 0 1px 0 rgba(120,200,230,.5),0 1px 0 rgba(0,0,0,.15);color:#fff;display:inline-block;-webkit-border-radius:3px;border-radius:3px;text-align:center}.cleantalk_link-manual:hover{color:#000}.cleantalk_link[disabled=disabled]{background:#5d9db5;color:#000}.apbct_status_icon{vertical-align:text-bottom;margin:0 5px 0 8px}.apbct-icon-upload-cloud{margin-left:3px}a.ct_support_link{color:#666;margin-right:.5em;font-size:10pt;font-weight:400}.ct-warning-test-failed{display:inline-block;position:relative;padding:5px;margin:4px;border:3px solid #f03232;border-radius:5px;background-color:#ffc8c8}.ct_settings_banner{text-align:right;display:inline-block;width:100%;margin:1em 0;vertical-align:top}#ct_translate_plugin{margin-left:0}.ct_rate_block{display:inline-block;width:370px;margin-right:3em;padding:.8em .8em 15px .8em;text-align:center;border:1px dashed #666}#ct_translate_plugin .apbct_button_rate{margin-bottom:10px}.apbct_long_desc{position:absolute;background:#5a5a5a;min-width:80px;min-height:80px;max-width:500px;padding:10px;color:#fff;z-index:10}.apbct_long_desc a{color:#78c8e6}i.animate-spin{-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear;display:inline-block;margin:0 auto;font-size:25px;line-height:20px;color:#78c8e6;margin:25px}@keyframes spin{to{transform:rotate(359deg)}}.apbct_long_desc__cancel{position:absolute;top:5px;right:5px;color:rgba(255,255,255,.5)}.apbct_long_desc__cancel:hover{color:#fff}.apbct_long_desc__angle{position:absolute;top:5px;left:-17px;width:10px;height:10px;background:#5a5a5a;-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg);-webkit-transform-origin:100% 100%;-ms-transform-origin:100% 100%;transform-origin:100% 100%}.apbct_long_desc__title{color:#78c8e6;margin:0}.--hide{display:none}.apbct_preloader_button{height:15px;margin-left:5px;vertical-align:text-top;display:none}.--invisible{visibility:hidden}.apbct_preloader{height:1em;width:1em;margin-left:.5em;display:inline-block}.key_changed_sync{font-size:1.5em;line-height:2em;vertical-align:middle}.key_changed_success{font-size:1.5em;line-height:2em;vertical-align:middle}.--upper-case{text-transform:uppercase}#cleantalk-modal-content{text-align:center}#cleantalk-modal-content>*{display:block;width:100%}.cleantalk-confirm-modal_header{font-size:15px;font-weight:500}.cleantalk-confirm-modal_text-block{padding:10px 0}.cleantalk-confirm-modal_buttons-block{display:flex!important;gap:10px;justify-content:center}button.ct_support_link{border:none;background:0 0;color:#666;text-decoration:underline;cursor:pointer}.apbct-btn-as-link{padding:0;margin:0;vertical-align:baseline;color:gray;border:0 none;border-bottom:1px solid;cursor:pointer}#apbct-account-email[contenteditable=true]{padding:6px;background-color:#fff;border:1px solid #ccc}button[value=save_changes]:disabled{color:#2271b1;border-color:#2271b1;background:#f6f7f7}#apbct_settings__advanced_settings{position:relative}#apbct_settings__advanced_settings_inner{width:70%}#apbct_hidden_section_nav{position:absolute;top:0;right:0;width:200px}#apbct_hidden_section_nav ul{z-index:9999;width:100%;top:40px}.apbct_settings__position_main_save_button{text-align:right;margin-right:40px}#apbct_settings__after_advanced_settings{margin-bottom:20px;width:70%}#apbct_settings__button_section{position:static;z-index:9999;padding-bottom:40px;bottom:0}.apbct_highlighted{outline-offset:5px;outline-color:red;outline-width:3px!important;outline-style:solid!important}.apbct_notice_inner{display:flex;margin-top:10px!important}.apbct_notification__advanced_settings{margin:20px 0;font-weight:400}.apbct_settings_top_info__div{float:right;padding:0 20px 0 0;font-size:13px;position:relative}.apbct_settings_top_info__p{padding:0;margin:2px}.apbct_settings_top_info__btn{display:inline-flex;gap:10px}.apbct_hidden_section_nav_mob_btn{display:none;background-image:url(images/menu.svg);background-repeat:no-repeat;background-size:contain;background-position-y:center}.apbct_hidden_section_nav_mob_btn-close{display:none}.apbct_settings__footer{background:#c2c2c2;position:absolute;left:0;padding-bottom:30px;padding-left:21px;z-index:-1}.apbct_settings_top_info__sub_btn{display:flex;flex-wrap:wrap;gap:10px;margin:15px 0}.apbct_settings__key_line{display:flex;align-items:center;justify-content:flex-start;flex-direction:row}.apbct_settings__key_line__elements{margin-right:10px}.apbct_setting__key_line__buttons{margin:0}.apbct_setting__key_line__get_key_manual_chunk{margin:10px 0 10px 0;border:1px solid #d3d3d3;border-radius:3px;padding:10px;background:#fff}.apbct_settings__key_line__service_block{margin-left:10px}#apbct_settings__public_offer{margin-top:10px;display:flex;align-items:center}#cleantalk_apikey_wrapper{max-width:65%}.apcbt_contact_data_encoder__line{max-width:70%;display:flex;flex-wrap:wrap;flex-direction:row}.apbct_warning_red_point{display:inline-block;width:4px;height:4px;background:red;border-radius:100%;margin:0 10px 3px 3px;vertical-align:middle;position:relative}.apbct_warning_red_point::after{content:'';position:absolute;left:50%;top:50%;width:12px;height:12px;border:1.5px solid rgba(255,2,2,.65);border-radius:50%;animation:apbct_pulsate 1.2s ease-out infinite;opacity:.7;pointer-events:none;transform:translate(-50%,-50%)}#apbct_summary_and_support{display:flex;flex-wrap:wrap;width:-webkit-fill-available;border:1px solid #ddd;border-radius:3px;padding:10px;background:#ededed;margin-top:4em}#apbct_summary_and_support-left_side{width:69%;padding:0 20px 0 0}#apbct_summary_and_support-right_side{width:31%;padding:0 20px;border-left:1px solid #dcdcde}#apbct_summary_and_support-sides_wrap{display:flex;flex-direction:row;margin:10px 0 10px;width:100%}.apbct_summary_and_support-side_header{margin:.5em 0}.apbct_summary_and_support-inner_header{margin:2em 0 .5em;font-size:medium}.apbct_summary_and_support-inner_header::after{content:'';display:block;width:100%;height:1px;background:linear-gradient(90deg,#d3d3d357 0,rgba(211,211,211,.14) 10%,transparent 100%);margin-top:5px}.apbct_summary_and_support-support_buttons_wrapper{display:flex;flex-direction:column}#apbct_summary_and_support-create_user_button{display:inline-flex;flex-direction:row;align-items:center;justify-content:center}.apbct_summary_and_support-user_creation_result{background:#eee;margin-top:10px;padding:5px;border:1px solid #d3d3d3;display:none}.apbct_summary-list_of_items{display:flex;flex-direction:column}.apbct_summary_list_item{margin-bottom:2px}.apbct-green{color:green}.apbct-red{color:red}#apbct_negative_reports_table{width:100%;border-collapse:collapse;margin:10px 5px}#apbct_sending_report_div{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;flex-wrap:nowrap}#apbct_sending_report_disabled_notice{margin-right:5px}.apbct-real-user-popup-img{align-self:start;margin:0!important;height:1.1em;vertical-align:initial}@keyframes apbct_pulsate{0%{transform:translate(-50%,-50%) scale(1);opacity:.7}100%{transform:translate(-50%,-50%) scale(1.5);opacity:0}}@media (max-width:768px){.apbct_settings_top_info__btn{display:inline-grid;width:100%;gap:5px}#apbct_settings__advanced_settings_inner,#apbct_settings__after_advanced_settings{width:100%}.apbct_settings-field_content--radio,.apbct_settings-field_wrapper>.apbct_settings-field_description{width:100%}.apbct_settings_top_info__sub_btn .apbct_bottom_links--left,.apbct_settings_top_info__sub_btn .apbct_bottom_links--other{margin-left:0;margin-right:0}#apbct_hidden_section_nav ul{display:none;background-color:#dadee3bd;margin:0;padding:10px;border-radius:3px;right:0;width:auto}#apbct_hidden_section_nav{right:0;min-width:167px}.apbct_setting_radio::before{height:6px!important;width:6px!important;margin:4px!important}.apbct_setting_checkbox::before{height:15px!important;width:15px!important;margin:0 -.05rem!important}.apbct_setting_checkbox,.apbct_setting_radio{height:16px!important;width:16px!important}.apbct_hidden_section_nav_mob_btn{display:block;width:32px;height:28px;right:20px;top:20px;position:fixed}.apbct_hidden_section_nav_mob_btn-close{display:block;position:relative;float:right;width:20px;height:22px;top:-5px;right:-5px;background-image:url(images/cancel.svg);background-repeat:no-repeat;background-size:contain;background-position-y:center;text-align:center}.apbct_long_desc{left:-2%!important;max-width:95%;margin-top:10%}.apbct_long_desc__angle{display:none}.ct_rate_block{max-width:240px;margin-right:20px}.apbct_settings__key_line{display:flex;align-items:flex-start;flex-direction:column;width:100%}.apbct_settings__key_line__elements{margin:5px 0 5px 0}#apbct_setting_apikey,.apbct_setting__key_line__buttons,.apbct_settings__key_line__elements{width:100%}#cleantalk_apikey_wrapper{max-width:100%}#apbct_settings__button_section{position:fixed;right:20px}.apcbt_contact_data_encoder__line{max-width:100%;margin-top:5px}#apbct_summary_and_support-right_side{border-top:1px solid #dcdcde;border-left:0;margin:35px 0 0;padding:0;width:-webkit-fill-available}#apbct_summary_and_support-left_side{border-top:1px solid #dcdcde;width:-webkit-fill-available}#apbct_summary_and_support-sides_wrap{flex-direction:column}}.apbct-trial-renew-fullpage{display:flex;position:fixed;width:stretch;top:0;bottom:0;overflow-y:auto;justify-content:center;align-items:flex-start;font-family:Inter,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;color:#444;background-color:#f0f0f0;z-index:99}.apbct-trial-renew-fullpage-notice{padding:32px 64px 64px 64px!important;margin-top:5em!important;margin-bottom:5em!important}.apbct-trial-renew-fullpage-body{margin-top:32px;margin-left:32px}.apbct-banner-header{display:flex;justify-content:space-between}.apbct-trial-renew-fullpage-body .apbct-banner-button-wrapper{display:flex;margin:0}.apbct-trial-renew-fullpage-image{flex:1 1 0;min-width:0;max-width:576px;height:auto;object-fit:contain;margin:auto 0;align-self:flex-start}.apbct-banner-desc-block-row{line-height:22px}.apbct-banner-red-point{display:inline-block;width:8px;height:8px;background-color:#bb3323;border-radius:50%;margin-right:8px;vertical-align:middle}.apbct-banner-error{background-color:#fff!important;box-shadow:0 4px 15px #84848440!important;border-left-color:#bb3323!important}.apbct-banner-background-container{display:grid;max-width:644px;gap:16px;padding:32px;background-color:#f3f6f9;border-radius:8px}.apbct-banner-desc-block-title{font-size:18px;font-weight:700;line-height:22px;color:#0a0a0a;margin-bottom:8px}.apbct-banner-stat-value{margin-bottom:0;color:#444;vertical-align:middle}.apbct-banner-desc-block-text{font-size:14px;line-height:22px;color:#717182;margin-left:12px;margin-top:0;vertical-align:middle}.apbct-banner-desc-blocks{display:grid;gap:16px;flex:1 1 0;min-width:0}.apbct-banner-desc-blocks-content{display:flex;gap:72px;flex-wrap:wrap;justify-content:center;margin:32px 0}.apbct-banner-check{vertical-align:middle}.apbct-trial-renew-upgrade{display:none;text-align:center}.apbct-trial-renew-upgrade-link{font-size:16px;line-height:22px;letter-spacing:0;text-decoration:underline solid 0 #026e88!important;text-underline-offset:25%;color:#026e88!important}@media (max-width:1366px){.apbct-banner-header{display:grid;gap:16px}.apbct-banner-desc-blocks-content{display:grid;gap:38px;place-items:center}.apbct-trial-renew-upgrade{display:block}}@media (max-width:1200px){.apbct-banner-desc-blocks-content{gap:32px}}@media (max-width:782px){.apbct-trial-renew-fullpage{padding-top:50px;padding-bottom:50px}.apbct-trial-renew-fullpage-notice{padding:24px 24px 32px 24px!important}.apbct-trial-renew-fullpage-body{margin-top:20px;margin-left:0}.apbct-banner-desc-blocks-content{gap:24px}.apbct-banner-background-container{padding:20px}.apbct-trial-renew-fullpage-image{width:100%}}@media (max-width:480px){.apbct-trial-renew-fullpage-notice{padding:16px 12px 24px 12px!important}.apbct-banner-background-container{padding:16px}.apbct-banner-desc-blocks-content{gap:16px}.apbct-banner-desc-block-title{font-size:16px}} \ No newline at end of file diff --git a/css/src/cleantalk-admin-settings-page.css b/css/src/cleantalk-admin-settings-page.css index 8b307d064..2922bd8a6 100644 --- a/css/src/cleantalk-admin-settings-page.css +++ b/css/src/cleantalk-admin-settings-page.css @@ -59,6 +59,10 @@ i.animate-spin { font-size: 14px; vertical-align: top; } +.apbct_settings-field_title--with-help{ + width: auto; + white-space: nowrap; +} .apbct_settings-field_title--radio{ display: inline-block; margin: 0; diff --git a/inc/cleantalk-settings.php b/inc/cleantalk-settings.php index f62950ca4..ab993d6fb 100644 --- a/inc/cleantalk-settings.php +++ b/inc/cleantalk-settings.php @@ -659,7 +659,8 @@ function apbct_settings__set_fields() 'data__email_decoder_obfuscation_mode', 'data__email_decoder_obfuscation_custom_text', 'data__email_decoder_encode_phone_numbers', - 'data__email_decoder_encode_email_addresses' + 'data__email_decoder_encode_email_addresses', + 'data__email_decoder_excluded_strings' ), 'long_description' => true, ), @@ -676,6 +677,14 @@ function apbct_settings__set_fields() 'parent' => 'data__email_decoder', 'long_description' => true, ), + 'data__email_decoder_excluded_strings' => array( + 'type' => 'textarea', + 'title' => __('Do not encode these contacts', 'cleantalk-spam-protect'), + 'description' => ContactsEncoder::getExcludedStringsDescription(), + 'parent' => 'data__email_decoder', + 'class' => 'apbct_settings-field_wrapper--sub', + 'long_description' => true, + ), 'data__email_decoder_obfuscation_mode' => array( 'title' => __('Encoder obfuscation mode', 'cleantalk-spam-protect'), 'description' => ContactsEncoder::getObfuscationModesDescription(), @@ -2382,6 +2391,7 @@ function apbct_settings__validate($incoming_settings) 'data__email_decoder_obfuscation_mode', 'data__email_decoder_obfuscation_custom_text', 'data__email_decoder_buffer', + 'data__email_decoder_excluded_strings', ); $incoming_settings = apbct_settings__keep_settings_state_values( $incoming_settings, @@ -2716,6 +2726,13 @@ function apbct_settings__validate($incoming_settings) $incoming_settings['data__email_decoder_obfuscation_custom_text'] = ContactsEncoder::getDefaultReplacingText(); } + $excluded_strings = apbct_settings__sanitize__exclusions( + isset($incoming_settings['data__email_decoder_excluded_strings']) + ? $incoming_settings['data__email_decoder_excluded_strings'] + : '' + ); + $incoming_settings['data__email_decoder_excluded_strings'] = $excluded_strings ? $excluded_strings : ''; + //sync discussion and plugin settings if (isset($incoming_settings['cleantalk_allowed_moderation'])) { update_option('cleantalk_allowed_moderation', TT::toString($incoming_settings['cleantalk_allowed_moderation'], '0')); @@ -3434,6 +3451,10 @@ function apbct_settings__get_long_descriptions_data() 'title' => __('Contact data encoding: phone numbers', 'cleantalk-spam-protect'), 'desc' => ContactsEncoder::getPhonesEncodingLongDescription(), ), + 'data__email_decoder_excluded_strings' => array( + 'title' => __('Contact data encoding: do not encode these contacts', 'cleantalk-spam-protect'), + 'desc' => ContactsEncoder::getExcludedStringsLongDescription(), + ), 'data__email_decoder' => array( 'title' => __('Contact data encoding', 'cleantalk-spam-protect'), 'desc' => ContactsEncoder::getEmailEncoderCommonLongDescription(), diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php index c7ecd24fe..a6af48c1f 100644 --- a/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php @@ -470,6 +470,36 @@ public static function getEmailsEncodingDescription() return __('Encode email addresses', 'cleantalk-spam-protect'); } + public static function getExcludedStringsDescription() + { + return __( + 'List emails, phone numbers or text fragments that must never be encoded. Use commas or new lines as separator. Each value is limited to 128 characters, up to 20 values.', + 'cleantalk-spam-protect' + ); + } + + public static function getExcludedStringsLongDescription() + { + $tmp = ' +

%s

+

%s

+

%s

+

%s

+

%s

+

%s

+ '; + $tmp = sprintf( + $tmp, + __('Use this list when you need a contact to stay readable everywhere, including titles, menus and widgets — WordPress does not run shortcodes in those places.', 'cleantalk-spam-protect'), + __('Put one value per line or separate them with commas:', 'cleantalk-spam-protect'), + __('an email, e.g. support@example.com', 'cleantalk-spam-protect'), + __('a phone number, e.g. +1 (234) 567-8901 — format differences are ignored', 'cleantalk-spam-protect'), + __('a text fragment, e.g. example.com to skip all emails on that domain', 'cleantalk-spam-protect'), + __('Each value is sliced to 128 characters. The list is limited to 20 values.', 'cleantalk-spam-protect') + ); + return $tmp; + } + public static function getPhonesEncodingLongDescription() { $tmp = ' diff --git a/lib/Cleantalk/ApbctWP/PluginSettingsPage/SettingsField.php b/lib/Cleantalk/ApbctWP/PluginSettingsPage/SettingsField.php index a4fc546f4..95914bf68 100644 --- a/lib/Cleantalk/ApbctWP/PluginSettingsPage/SettingsField.php +++ b/lib/Cleantalk/ApbctWP/PluginSettingsPage/SettingsField.php @@ -480,7 +480,11 @@ class="apbct_setting_{{type}} apbct_setting---{{name}}" */ private function getInputTextarea() { - $title_layout = '

{{title}} {{popup_description}}

'; + $title_class = 'apbct_settings-field_title apbct_settings-field_title--{{type}}'; + if ( $this->description_popup !== '' ) { + $title_class .= ' apbct_settings-field_title--with-help'; + } + $title_layout = '

{{title}} {{popup_description}}

'; $raw_value = empty($this->value) ? TT::getArrayValueAsString($this->params, 'value') : $this->value; if (is_array($raw_value)) { diff --git a/lib/Cleantalk/ApbctWP/State.php b/lib/Cleantalk/ApbctWP/State.php index 53c0ff27b..92b1cbd0d 100644 --- a/lib/Cleantalk/ApbctWP/State.php +++ b/lib/Cleantalk/ApbctWP/State.php @@ -94,6 +94,7 @@ class State extends \Cleantalk\Common\State 'data__email_decoder_obfuscation_custom_text' => '', 'data__email_decoder_encode_phone_numbers' => 0, 'data__email_decoder_encode_email_addresses' => 1, + 'data__email_decoder_excluded_strings' => '', 'data__wc_store_blocked_orders' => 0, // Exclusions diff --git a/lib/Cleantalk/Common/ContactsEncoder/ContactsEncoder.php b/lib/Cleantalk/Common/ContactsEncoder/ContactsEncoder.php index 46425bb20..beda1fafc 100644 --- a/lib/Cleantalk/Common/ContactsEncoder/ContactsEncoder.php +++ b/lib/Cleantalk/Common/ContactsEncoder/ContactsEncoder.php @@ -308,6 +308,10 @@ public function modifyGlobalEmails($content) $position = $this->advanceMatchCursor($matches[0], $match_cursor); + if ( $this->exclusions->isContactExcluded($matches[0]) ) { + return $matches[0]; + } + if ( isset($matches[3]) && in_array(strtolower($matches[3]), ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']) ) { return $matches[0]; } @@ -372,6 +376,10 @@ function ($matches) use (&$match_cursor) { $position = $this->advanceMatchCursor($matches[0], $match_cursor); + if ( $this->exclusions->isContactExcluded($matches[0]) ) { + return $matches[0]; + } + if ( $this->helper->isTelTag($matches[0]) ) { return $this->encodeTelLink($matches[0]); } diff --git a/lib/Cleantalk/Common/ContactsEncoder/Dto/Params.php b/lib/Cleantalk/Common/ContactsEncoder/Dto/Params.php index e73403182..f0e4acff3 100644 --- a/lib/Cleantalk/Common/ContactsEncoder/Dto/Params.php +++ b/lib/Cleantalk/Common/ContactsEncoder/Dto/Params.php @@ -19,4 +19,11 @@ class Params public $obfuscation_text = 'hidden contact data'; public $do_encode_emails = 1; public $do_encode_phones = 0; + + /** + * Emails, phones or text fragments that must never be encoded. + * + * @var string[] + */ + public $excluded_strings = array(); } diff --git a/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php b/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php index 39c1dbf31..98840d171 100644 --- a/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php +++ b/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php @@ -82,4 +82,118 @@ protected function byLoggedIn() { return $this->params->is_logged_in; } + + /** + * Split a settings textarea into unique non-empty exclusion strings. + * + * @param string $raw + * + * @return string[] + * @psalm-suppress PossiblyUnusedMethod Public API for host apps that store the list as raw text + */ + public static function parseExcludedStrings($raw) + { + if ( ! is_string($raw) || $raw === '' ) { + return array(); + } + + $parts = preg_split('/[\r\n,]+/', $raw); + if ( ! is_array($parts) ) { + return array(); + } + + $result = array(); + foreach ( $parts as $part ) { + $part = trim($part, " \n\r\t\v\x00"); + if ( $part !== '' ) { + $result[] = $part; + } + } + + return array_values(array_unique($result)); + } + + /** + * Whether a matched email or phone must stay unencoded. + * + * @param string $match + * + * @return bool + */ + public function isContactExcluded($match) + { + if ( ! is_string($match) || $match === '' ) { + return false; + } + + if ( empty($this->params->excluded_strings) || ! is_array($this->params->excluded_strings) ) { + return false; + } + + $normalized_match = $this->normalizeContactString($match); + $match_digits = $this->extractDigits($match); + + foreach ( $this->params->excluded_strings as $exclusion ) { + if ( ! is_string($exclusion) || $exclusion === '' ) { + continue; + } + + $normalized_exclusion = $this->normalizeContactString($exclusion); + if ( $normalized_exclusion === '' ) { + continue; + } + + if ( $normalized_match === $normalized_exclusion ) { + return true; + } + + if ( strpos($normalized_match, $normalized_exclusion) !== false ) { + return true; + } + + $exclusion_digits = $this->extractDigits($exclusion); + if ( + strlen($exclusion_digits) >= 8 + && strlen($match_digits) >= 8 + && ( + strpos($match_digits, $exclusion_digits) !== false + || strpos($exclusion_digits, $match_digits) !== false + ) + ) { + return true; + } + } + + return false; + } + + /** + * @param string $value + * + * @return string + */ + private function normalizeContactString($value) + { + $value = trim($value, " \n\r\t\v\x00"); + if ( stripos($value, 'mailto:') === 0 ) { + $value = substr($value, 7); + } + if ( stripos($value, 'tel:') === 0 ) { + $value = substr($value, 4); + } + + return strtolower(trim($value, " \n\r\t\v\x00")); + } + + /** + * @param string $value + * + * @return string + */ + private function extractDigits($value) + { + $digits = preg_replace('/\D+/', '', $value); + + return is_string($digits) ? $digits : ''; + } } diff --git a/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php b/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php index a48357a8a..a2f6532bd 100644 --- a/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php +++ b/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php @@ -651,6 +651,33 @@ public function testByContentSignsIgnoresNonArrayFilterResult() $this->assertFalse($result); } + public function testIsContactExcludedMatchesEmailAndPhoneVariants() + { + $params = new Params(); + $params->api_key = 'testapikey'; + $params->excluded_strings = array('keep@example.com', '+1 800 555-1234', 'company.org'); + $service = new ExclusionsService($params); + + $this->assertTrue($service->isContactExcluded('keep@example.com')); + $this->assertTrue($service->isContactExcluded('mailto:keep@example.com')); + $this->assertTrue($service->isContactExcluded('office@company.org')); + $this->assertTrue($service->isContactExcluded('(800) 555-1234')); + $this->assertFalse($service->isContactExcluded('public@other.net')); + $this->assertFalse($service->isContactExcluded('(800) 555-9999')); + } + + public function testParseExcludedStringsSplitsLinesAndCommas() + { + $parsed = \Cleantalk\Common\ContactsEncoder\Exclusions\ExclusionsService::parseExcludedStrings( + "keep@example.com\n+1 800 555-1234, example.com\n" + ); + + $this->assertSame( + array('keep@example.com', '+1 800 555-1234', 'example.com'), + $parsed + ); + } + /** * Helper method to invoke private methods for testing * diff --git a/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php b/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php index 6d31823f9..dc6118ee2 100644 --- a/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php +++ b/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php @@ -224,6 +224,94 @@ public function testModifyContentSkipsEmailsInsideOptionTags() $this->assertStringContainsString('apbct-email-encoder', $result); } + /** + * Settings skip list keeps listed emails plain, including in title-like strings. + */ + public function testModifyContentSkipsEmailsFromExcludedStringsSetting() + { + global $apbct; + $previous = isset($apbct->settings['data__email_decoder_excluded_strings']) + ? $apbct->settings['data__email_decoder_excluded_strings'] + : ''; + + $keep = 'keep@example.com'; + $encode = 'public@other.net'; + $title = 'Contact ' . $keep . ' or ' . $encode; + + $apbct->settings['data__email_decoder_excluded_strings'] = $keep; + $this->contacts_encoder->dropInstance(); + $this->contacts_encoder = apbctGetContactsEncoder(); + + $result = $this->contacts_encoder->modifyContent($title); + + $apbct->settings['data__email_decoder_excluded_strings'] = $previous; + $this->contacts_encoder->dropInstance(); + $this->contacts_encoder = apbctGetContactsEncoder(); + + $this->assertStringContainsString($keep, $result); + $this->assertStringNotContainsString($encode, $result); + $this->assertStringContainsString('apbct-email-encoder', $result); + } + + /** + * Domain fragment in the skip list excludes every matching email. + */ + public function testModifyContentSkipsEmailsByDomainFragment() + { + global $apbct; + $previous = isset($apbct->settings['data__email_decoder_excluded_strings']) + ? $apbct->settings['data__email_decoder_excluded_strings'] + : ''; + + $skip = 'office@company.org'; + $encode = 'user@other.net'; + + $apbct->settings['data__email_decoder_excluded_strings'] = 'company.org'; + $this->contacts_encoder->dropInstance(); + $this->contacts_encoder = apbctGetContactsEncoder(); + + $result = $this->contacts_encoder->modifyContent($skip . ' ' . $encode); + + $apbct->settings['data__email_decoder_excluded_strings'] = $previous; + $this->contacts_encoder->dropInstance(); + $this->contacts_encoder = apbctGetContactsEncoder(); + + $this->assertStringContainsString($skip, $result); + $this->assertStringNotContainsString($encode, $result); + $this->assertStringContainsString('apbct-email-encoder', $result); + } + + /** + * Phone skip list matches format variants by digits. + */ + public function testModifyContentSkipsPhonesFromExcludedStringsSetting() + { + global $apbct; + $previous_strings = isset($apbct->settings['data__email_decoder_excluded_strings']) + ? $apbct->settings['data__email_decoder_excluded_strings'] + : ''; + $previous_phones = $apbct->settings['data__email_decoder_encode_phone_numbers']; + + $keep = '(800) 555-1234'; + $encode = '(800) 555-9999'; + + $apbct->settings['data__email_decoder_encode_phone_numbers'] = 1; + $apbct->settings['data__email_decoder_excluded_strings'] = '+1 800 555-1234'; + $this->contacts_encoder->dropInstance(); + $this->contacts_encoder = apbctGetContactsEncoder(); + + $result = $this->contacts_encoder->modifyContent('Call ' . $keep . ' or ' . $encode); + + $apbct->settings['data__email_decoder_excluded_strings'] = $previous_strings; + $apbct->settings['data__email_decoder_encode_phone_numbers'] = $previous_phones; + $this->contacts_encoder->dropInstance(); + $this->contacts_encoder = apbctGetContactsEncoder(); + + $this->assertStringContainsString($keep, $result); + $this->assertStringNotContainsString($encode, $result); + $this->assertStringContainsString('apbct-email-encoder', $result); + } + /** * aria-label values must survive email encoding round-trip intact. */ @@ -367,6 +455,19 @@ public function testGetPhonesEncodingLongDescription() $this->assertStringEndsWith('>', $description); } + public function testGetExcludedStringsDescription() + { + $description = ContactsEncoder::getExcludedStringsDescription(); + $this->assertIsString($description); + $this->assertNotEmpty($description); + + $long = ContactsEncoder::getExcludedStringsLongDescription(); + $this->assertIsString($long); + $this->assertStringContainsString('128 characters', $long); + $this->assertStringNotContainsString('apbct_skip_encoding', $description); + $this->assertStringNotContainsString('apbct_skip_encoding', $long); + } + public function testModifyBuffer() { global $apbct; From d05dc4aed5469e88165a20f72f6dc827034f58c6 Mon Sep 17 00:00:00 2001 From: svedge Date: Thu, 10 Sep 2026 11:24:46 +0500 Subject: [PATCH 2/6] fix psalm --- lib/Cleantalk/Common/Helper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Cleantalk/Common/Helper.php b/lib/Cleantalk/Common/Helper.php index ee38aa92b..27f14fe89 100644 --- a/lib/Cleantalk/Common/Helper.php +++ b/lib/Cleantalk/Common/Helper.php @@ -593,9 +593,9 @@ public static function ipResolve($ip) * Resolve DNS to IP * * @param $host - * @param bool $out + * @param bool|string $out * - * @return bool + * @return bool|string * @psalm-suppress PossiblyUnusedMethod */ public static function dnsResolve($host, $out = false) From 11cebac2b09d7ec9bc900b2ff3faf8c098417996 Mon Sep 17 00:00:00 2001 From: svedge Date: Thu, 10 Sep 2026 13:56:51 +0500 Subject: [PATCH 3/6] Upd. ContactEncoder. Update option, exclude only by new line. --- inc/cleantalk-settings.php | 29 +++++++++++++++++-- .../ContactsEncoder/ContactsEncoder.php | 4 +-- .../Exclusions/ExclusionsService.php | 4 +-- .../Exclusions/TestExclusionsService.php | 16 ++++++++-- .../ContactsEncoder/TestContactsEncoder.php | 3 ++ tests/StandaloneFunctions/SettingsTest.php | 21 ++++++++++++++ 6 files changed, 69 insertions(+), 8 deletions(-) diff --git a/inc/cleantalk-settings.php b/inc/cleantalk-settings.php index ab993d6fb..69b0ca5ea 100644 --- a/inc/cleantalk-settings.php +++ b/inc/cleantalk-settings.php @@ -2726,12 +2726,11 @@ function apbct_settings__validate($incoming_settings) $incoming_settings['data__email_decoder_obfuscation_custom_text'] = ContactsEncoder::getDefaultReplacingText(); } - $excluded_strings = apbct_settings__sanitize__exclusions( + $incoming_settings['data__email_decoder_excluded_strings'] = apbct_settings__sanitize__excluded_contact_strings( isset($incoming_settings['data__email_decoder_excluded_strings']) ? $incoming_settings['data__email_decoder_excluded_strings'] : '' ); - $incoming_settings['data__email_decoder_excluded_strings'] = $excluded_strings ? $excluded_strings : ''; //sync discussion and plugin settings if (isset($incoming_settings['cleantalk_allowed_moderation'])) { @@ -3281,6 +3280,32 @@ function apbct_settings__sanitize__exclusions($exclusions, $regexp = false, $url } } +/** + * Sanitize the Contacts Encoder skip-list: one value per line, max 20 items, 128 chars each. + * + * @param mixed $exclusions + * + * @return string + */ +function apbct_settings__sanitize__excluded_contact_strings($exclusions) +{ + if ( ! is_string($exclusions) ) { + return ''; + } + + $parts = \Cleantalk\Common\ContactsEncoder\Exclusions\ExclusionsService::parseExcludedStrings($exclusions); + $parts = array_slice($parts, 0, 20); + $result = array(); + foreach ( $parts as $part ) { + $part = trim(substr($part, 0, 128), " \n\r\t\v\x00"); + if ( $part !== '' ) { + $result[] = $part; + } + } + + return implode("\n", array_values(array_unique($result))); +} + function apbct_settings__get__long_description() { global $apbct; diff --git a/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php b/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php index a6af48c1f..3cceeda4a 100644 --- a/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php +++ b/lib/Cleantalk/ApbctWP/ContactsEncoder/ContactsEncoder.php @@ -473,7 +473,7 @@ public static function getEmailsEncodingDescription() public static function getExcludedStringsDescription() { return __( - 'List emails, phone numbers or text fragments that must never be encoded. Use commas or new lines as separator. Each value is limited to 128 characters, up to 20 values.', + 'List emails, phone numbers or text fragments that must never be encoded. Put one value per line. Each value is limited to 128 characters, up to 20 values.', 'cleantalk-spam-protect' ); } @@ -491,7 +491,7 @@ public static function getExcludedStringsLongDescription() $tmp = sprintf( $tmp, __('Use this list when you need a contact to stay readable everywhere, including titles, menus and widgets — WordPress does not run shortcodes in those places.', 'cleantalk-spam-protect'), - __('Put one value per line or separate them with commas:', 'cleantalk-spam-protect'), + __('Put one value per line:', 'cleantalk-spam-protect'), __('an email, e.g. support@example.com', 'cleantalk-spam-protect'), __('a phone number, e.g. +1 (234) 567-8901 — format differences are ignored', 'cleantalk-spam-protect'), __('a text fragment, e.g. example.com to skip all emails on that domain', 'cleantalk-spam-protect'), diff --git a/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php b/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php index 98840d171..c851a6cf9 100644 --- a/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php +++ b/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php @@ -84,7 +84,7 @@ protected function byLoggedIn() } /** - * Split a settings textarea into unique non-empty exclusion strings. + * Split a settings textarea into unique non-empty exclusion strings (one value per line). * * @param string $raw * @@ -97,7 +97,7 @@ public static function parseExcludedStrings($raw) return array(); } - $parts = preg_split('/[\r\n,]+/', $raw); + $parts = preg_split('/\R+/', $raw); if ( ! is_array($parts) ) { return array(); } diff --git a/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php b/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php index a2f6532bd..eba4f5123 100644 --- a/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php +++ b/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php @@ -666,10 +666,10 @@ public function testIsContactExcludedMatchesEmailAndPhoneVariants() $this->assertFalse($service->isContactExcluded('(800) 555-9999')); } - public function testParseExcludedStringsSplitsLinesAndCommas() + public function testParseExcludedStringsSplitsLines() { $parsed = \Cleantalk\Common\ContactsEncoder\Exclusions\ExclusionsService::parseExcludedStrings( - "keep@example.com\n+1 800 555-1234, example.com\n" + "keep@example.com\n+1 800 555-1234\nexample.com\n" ); $this->assertSame( @@ -678,6 +678,18 @@ public function testParseExcludedStringsSplitsLinesAndCommas() ); } + public function testParseExcludedStringsKeepsCommaInsideALine() + { + $parsed = \Cleantalk\Common\ContactsEncoder\Exclusions\ExclusionsService::parseExcludedStrings( + "keep@example.com, office@example.com\n+1 800 555-1234" + ); + + $this->assertSame( + array('keep@example.com, office@example.com', '+1 800 555-1234'), + $parsed + ); + } + /** * Helper method to invoke private methods for testing * diff --git a/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php b/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php index dc6118ee2..b767ed4b8 100644 --- a/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php +++ b/tests/ApbctWP/ContactsEncoder/TestContactsEncoder.php @@ -464,6 +464,9 @@ public function testGetExcludedStringsDescription() $long = ContactsEncoder::getExcludedStringsLongDescription(); $this->assertIsString($long); $this->assertStringContainsString('128 characters', $long); + $this->assertStringContainsString('one value per line', $description); + $this->assertStringNotContainsString('comma', strtolower($description)); + $this->assertStringNotContainsString('comma', strtolower($long)); $this->assertStringNotContainsString('apbct_skip_encoding', $description); $this->assertStringNotContainsString('apbct_skip_encoding', $long); } diff --git a/tests/StandaloneFunctions/SettingsTest.php b/tests/StandaloneFunctions/SettingsTest.php index a9e9c6ca6..b3f7f6b9f 100644 --- a/tests/StandaloneFunctions/SettingsTest.php +++ b/tests/StandaloneFunctions/SettingsTest.php @@ -122,6 +122,27 @@ public function test_apbct_settings__sanitize__exclusions__wrong_params() $this->assertIsString(apbct_settings__sanitize__exclusions('', 0)); } + public function test_apbct_settings__sanitize__excluded_contact_strings() + { + $this->assertSame('', apbct_settings__sanitize__excluded_contact_strings(null)); + $this->assertSame('', apbct_settings__sanitize__excluded_contact_strings('')); + $this->assertSame( + "keep@example.com\n+1 800 555-1234", + apbct_settings__sanitize__excluded_contact_strings("keep@example.com\n+1 800 555-1234\n") + ); + $this->assertSame( + 'keep@example.com, office@example.com', + apbct_settings__sanitize__excluded_contact_strings('keep@example.com, office@example.com') + ); + + $lines = array(); + for ( $i = 1; $i <= 25; $i++ ) { + $lines[] = 'user' . $i . '@example.com'; + } + $sanitized = apbct_settings__sanitize__excluded_contact_strings(implode("\n", $lines)); + $this->assertCount(20, explode("\n", $sanitized)); + } + public function test_apbct_settings__set_fields() { $fields = apbct_settings__set_fields(); From 9d31b948a49700971bde3431c0ebd35976b90d11 Mon Sep 17 00:00:00 2001 From: svedge Date: Thu, 10 Sep 2026 18:39:34 +0500 Subject: [PATCH 4/6] Upd. ContactEncoder. Update option, exclude only by new line. --- .../Exclusions/ExclusionsService.php | 23 ++++++++++++------- lib/Cleantalk/Common/Helper.php | 11 +++++---- .../Exclusions/TestExclusionsService.php | 13 +++++++++++ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php b/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php index c851a6cf9..692324a17 100644 --- a/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php +++ b/lib/Cleantalk/Common/ContactsEncoder/Exclusions/ExclusionsService.php @@ -151,16 +151,23 @@ public function isContactExcluded($match) return true; } - $exclusion_digits = $this->extractDigits($exclusion); + // Digit compare is for phones only. Emails with long number sequences + // must not match an excluded phone (or the other way around). if ( - strlen($exclusion_digits) >= 8 - && strlen($match_digits) >= 8 - && ( - strpos($match_digits, $exclusion_digits) !== false - || strpos($exclusion_digits, $match_digits) !== false - ) + strpos($normalized_match, '@') === false + && strpos($normalized_exclusion, '@') === false ) { - return true; + $exclusion_digits = $this->extractDigits($exclusion); + if ( + strlen($exclusion_digits) >= 8 + && strlen($match_digits) >= 8 + && ( + strpos($match_digits, $exclusion_digits) !== false + || strpos($exclusion_digits, $match_digits) !== false + ) + ) { + return true; + } } } diff --git a/lib/Cleantalk/Common/Helper.php b/lib/Cleantalk/Common/Helper.php index 27f14fe89..d0928fbcb 100644 --- a/lib/Cleantalk/Common/Helper.php +++ b/lib/Cleantalk/Common/Helper.php @@ -590,12 +590,15 @@ public static function ipResolve($ip) } /** - * Resolve DNS to IP + * Resolve DNS to IP. * - * @param $host - * @param bool|string $out + * $host must be a full URL that passes FILTER_VALIDATE_URL (scheme required). + * A bare hostname such as HTTP_HOST is rejected and $out is returned. * - * @return bool|string + * @param string $host Full URL (FILTER_VALIDATE_URL) + * @param bool|string $out Fallback when $host is invalid or lookup fails + * + * @return bool|string First A-record IPv4 on success, otherwise $out * @psalm-suppress PossiblyUnusedMethod */ public static function dnsResolve($host, $out = false) diff --git a/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php b/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php index eba4f5123..2c16d61ab 100644 --- a/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php +++ b/tests/ApbctWP/ContactsEncoder/Exclusions/TestExclusionsService.php @@ -666,6 +666,19 @@ public function testIsContactExcludedMatchesEmailAndPhoneVariants() $this->assertFalse($service->isContactExcluded('(800) 555-9999')); } + public function testIsContactExcludedDoesNotCrossMatchEmailAndPhoneDigits() + { + $params = new Params(); + $params->api_key = 'testapikey'; + $params->excluded_strings = array('+1 800 555-1234', 'user12345678@example.com'); + $service = new ExclusionsService($params); + + $this->assertFalse($service->isContactExcluded('ticket18005551234@shop.com')); + $this->assertFalse($service->isContactExcluded('123-456-7890')); + $this->assertTrue($service->isContactExcluded('(800) 555-1234')); + $this->assertTrue($service->isContactExcluded('user12345678@example.com')); + } + public function testParseExcludedStringsSplitsLines() { $parsed = \Cleantalk\Common\ContactsEncoder\Exclusions\ExclusionsService::parseExcludedStrings( From 9bec5330b3f5189ad5d31ec8332f84a928f4e827 Mon Sep 17 00:00:00 2001 From: svedge Date: Thu, 10 Sep 2026 18:46:57 +0500 Subject: [PATCH 5/6] Upd. ContactEncoder. Update option, exclude only by new line. --- lib/Cleantalk/Common/Helper.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Cleantalk/Common/Helper.php b/lib/Cleantalk/Common/Helper.php index d0928fbcb..d1a0d891a 100644 --- a/lib/Cleantalk/Common/Helper.php +++ b/lib/Cleantalk/Common/Helper.php @@ -593,9 +593,10 @@ public static function ipResolve($ip) * Resolve DNS to IP. * * $host must be a full URL that passes FILTER_VALIDATE_URL (scheme required). - * A bare hostname such as HTTP_HOST is rejected and $out is returned. + * A bare hostname such as HTTP_HOST, or any non-string value, is rejected + * and $out is returned. * - * @param string $host Full URL (FILTER_VALIDATE_URL) + * @param mixed $host Full URL (FILTER_VALIDATE_URL); other types are rejected * @param bool|string $out Fallback when $host is invalid or lookup fails * * @return bool|string First A-record IPv4 on success, otherwise $out @@ -604,7 +605,7 @@ public static function ipResolve($ip) public static function dnsResolve($host, $out = false) { // Check if the $url is set and it is an url - if ( ! $host || ! filter_var($host, FILTER_VALIDATE_URL)) { + if ( ! is_string($host) || $host === '' || ! filter_var($host, FILTER_VALIDATE_URL)) { return $out; } From cbf0ce52c65a4b7f608e05ea8a8a0b64150d58f5 Mon Sep 17 00:00:00 2001 From: svedge Date: Fri, 11 Sep 2026 16:25:14 +0500 Subject: [PATCH 6/6] fix psalm --- lib/Cleantalk/Common/Helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cleantalk/Common/Helper.php b/lib/Cleantalk/Common/Helper.php index 4abf9e8af..63b07cfe9 100644 --- a/lib/Cleantalk/Common/Helper.php +++ b/lib/Cleantalk/Common/Helper.php @@ -602,7 +602,7 @@ public static function ipResolve($ip) * @return bool|string First A-record IPv4 on success, otherwise $out * @psalm-suppress PossiblyUnusedMethod */ - public static function dnsResolve($host) + public static function dnsResolve($host, $out = false) { // Check if the $url is set and it is an url if ( ! is_string($host) || $host === '' || ! filter_var($host, FILTER_VALIDATE_URL)) {