diff --git a/.gitignore b/.gitignore index 7681e02..b9ac0c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /* !/cleantalk.antispam !/cleantalk.antispam/* +!/cleantalk +!/cleantalk/* !/.github !/.github/* \ No newline at end of file diff --git a/cleantalk.antispam/assets/js/external.js b/cleantalk.antispam/assets/js/external.js new file mode 100644 index 0000000..a6f85c4 --- /dev/null +++ b/cleantalk.antispam/assets/js/external.js @@ -0,0 +1,326 @@ + +window.addEventListener('load', function() { + // if ( ! +ctPublic.settings__forms__check_external ) { + // return; + // } + + setTimeout(function() { + ctProtectExternal(); + }, 2000); +}); + +/** + * Handle external forms + */ +function ctProtectExternal() { + for (let i = 0; i < document.forms.length; i++) { + const currentForm = document.forms[i]; + + // Ajax checking for the integrated forms - will be changed the whole form object to make protection + if (isIntegratedForm(currentForm)) { + apbctProcessExternalForm(currentForm, i, document); + } + } +} + +/** + * Check if the form is integrated + * @param {HTMLFormElement} form + * @returns {boolean} + */ +function isIntegratedForm(form) { + // if form contains input with class b24-form-control + if (form.querySelector('input[class*="b24-form-control"]')) { + console.log('Form is integrated'); + return true; + } + console.log('Form is not integrated'); + return false; +} + +/** + * Process external form + * @param {HTMLFormElement} form + * @param {number} iterator + * @param {HTMLDocument} document + */ +function apbctProcessExternalForm(currentForm, iterator, documentObject) { + console.log('Processing external form'); + + const cleantalkPlaceholder = document.createElement('i'); + cleantalkPlaceholder.className = 'cleantalk_placeholder'; + cleantalkPlaceholder.style = 'display: none'; + + currentForm.parentElement.insertBefore(cleantalkPlaceholder, currentForm); + + // Deleting form to prevent submit event + const prev = currentForm.previousSibling; + const formHtml = currentForm.outerHTML; + const formOriginal = currentForm; + const formContent = currentForm.querySelectorAll('input, textarea, select'); + + // Remove the original form + currentForm.parentElement.removeChild(currentForm); + + // Insert a clone + const placeholder = document.createElement('div'); + placeholder.innerHTML = formHtml; + const clonedForm = placeholder.firstElementChild; + prev.after(clonedForm); + + if (formContent && formContent.length > 0) { + formContent.forEach(function(content) { + if (content && content.name && content.type !== 'submit' && content.type !== 'button') { + if (content.type === 'checkbox') { + const checkboxInput = clonedForm.querySelector(`input[name="${content.name}"]`); + if (checkboxInput) { + checkboxInput.checked = content.checked; + } + } else { + const input = clonedForm.querySelector( + `input[name="${content.name}"], ` + + `textarea[name="${content.name}"], ` + + `select[name="${content.name}"]`, + ); + if (input) { + input.value = content.value; + } + } + } + }); + } + + const forceAction = document.createElement('input'); + forceAction.name = 'action'; + forceAction.value = 'cleantalk_force_ajax_check'; + forceAction.type = 'hidden'; + + const reUseCurrentForm = documentObject.forms[iterator]; + + reUseCurrentForm.appendChild(forceAction); + reUseCurrentForm.apbctPrev = prev; + reUseCurrentForm.apbctFormOriginal = formOriginal; + + documentObject.forms[iterator].onsubmit = function(event) { + event.preventDefault(); + sendAjaxCheckingFormData(event.currentTarget); + }; +} + +/** + * Replace input values from one form to another + * @param {HTMLElement} sourceForm + * @param {HTMLElement} targetForm + */ +function apbctReplaceInputsValuesFromOtherForm(sourceForm, targetForm) { + if (!sourceForm || !targetForm) return; + + const sourceInputs = sourceForm.querySelectorAll('input, textarea, select'); + sourceInputs.forEach(function(sourceInput) { + if (sourceInput.name && sourceInput.type !== 'submit' && sourceInput.type !== 'button') { + const targetInput = targetForm.querySelector(`[name="${sourceInput.name}"]`); + if (targetInput) { + if (sourceInput.type === 'checkbox' || sourceInput.type === 'radio') { + targetInput.checked = sourceInput.checked; + } else { + targetInput.value = sourceInput.value; + } + } + } + }); +} + +function apbctParseBlockMessageForAajax(result) { + let message = ''; + console.table('result',result) + if (result.apbct && result.apbct.comment) { + message = result.apbct.comment; + } else if (result.error && result.error.msg) { + message = result.error.msg; + } else if (result.data && result.data.message) { + message = result.data.message; + } + console.table('message',message) + if (message) { + alert(message); + if (result.apbct && result.apbct.stop_script == 1) { + window.stop(); + } + } +} +/** + * Sending Ajax for checking form data + * @param {HTMLElement} form + */ +function sendAjaxCheckingFormData(form) { + const botDetectorEventToken = getBotDetectorToken(); + const data = prepareFormData(form, botDetectorEventToken); + + if (typeof BX !== 'undefined' && BX.ajax) { + sendBitrixAjax(form, data); + } else { + sendNativeAjax(form, data); + } +} + +/** + * Get bot detector token from localStorage + */ +function getBotDetectorToken() { + let token = localStorage.getItem('bot_detector_event_token'); + if (typeof token === 'string') { + token = JSON.parse(token); + if (typeof token === 'object') { + return token.value; + } + } + return token; +} + +/** + * Prepare form data for submission + */ +function prepareFormData(form, botDetectorToken) { + const data = { + 'ct_bot_detector_event_token': botDetectorToken, + }; + + const elements = Array.from(form.elements); + elements.forEach((elem, index) => { + const key = elem.name || `input_${index}`; + data[key] = elem.value; + }); + + return data; +} + +/** + * Send AJAX using Bitrix framework + */ +function sendBitrixAjax(form, data) { + BX.ajax({ + url: '/bitrix/components/cleantalk/ajax/ajax.php', + method: 'POST', + data: data, + dataType: 'json', + async: false, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + }, + onsuccess: (result) => handleSuccess(form, result), + onfailure: (result) => handleFailure(form, result) + }); +} + +/** + * Send AJAX using native XMLHttpRequest + */ +function sendNativeAjax(form, data) { + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/bitrix/components/cleantalk/ajax/ajax.php', false); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + + const formData = new URLSearchParams(); + for (const key in data) { + formData.append(key, data[key]); + } + + xhr.send(formData); + + if (xhr.status === 200) { + try { + const result = JSON.parse(xhr.responseText); + handleSuccess(form, result); + } catch (e) { + console.error('CleanTalk JSON parse error:', e); + restoreAndSubmitForm(form); + } + } else { + console.error('CleanTalk AJAX request failed:', xhr.status); + restoreAndSubmitForm(form); + } +} + +/** + * Handle successful AJAX response + */ +function handleSuccess(form, result) { + console.log('CleanTalk AJAX success:', result); + + const isAllowed = (result.apbct === undefined && result.data === undefined) || + (result.apbct !== undefined && !+result.apbct.blocked); + + const isBlocked = (result.apbct !== undefined && +result.apbct.blocked) || + (result.data !== undefined && result.data.message !== undefined); + + if (isAllowed) { + restoreAndSubmitForm(form, true); + } else if (isBlocked) { + apbctParseBlockMessageForAajax(result); + } +} + +/** + * Handle AJAX failure + */ +function handleFailure(form, result) { + console.error('CleanTalk AJAX error:', result); + restoreAndSubmitForm(form); +} + +/** + * Restore original form and trigger submission + */ +function restoreAndSubmitForm(form, isSuccess = false) { + const formNew = form; + const prev = form.apbctPrev; + const formOriginal = form.apbctFormOriginal; + + // Remove current form and restore original + form.parentElement.removeChild(form); + apbctReplaceInputsValuesFromOtherForm(formNew, formOriginal); + prev.after(formOriginal); + + // Clean up service fields + removeServiceFields(formOriginal); + + // Find and trigger submit button + const submitButton = findSubmitButton(formOriginal); + if (submitButton) { + console.log(`CleanTalk AJAX ${isSuccess ? 'success' : 'error'}:`, submitButton); + submitButton.click(); + + // Schedule external forms protection for successful cases + if (isSuccess) { + setTimeout(() => { + ctProtectExternal(); + }, 1500); + } + } +} + +/** + * Remove service fields from form + */ +function removeServiceFields(form) { + const selectors = [ + 'input[value="cleantalk_force_ajax_check"]', + 'input[name="ct_bot_detector_event_token"]' + ]; + + selectors.forEach(selector => { + form.querySelectorAll(selector).forEach(el => el.remove()); + }); +} + +/** + * Find submit button in form + */ +function findSubmitButton(form) { + const button = form.querySelector('button[type="submit"]'); + if (button) return button; + + const input = form.querySelector('input[type="submit"]'); + return input || null; +} diff --git a/cleantalk.antispam/assets/js/public.js b/cleantalk.antispam/assets/js/public.js new file mode 100644 index 0000000..0acb5bd --- /dev/null +++ b/cleantalk.antispam/assets/js/public.js @@ -0,0 +1,149 @@ +var ct_date = new Date(); + +function ctSetCookie(c_name, value) { + document.cookie = c_name + '=' + encodeURIComponent(value) + '; path=/'; +} + +ctSetCookie('ct_ps_timestamp', Math.floor(new Date().getTime()/1000)); +ctSetCookie('ct_fkp_timestamp', '0'); +ctSetCookie('ct_timezone', '0'); + +ct_attach_event_handler(window, 'DOMContentLoaded', ct_ready); + +setTimeout(function(){ + ctSetCookie('ct_timezone', ct_date.getTimezoneOffset()/60*(-1)); + ctSetCookie('ct_checkjs', ct_checkjs_val); +},1000); + +/* Writing first key press timestamp */ +var ctFunctionFirstKey = function output(event){ + var KeyTimestamp = Math.floor(new Date().getTime()/1000); + ctSetCookie('ct_fkp_timestamp', KeyTimestamp); + ctKeyStopStopListening(); +} + +/* Stop key listening function */ +function ctKeyStopStopListening(){ + if(typeof window.addEventListener == 'function'){ + window.removeEventListener('mousedown', ctFunctionFirstKey); + window.removeEventListener('keydown', ctFunctionFirstKey); + }else{ + window.detachEvent('mousedown', ctFunctionFirstKey); + window.detachEvent('keydown', ctFunctionFirstKey); + } +} + +if(typeof window.addEventListener == 'function'){ + window.addEventListener('mousedown', ctFunctionFirstKey); + window.addEventListener('keydown', ctFunctionFirstKey); +}else{ + window.attachEvent('mousedown', ctFunctionFirstKey); + window.attachEvent('keydown', ctFunctionFirstKey); +} +/* Ready function */ +function ct_ready(){ + ctSetCookie('ct_visible_fields', 0); + ctSetCookie('ct_visible_fields_count', 0); + setTimeout(function(){ + for(var i = 0; i < document.forms.length; i++){ + var form = document.forms[i]; + if (form.action.toString().indexOf('/auth/?forgot_password') !== -1) { + continue; + } + form.onsubmit_prev = form.onsubmit; + form.onsubmit = function(event){ + + /* Get only fields */ + var elements = []; + for(var key in this.elements){ + if(!isNaN(+key)) + elements[key] = this.elements[key]; + } + + /* Filter fields */ + elements = elements.filter(function(elem){ + + var pass = true; + + /* Filter fields */ + if( getComputedStyle(elem).display === 'none' || // hidden + getComputedStyle(elem).visibility === 'hidden' || // hidden + getComputedStyle(elem).opacity === '0' || // hidden + elem.getAttribute('type') === 'hidden' || // type == hidden + elem.getAttribute('type') === 'submit' || // type == submit + elem.value === '' || // empty value + elem.getAttribute('name') === null + ){ + return false; + } + + /* Filter elements with same names for type == radio */ + if(elem.getAttribute('type') === 'radio'){ + elements.forEach(function(el, j, els){ + if(elem.getAttribute('name') === el.getAttribute('name')){ + pass = false; + return; + } + }); + } + + return true; + }); + + /* Visible fields count */ + var visible_fields_count = elements.length; + + /* Visible fields */ + var visible_fields = ''; + elements.forEach(function(elem, i, elements){ + visible_fields += ' ' + elem.getAttribute('name'); + }); + visible_fields = visible_fields.trim(); + + ctSetCookie('ct_visible_fields', visible_fields); + ctSetCookie('ct_visible_fields_count', visible_fields_count); + + /* Call previous submit action */ + if(event.target.onsubmit_prev instanceof Function){ + setTimeout(function(){ + event.target.onsubmit_prev.call(event.target, event); + }, 500); + } + }; + } + }, 1000); +} + +function ct_attach_event_handler(elem, event, callback){ + if(typeof window.addEventListener === 'function') elem.addEventListener(event, callback); + else elem.attachEvent(event, callback); +} + +function ct_remove_event_handler(elem, event, callback){ + if(typeof window.removeEventListener === 'function') elem.removeEventListener(event, callback); + else elem.detachEvent(event, callback); +} + +if(typeof jQuery !== 'undefined') { + +/* Capturing responses and output block message for unknown AJAX forms */ +jQuery(document).ajaxComplete(function (event, xhr, settings) { + if (xhr.responseText && xhr.responseText.indexOf('\"apbct') !== -1) { + try { + var response = JSON.parse(xhr.responseText); + if (typeof response.apbct !== 'undefined') { + response = response.apbct; + if (response.blocked) { + alert(response.comment); + if(+response.stop_script == 1) + window.stop(); + } + } + } catch (e) { + return; + } + + } +}); + +} \ No newline at end of file diff --git a/cleantalk.antispam/default_option.php b/cleantalk.antispam/default_option.php index 1ef1f12..185d7ab 100644 --- a/cleantalk.antispam/default_option.php +++ b/cleantalk.antispam/default_option.php @@ -11,6 +11,7 @@ 'web_form' => 1, 'form_global_check' => 0, 'form_global_check_without_email' => 0, + 'form_external_ajax' => 0, 'bot_detector' => 1, 'form_sfw' => 1, 'form_sfw_uniq_get_option' => 1, diff --git a/cleantalk.antispam/include.php b/cleantalk.antispam/include.php index 1fa9c82..62a9ca9 100644 --- a/cleantalk.antispam/include.php +++ b/cleantalk.antispam/include.php @@ -8,6 +8,7 @@ //Antispam classes use Bitrix\Main\Page\Asset; +use Bitrix\Main\Context; use Cleantalk\Antispam\Cleantalk; use Cleantalk\Antispam\CleantalkRequest; use Cleantalk\Antispam\CleantalkResponse; @@ -19,6 +20,7 @@ use Cleantalk\ApbctBitrix\RemoteCalls; use Cleantalk\ApbctBitrix\Cron; use Cleantalk\ApbctBitrix\DB; +use Cleantalk\ApbctBitrix\CleantalkExternalFormsAjaxHandler; use Cleantalk\Common\Variables\Server; use Cleantalk\ApbctBitrix\SFW; @@ -1131,157 +1133,17 @@ static function FormAddon() { $ct_check_values = self::SetCheckJSValues(); - $js_template = ""; + $js_template = ""; return $js_template; } @@ -1333,6 +1195,7 @@ static function CheckAllBefore(&$arEntity, $bSendEmail = FALSE, $form_errors = n 'register', 'order', 'feedback_general_contact_form', + 'feedback_ajax', 'private_message', ); @@ -1534,6 +1397,19 @@ static function CheckAllBefore(&$arEntity, $bSendEmail = FALSE, $form_errors = n $request_params['post_info']['comment_type'] = 'private_message'; $ct_request = new CleantalkRequest($request_params); $ct_result = $ct->isAllowMessage($ct_request); + break; + + case 'feedback_ajax': + + $timelabels_key = 'mail_error_comment'; + if (is_array($arEntity['message'])) { + $arEntity['message'] = json_encode($arEntity['message']); + } + $request_params['message'] = $arEntity['message']; + $request_params['tz'] = isset($arEntity['user_timezone']) ? $arEntity['user_timezone'] : NULL; + $request_params['post_info']['comment_type'] = 'feedback_ajax'; + $ct_request = new CleantalkRequest($request_params); + $ct_result = $ct->isAllowMessage($ct_request); } $ret_val = array(); @@ -1982,7 +1858,7 @@ private static function ct_cookies_test() } - private static function apbct__filter_form_data($form_data) + public static function apbct__filter_form_data($form_data) { // It is a service field. Need to be deleted before the processing. if ( isset($form_data['apbct_visible_fields']) ) { @@ -2106,4 +1982,24 @@ private static function urlIsExclusion(){ } return false; } + + public static function onProlog() + { + if (self::siteIsExclusion() || self::urlIsExclusion()) { + return; + } + + $external_forms_enabled = COption::GetOptionString( 'cleantalk.antispam', 'form_external_ajax', '' ); + + if (empty($external_forms_enabled)) { + return; + } + + $request = Context::getCurrent()->getRequest(); + + // Проверяем, что это AJAX запрос + if ($request->isAjaxRequest()) { + CleantalkExternalFormsAjaxHandler::handleAjax($request); + } + } } diff --git a/cleantalk.antispam/install/index.php b/cleantalk.antispam/install/index.php index a0ccfc3..74c9c24 100644 --- a/cleantalk.antispam/install/index.php +++ b/cleantalk.antispam/install/index.php @@ -7,6 +7,7 @@ use Cleantalk\Common\API as CleantalkAPI; use Bitrix\Main\Config\Option; +use Bitrix\Main\EventManager; /** * Installer for CleanTalk module @@ -52,7 +53,7 @@ function __construct() { } $this->MODULE_NAME = GetMessage('CLEANTALK_MODULE_NAME'); $this->MODULE_DESCRIPTION = GetMessage('CLEANTALK_MODULE_DESCRIPTION'); - $this->PARTNER_NAME = "CleanTalk"; + $this->PARTNER_NAME = "CleanTalk"; $this->PARTNER_URI = "http://www.cleantalk.org"; // Values for all templates @@ -71,6 +72,30 @@ function __construct() { $this->template_messages = array(); } + public function InstallEvents() + { + $eventManager = EventManager::getInstance(); + $eventManager->registerEventHandler( + 'main', + 'OnProlog', + 'cleantalk.antispam', + 'CleantalkAntispam', + 'OnProlog' + ); + } + + public function UnInstallEvents() + { + $eventManager = EventManager::getInstance(); + $eventManager->unRegisterEventHandler( + 'main', + 'OnProlog', + 'cleantalk.antispam', + 'CleantalkAntispam', + 'OnProlog' + ); + } + function DoInstall() { global $DOCUMENT_ROOT, $APPLICATION; @@ -112,11 +137,11 @@ function DoInstall() { if (IsModuleInstalled('bitrix.eshop')) { RegisterModuleDependences('sale', 'OnBeforeOrderAdd', 'cleantalk.antispam', 'CleantalkAntispam', 'OnBeforeOrderAddHandler'); - } + } if (IsModuleInstalled('form')) { RegisterModuleDependences('form', 'OnBeforeResultAdd', 'cleantalk.antispam', 'CleantalkAntispam', 'OnBeforeResultAddHandler'); - } + } } //init default options if no options set @@ -143,7 +168,7 @@ function DoInstall() { //Checking API key if already set $api_key = COption::GetOptionString( 'cleantalk.antispam', 'key', ''); $form_sfw = COption::GetOptionInt( 'cleantalk.antispam', 'form_sfw', 0 ); //TODO For what is it? - + $result = CleantalkAPI::method__notice_paid_till($api_key, preg_replace('/http[s]?:\/\//', '', $_SERVER['HTTP_HOST'], 1)); COption::SetOptionInt( 'cleantalk.antispam', 'key_is_ok', isset($result['valid']) && $result['valid'] == '1' ? 1 : 0); @@ -155,7 +180,7 @@ function DoInstall() { } $GLOBALS["errors"] = $this->errors; $GLOBALS["messages"] = $this->messages; - $APPLICATION->IncludeAdminFile(GetMessage('CLEANTALK_INSTALL_TITLE'), $DOCUMENT_ROOT.'/bitrix/modules/cleantalk.antispam/install/step.php'); + $APPLICATION->IncludeAdminFile(GetMessage('CLEANTALK_INSTALL_TITLE'), $DOCUMENT_ROOT.'/bitrix/modules/cleantalk.antispam/install/step.php'); } function DoUninstall() { @@ -187,7 +212,7 @@ function DoUninstall() { UnRegisterModuleDependences('main', 'OnEndBufferContent', 'cleantalk.antispam', 'CleantalkAntispam', 'OnEndBufferContentHandler'); if (IsModuleInstalled('form')){ UnRegisterModuleDependences('form', 'OnBeforeResultAdd', 'cleantalk.antispam', 'CleantalkAntispam', 'OnBeforeResultAddHandler'); - } + } UnRegisterModule('cleantalk.antispam'); $this->UnInstallDB(); $this->UnInstallFiles(); @@ -203,7 +228,7 @@ function DoUninstall() { } function InstallFiles() { - + $results = $this->install_ct_template__in_dirs( $this->SAR_template_file, array( @@ -216,18 +241,18 @@ function InstallFiles() { $this->ct_template_addon_tag, $this->ct_template_addon_body ); - + foreach ($results as $dir => $result){ if(is_dir($dir) && $result != 0){ error_log('CLEANTALK_ERROR: INSTALLING_IN_TEMPLATE_FILES: ' . $dir . sprintf('%02d', $result )); } } - + return true; } function UnInstallFiles() { - + $results = $this->uninstall_ct_template__in_dirs( $this->SAR_template_file, array( @@ -248,9 +273,9 @@ function UnInstallFiles() { } function InstallDB() { - + global $DB; - + // Creating SFW DATA $result = $DB->Query( 'CREATE TABLE IF NOT EXISTS `cleantalk_sfw` ( @@ -266,7 +291,7 @@ function InstallDB() { $this->errors[] = GetMessage('CLEANTALK_ERROR_CREATE_SFW_DATA'); return FALSE; } - + // Creating SFW LOGS $result = $DB->Query( 'CREATE TABLE IF NOT EXISTS `cleantalk_sfw_logs` ( @@ -284,7 +309,7 @@ function InstallDB() { $this->errors[] = GetMessage('CLEANTALK_ERROR_CREATE_SFW_LOGS'); return FALSE; } - + // Creating TIMELABELS $result = $DB->Query( 'CREATE @@ -298,7 +323,7 @@ function InstallDB() { $this->errors[] = GetMessage('CLEANTALK_ERROR_CREATE_TIMELABELS'); return FALSE; } - + // Creating CIDS $result = $DB->Query( 'CREATE @@ -314,7 +339,7 @@ function InstallDB() { $this->errors[] = GetMessage('CLEANTALK_ERROR_CREATE_CIDS'); return FALSE; } - + // Creating SERVER $result = $DB->Query( 'CREATE @@ -329,7 +354,7 @@ function InstallDB() { $this->errors[] = GetMessage('CLEANTALK_ERROR_CREATE_SERVER'); return FALSE; } - + // Creating CHECKJS $result = $DB->Query( 'CREATE @@ -356,7 +381,7 @@ function UnInstallDB($arParams = Array()) { $DB->Query('DROP TABLE IF EXISTS cleantalk_checkjs'); return TRUE; } - + /** * Wrapper for cleantalk_antispam::install_ct_template__in_dir() * Allows to pass an array into it @@ -376,7 +401,7 @@ function install_ct_template__in_dirs($template_file, $template_dirs, $pattern, } return $out; } - + /** * Copies needed template from system dir to local dir and inserts CleanTalk addon into it * @@ -389,19 +414,19 @@ function install_ct_template__in_dirs($template_file, $template_dirs, $pattern, * @return int Returns error code or 0 when success */ function install_ct_template__in_dir( $template_file, $template_dir, $pattern, $ct_template_addon_tag, $ct_template_addon_body ){ - + // Check system folders if(!file_exists($template_dir)){ // No required system folders return 0; } $all_templates_folder = glob( $template_dir . '*' , GLOB_ONLYDIR); - + if (file_exists( $template_dir . '.default')) $all_templates_folder[] = $template_dir . '.default'; - + foreach ($all_templates_folder as $current_template){ - + // Exception for template mail templates // By type $description_file = $current_template . '/description.php'; @@ -414,26 +439,26 @@ function install_ct_template__in_dir( $template_file, $template_dir, $pattern, $ // Deleting mail template if( in_array( $current_template, array( 'mail_user' ) ) ) continue; - + $template_file_path = $current_template.'/'.$template_file; - + $start_pattern = ''; // don't change this! $end_pattern = ''; // don't change this! - + $result = $this->ct_file__clean_up( $template_file_path, $start_pattern, $end_pattern ); if( $result === true ){ - + // Check is it parsable $template_content = file_get_contents( $template_file_path ); if( $template_content ){ - + if( preg_match( $pattern, $template_content ) === 1 ){ - + $ct_template_addon = $start_pattern . $ct_template_addon_body . $end_pattern . "\n"; $template_content = preg_replace($pattern, $ct_template_addon . '${1}', $template_content, 1); - + if( file_put_contents( $template_file_path, $template_content ) ){ - + }else return 9; // Cannot write new content to template PHP file }else @@ -446,7 +471,7 @@ function install_ct_template__in_dir( $template_file, $template_dir, $pattern, $ // Here all is OK - new template PHP file with CLEANTALK addon inserted is ready return 0; } - + /** * Wrapper for cleantalk_antispam::install_ct_template__in_dir() * Allows to pass an array into it @@ -464,7 +489,7 @@ function uninstall_ct_template__in_dirs( $template_file, $template_dirs, $ct_tem } return $out; } - + /** * Remove addon from needed local component template * @@ -475,7 +500,7 @@ function uninstall_ct_template__in_dirs( $template_file, $template_dirs, $ct_tem * @return int Returns error code or 0 when success */ function uninstall_ct_template__in_dir( $template_file, $template_dir, $ct_template_addon_tag ){ - + // Check system folders if(!file_exists($template_dir)){ // No required system folders @@ -486,12 +511,12 @@ function uninstall_ct_template__in_dir( $template_file, $template_dir, $ct_templ if (file_exists( $template_dir . '.default')) $all_templates_folder[] = $template_dir . '.default'; foreach ($all_templates_folder as $current_template){ - + $template_file_path = $current_template.'/'.$template_file; - + $start_pattern = ''; // don't change this! $end_pattern = ''; // don't change this! - + $result = $this->ct_file__clean_up( $template_file_path, $start_pattern, $end_pattern ); if($result !== true ) return $result; @@ -500,7 +525,7 @@ function uninstall_ct_template__in_dir( $template_file, $template_dir, $ct_templ // Here all is OK - new template PHP file with CLEANTALK addon inserted is ready return 0; } - + /** * @param $file_path * @param $start_pattern @@ -509,31 +534,31 @@ function uninstall_ct_template__in_dir( $template_file, $template_dir, $ct_templ * @return bool|int */ function ct_file__clean_up( $file_path, $start_pattern, $end_pattern ){ - + // Last check - template PHP file if( is_file( $file_path ) || is_writable( $file_path ) ){ - + // Try to get template PHP file content $file_content = file_get_contents( $file_path ); - + if( $file_content ){ - + // Clean all CLEANTALK template addons $pos_begin = strpos( $file_content, $start_pattern ); $pos_end = strpos( $file_content, $end_pattern ); - + // Nothing to clean if($pos_begin === false && $pos_end === false) return true; - + if( $pos_begin !== false ){ if( $pos_end !== false ){ if( $pos_begin < $pos_end ){ - + // Cleaning up $file_content = substr( $file_content, 0, $pos_begin ) . substr( $file_content, $pos_end + strlen( $end_pattern ) ); // $file_content = substr( $file_content, 0, $pos_begin ) . substr( $file_content, $pos_end + strlen( '' ) ); - + if( file_put_contents( $file_path, $file_content ) ){ return true; }else @@ -549,5 +574,5 @@ function ct_file__clean_up( $file_path, $start_pattern, $end_pattern ){ }else return 4; // No template PHP file } - + } diff --git a/cleantalk.antispam/lang/en/options.php b/cleantalk.antispam/lang/en/options.php index 99a5314..b9ab79c 100644 --- a/cleantalk.antispam/lang/en/options.php +++ b/cleantalk.antispam/lang/en/options.php @@ -9,6 +9,8 @@ $MESS['CLEANTALK_LABEL_SEND_EXAMPLE'] = 'Send texts for off-top analysis'; $MESS['CLEANTALK_LABEL_ORDER'] = 'Order form protection'; $MESS['CLEANTALK_LABEL_WEB_FORMS'] = 'Web forms protection'; +$MESS['CLEANTALK_LABEL_FORM_EXTERNAL_AJAX'] = 'Protect forms with external AJAX handler'; +$MESS['CLEANTALK_DESCRIPTION_EXTERNAL_AJAX'] = 'At the moment only Bitrix24 external forms are supported'; $MESS['CLEANTALK_BUTTON_SAVE'] = 'Save'; $MESS['CLEANTALK_GET_AUTO_KEY'] = 'Get access key automatically'; $MESS['CLEANTALK_GET_MANUAL_KEY'] = 'Get access key manually'; @@ -56,4 +58,4 @@ $MESS['CLEANTALK_RESET_OPTIONS_FAILED'] = 'Can not reset options to defaults.'; $MESS['CLEANTALK_MULTISITE_LABEL_KEY'] = 'If you want to use specific Access Key for this website paste it here. Otherwise, leave it empty.'; $MESS['CLEANTALK_USE_CUSTOM_SERVER'] = 'Use custom server'; -$MESS['CLEANTALK_USE_CUSTOM_SERVER_DESCRIPTION'] = 'Use custom server for spam checking. You can enter cleantalk.ru and the plugin will use only ru servers. This option is for critical situations when it is impossible to choose the server automatically.'; \ No newline at end of file +$MESS['CLEANTALK_USE_CUSTOM_SERVER_DESCRIPTION'] = 'Use custom server for spam checking. You can enter cleantalk.ru and the plugin will use only ru servers. This option is for critical situations when it is impossible to choose the server automatically.'; diff --git a/cleantalk.antispam/lang/ru/options.php b/cleantalk.antispam/lang/ru/options.php index 47adc3e..e2f8610 100644 --- a/cleantalk.antispam/lang/ru/options.php +++ b/cleantalk.antispam/lang/ru/options.php @@ -9,6 +9,8 @@ $MESS['CLEANTALK_LABEL_SEND_EXAMPLE'] = 'Отсылать тексты для офф-топ анализа'; $MESS['CLEANTALK_LABEL_ORDER'] = 'Проверять формы заказов'; $MESS['CLEANTALK_LABEL_WEB_FORMS'] = 'Защита Веб-форм'; +$MESS['CLEANTALK_LABEL_FORM_EXTERNAL_AJAX'] = 'Защита внешних AJAX форм'; +$MESS['CLEANTALK_DESCRIPTION_EXTERNAL_AJAX'] = 'На данный момент поддерживаются только внешние формы для Битрикс24'; $MESS['CLEANTALK_BUTTON_SAVE'] = 'Сохранить'; $MESS['CLEANTALK_GET_AUTO_KEY'] = 'Получить ключ автоматически'; $MESS['CLEANTALK_GET_MANUAL_KEY'] = 'Получить ключ вручную'; @@ -57,4 +59,4 @@ $MESS['CLEANTALK_MULTISITE_LABEL_KEY'] = 'Если вы хотите использовать отдельный ключ доступа для этого сайта, вставьте его здесь. В противном случае оставьте поле пустым.'; $MESS['CLEANTALK_USE_CUSTOM_SERVER'] = 'Использовать кастомный сервер'; $MESS['CLEANTALK_USE_CUSTOM_SERVER_DESCRIPTION'] = 'Использовать кастомный сервер для проверки спама. Можете ввести cleantalk.ru и плагин будет использовать только ru сервера. Опция для критических ситуаций, когда невозможен выбор сервера автоматически.'; -$MESS['CLEANTALK_SERVER_NOT_AVAILABLE'] = 'Указанные сервера не доступны, обратитесь в тех поддержку https://cleantalk.org/my/support/open'; \ No newline at end of file +$MESS['CLEANTALK_SERVER_NOT_AVAILABLE'] = 'Указанные сервера не доступны, обратитесь в тех поддержку https://cleantalk.org/my/support/open'; diff --git a/cleantalk.antispam/lib/Cleantalk/Antispam/Cleantalk.php b/cleantalk.antispam/lib/Cleantalk/Antispam/Cleantalk.php index 5cff41f..e70baf0 100644 --- a/cleantalk.antispam/lib/Cleantalk/Antispam/Cleantalk.php +++ b/cleantalk.antispam/lib/Cleantalk/Antispam/Cleantalk.php @@ -11,7 +11,7 @@ * @author Cleantalk team (welcome@cleantalk.org) * @copyright (C) 2014 CleanTalk team (http://cleantalk.org) * @license GNU/GPL: http://www.gnu.org/copyleft/gpl.html - * @see https://github.com/CleanTalk/php-antispam + * @see https://github.com/CleanTalk/php-antispam * */ @@ -27,15 +27,15 @@ class Cleantalk { */ public $debug = 0; - + /** - * Data compression rate + * Data compression rate * @var int */ private $compressRate = 6; - + /** - * Server connection timeout in seconds + * Server connection timeout in seconds * @var int */ private $server_timeout = 15; @@ -75,27 +75,27 @@ class Cleantalk { * @var bool */ public $stay_on_server = false; - + /** - * Codepage of the data + * Codepage of the data * @var bool */ public $data_codepage = null; - + /** - * API version to use + * API version to use * @var string */ public $api_version = '/api2.0'; - + /** - * Use https connection to servers - * @var bool + * Use https connection to servers + * @var bool */ public $ssl_on = false; - + /** - * Path to SSL certificate + * Path to SSL certificate * @var string */ public $ssl_path = ''; @@ -160,7 +160,7 @@ private function filterRequest(CleantalkRequest $request) { $request->$param = NULL; } } - + if (in_array($param, array('js_on')) && !empty($value)) { if (!is_integer($value)) { $request->$param = NULL; @@ -217,33 +217,33 @@ private function createMsg($method, CleantalkRequest $request) { } break; } - + $request->method_name = $method; - + // // Removing non UTF8 characters from request, because non UTF8 or malformed characters break json_encode(). // foreach ($request as $param => $value) { if (!preg_match('//u', $value)) { - $request->{$param} = 'Nulled. Not UTF8 encoded or malformed.'; + $request->{$param} = 'Nulled. Not UTF8 encoded or malformed.'; } } - + return $request; } - + /** - * Send JSON request to servers + * Send JSON request to servers * @param $msg * @return boolean|\CleantalkResponse */ private function sendRequest($data = null, $url, $server_timeout = 15) { // Convert to array $data = (array)json_decode(json_encode($data), true); - + $original_url = $url; $original_data = $data; - + //Cleaning from 'null' values $tmp_data = array(); foreach($data as $key => $value){ @@ -253,19 +253,19 @@ private function sendRequest($data = null, $url, $server_timeout = 15) { } $data = $tmp_data; unset($key, $value, $tmp_data); - + // Convert to JSON $data = json_encode($data); - + if (isset($this->api_version)) { $url = $url . $this->api_version; } - + // Switching to secure connection if ($this->ssl_on && !preg_match("/^https:/", $url)) { $url = preg_replace("/^(http)/i", "$1s", $url); } - + $result = false; $curl_error = null; if(function_exists('curl_init')){ @@ -317,17 +317,17 @@ private function sendRequest($data = null, $url, $server_timeout = 15) { $result = @file_get_contents($url, false, $context); } } - + if (!$result || !cleantalk_is_JSON($result)) { $response = null; $response['errno'] = 1; $response['errstr'] = true; $response['curl_err'] = isset($curl_error) ? $curl_error : false; $response = json_decode(json_encode($response)); - + return $response; } - + $errstr = null; $response = json_decode($result); if ($result !== false && is_object($response)) { @@ -335,45 +335,45 @@ private function sendRequest($data = null, $url, $server_timeout = 15) { $response->errstr = $errstr; } else { $errstr = 'Unknown response from ' . $url . '.' . ' ' . $result; - + $response = null; $response['errno'] = 1; $response['errstr'] = $errstr; $response = json_decode(json_encode($response)); - } - - + } + + return $response; } /** - * httpRequest + * httpRequest * @param $msg * @return boolean|\CleantalkResponse */ private function httpRequest($msg) { $result = false; - + if($msg->method_name != 'send_feedback'){ $ct_tmp = apache_request_headers(); - + if(isset($ct_tmp['Cookie'])) $cookie_name = 'Cookie'; elseif(isset($ct_tmp['cookie'])) $cookie_name = 'cookie'; else $cookie_name = 'COOKIE'; - + if(isset($tmp[$cookie_name])) $ct_tmp[$cookie_name] = preg_replace(array( '/\s{0,1}ct_checkjs=[a-z0-9]*[;|$]{0,1}/', '/\s{0,1}ct_timezone=.{0,1}\d{1,2}[;|$]/', '/;{0,1}\s{0,3}$/' ), '', $ct_tmp[$cookie_name]); - + $msg->all_headers=json_encode($ct_tmp); } - + //$msg->remote_addr=$_SERVER['REMOTE_ADDR']; //$msg->sender_info['remote_addr']=$_SERVER['REMOTE_ADDR']; $si=(array)json_decode($msg->sender_info,true); @@ -381,13 +381,13 @@ private function httpRequest($msg) { $si['remote_addr']=$_SERVER['REMOTE_ADDR']; $msg->x_forwarded_for=@$_SERVER['X_FORWARDED_FOR']; $msg->x_real_ip=@$_SERVER['X_REAL_IP']; - + $msg->sender_info=json_encode($si); if (((isset($this->work_url) && $this->work_url !== '') && ($this->server_changed + $this->server_ttl > time())) || $this->stay_on_server == true) { - + $url = (!empty($this->work_url)) ? $this->work_url : $this->server_url; - + $result = $this->sendRequest($msg, $url, $this->server_timeout); } @@ -401,11 +401,11 @@ private function httpRequest($msg) { $pool = null; if (isset($matches[2])) $pool = $matches[2]; - + $url_suffix = ''; if (isset($matches[3])) $url_suffix = $matches[3]; - + if ($url_prefix === '') $url_prefix = 'http://'; @@ -421,13 +421,13 @@ private function httpRequest($msg) { $work_url = $server_host; } $host = filter_var($work_url,FILTER_VALIDATE_IP) ? gethostbyaddr($work_url) : $work_url; - $work_url = $url_prefix . $host; - if (isset($url_suffix)) + $work_url = $url_prefix . $host; + if (isset($url_suffix)) $work_url = $work_url . $url_suffix; - + $this->work_url = $work_url; $this->server_ttl = $server['ttl']; - + $result = $this->sendRequest($msg, $this->work_url, $this->server_timeout); if ($result !== false && $result->errno === 0) { @@ -437,9 +437,9 @@ private function httpRequest($msg) { } } } - + $response = new CleantalkResponse(null, $result); - + if (!empty($this->data_codepage) && $this->data_codepage !== 'UTF-8') { if (!empty($response->comment)) $response->comment = $this->stringFromUTF8($response->comment, $this->data_codepage); @@ -448,10 +448,10 @@ private function httpRequest($msg) { if (!empty($response->sms_error_text)) $response->sms_error_text = $this->stringFromUTF8($response->sms_error_text, $this->data_codepage); } - + return $response; } - + /** * Function DNS request * @param $host @@ -471,6 +471,11 @@ public function get_servers_ip($host) { } } } + + if (is_null($response)) { + $response = array(); + } + if (count($response) == 0 && function_exists('gethostbynamel')) { $records = gethostbynamel($host); @@ -495,21 +500,21 @@ public function get_servers_ip($host) { $r_temp = null; $fast_server_found = false; foreach ($response as $server) { - + // Do not test servers because fast work server found if ($fast_server_found) { - $ping = $this->min_server_timeout; + $ping = $this->min_server_timeout; } else { $ping = $this->httpPing($server['ip']); $ping = $ping * 1000; } - + // -1 server is down, skips not reachable server if ($ping != -1) { $r_temp[$ping + $i] = $server; } $i++; - + if ($ping < $this->min_server_timeout) { $fast_server_found = true; } @@ -561,7 +566,7 @@ public function delCleantalkComment($message) { $message = preg_replace('/\*\*\*.+\*\*\*$/', '', $message); $message = preg_replace('/\[\n]{0,1}\[\n]{0,1}\*\*\*.+\*\*\*$/', '', $message); - + return $message; } @@ -573,7 +578,7 @@ public function ct_session_ip( $data_ip ) { return $data_ip; } /*if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { - + $forwarded_ip = explode(",", $_SERVER['HTTP_X_FORWARDED_FOR']); // Looking for first value in the list, it should be sender real IP address @@ -595,14 +600,14 @@ public function ct_session_ip( $data_ip ) { if ($private_src_ip) { continue; } - + if ($this->net_match($v, $data_ip)) { $private_src_ip = true; } } if ($private_src_ip) { - // Taking first IP from the list HTTP_X_FORWARDED_FOR - $data_ip = $forwarded_ip[0]; + // Taking first IP from the list HTTP_X_FORWARDED_FOR + $data_ip = $forwarded_ip[0]; } } @@ -613,11 +618,11 @@ public function ct_session_ip( $data_ip ) { /** * From http://php.net/manual/en/function.ip2long.php#82397 */ - public function net_match($CIDR,$IP) { - list ($net, $mask) = explode ('/', $CIDR); - return ( ip2long ($IP) & ~((1 << (32 - $mask)) - 1) ) == ip2long ($net); - } - + public function net_match($CIDR,$IP) { + list ($net, $mask) = explode ('/', $CIDR); + return ( ip2long ($IP) & ~((1 << (32 - $mask)) - 1) ) == ip2long ($net); + } + /** * Function to check response time * param string @@ -626,7 +631,7 @@ public function net_match($CIDR,$IP) { function httpPing($host){ // Skip localhost ping cause it raise error at fsockopen. - // And return minimun value + // And return minimun value if ($host == 'localhost') return 0.001; @@ -641,19 +646,19 @@ function httpPing($host){ $status = ($stoptime - $starttime); $status = round($status, 4); } - + return $status; } - + /** - * Function convert string to UTF8 and removes non UTF8 characters + * Function convert string to UTF8 and removes non UTF8 characters * param string * param string * @return string */ function stringToUTF8($str, $data_codepage = null){ if (!preg_match('//u', $str) && function_exists('mb_detect_encoding') && function_exists('mb_convert_encoding')) { - + if ($data_codepage !== null) return mb_convert_encoding($str, 'UTF-8', $data_codepage); @@ -661,12 +666,12 @@ function stringToUTF8($str, $data_codepage = null){ if ($encoding) return mb_convert_encoding($str, 'UTF-8', $encoding); } - + return $str; } - + /** - * Function convert string from UTF8 + * Function convert string from UTF8 * param string * param string * @return string @@ -675,19 +680,19 @@ function stringFromUTF8($str, $data_codepage = null){ if (preg_match('//u', $str) && function_exists('mb_convert_encoding') && $data_codepage !== null) { return mb_convert_encoding($str, $data_codepage, 'UTF-8'); } - + return $str; } - + /** - * Function gets information about spam active networks + * Function gets information about spam active networks * * @param string api_key - * @return JSON/array + * @return JSON/array */ public function get_2s_blacklists_db ($api_key) { $request=array(); - $request['method_name'] = '2s_blacklists_db'; + $request['method_name'] = '2s_blacklists_db'; $request['auth_key'] = $api_key; $url='https://api.cleantalk.org'; $result=CleantalkHelper::sendRawRequest($url,$request); @@ -755,7 +760,7 @@ function cleantalk_is_JSON($string) } // Patch for locale_get_display_region() for old PHP versions -if( !function_exists('locale_get_display_region') ){ +if( !function_exists('locale_get_display_region') ){ function locale_get_display_region($locale, $curr_lcoale='en'){ return $locale; } diff --git a/cleantalk.antispam/lib/Cleantalk/ApbctBitrix/CleantalkExternalFormsAjaxHandler.php b/cleantalk.antispam/lib/Cleantalk/ApbctBitrix/CleantalkExternalFormsAjaxHandler.php new file mode 100644 index 0000000..87f5cda --- /dev/null +++ b/cleantalk.antispam/lib/Cleantalk/ApbctBitrix/CleantalkExternalFormsAjaxHandler.php @@ -0,0 +1,107 @@ +getPostList()->toArray(); + + if ( + empty($formData) || + !isset($formData['action']) || + $formData['action'] + !== 'cleantalk_force_ajax_check' + ) { + return; + } + // Remove the action parameter + unset($formData['action']); + + // Sanitize and filter form data + $formData = CleantalkAntispam::apbct__filter_form_data($formData); + + // Prepare user data for checking + $arUser = array(); + $arUser["type"] = "feedback_ajax"; + $arUser["sender_email"] = $formData['email'] ?? ''; + $arUser["sender_nickname"] = $formData['name'] ?? $formData['nickname'] ?? ''; + $arUser["subject"] = $formData['subject'] ?? ''; + $arUser["message"] = array_filter($formData, function($key) { + return !in_array($key, ['email', 'name', 'nickname', 'subject', 'action', 'ct_bot_detector_event_token']); + }, ARRAY_FILTER_USE_KEY); + + // Check for spam + $aResult = CleantalkAntispam::CheckAllBefore($arUser, false); + + if (isset($aResult) && is_array($aResult)) { + if ($aResult['errno'] == 0) { + if ($aResult['allow'] == 1) { + // Not spam - allow + self::showSuccess([ + 'apbct' => [ + 'blocked' => false, + 'comment' => '' + ], + 'success' => true + ]); + } else { + // Spam detected - block + self::showSuccess([ + 'apbct' => [ + 'blocked' => true, + 'comment' => $aResult['ct_result_comment'] + ], + 'error' => [ + 'msg' => $aResult['ct_result_comment'] + ] + ]); + } + } else { + // Error occurred + self::showSuccess([ + 'apbct' => [ + 'blocked' => false, + 'comment' => '' + ], + 'error' => [ + 'msg' => $aResult['errstr'] ?? 'Unknown error' + ] + ]); + } + } else { + // No result - allow by default + self::showSuccess([ + 'apbct' => [ + 'blocked' => false, + 'comment' => '' + ], + 'success' => true + ]); + } + } + + private static function showSuccess($data) + { + header('Content-Type: application/json'); + echo Json::encode($data); + die(); + } + + private static function showError($message) + { + header('Content-Type: application/json'); + echo Json::encode(['error' => $message]); + die(); + } +} diff --git a/cleantalk.antispam/options.php b/cleantalk.antispam/options.php index e221eb5..b95a831 100644 --- a/cleantalk.antispam/options.php +++ b/cleantalk.antispam/options.php @@ -138,6 +138,7 @@ Option::set( $sModuleId, 'form_comment_treelike', $_POST['form_comment_treelike'] == '1' ? 1 : 0 ); Option::set( $sModuleId, 'form_send_example', $_POST['form_send_example'] == '1' ? 1 : 0 ); Option::set( $sModuleId, 'form_order', $_POST['form_order'] == '1' ? 1 : 0 ); + Option::set( $sModuleId, 'form_external_ajax', $_POST['form_external_ajax'] == '1' ? 1 : 0 ); Option::set( $sModuleId, 'web_form', $_POST['web_form'] == '1' ? 1 : 0 ); Option::set( $sModuleId, 'is_paid', $_POST['is_paid'] == '1' ? 1 : 0 ); Option::set( $sModuleId, 'last_checked', $_POST['last_checked'] == '1' ? 1 : 0 ); @@ -554,6 +555,18 @@ function ctDisableInputLine(ct_input_line){ checked="checked"value="1" /> + + +