diff --git a/cleantalk.antispam/ajax_handler.php b/cleantalk.antispam/ajax_handler.php new file mode 100644 index 0000000..a7cf7a7 --- /dev/null +++ b/cleantalk.antispam/ajax_handler.php @@ -0,0 +1,12 @@ + 'Module not loaded']); + exit; +} + +$fields = $_POST; +$response = \Cleantalk\Integrations\IntegrationFactory::handle($fields); +echo json_encode($response); 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 5b7b134..b1e04f1 100644 --- a/cleantalk.antispam/include.php +++ b/cleantalk.antispam/include.php @@ -34,6 +34,14 @@ define('APBCT_SELECT_LIMIT', 5000); // Select limit for logs. define('APBCT_WRITE_LIMIT', 5000); // Write limit for firewall data. +// Register IntegrationFactory for autoload +\Bitrix\Main\Loader::registerAutoLoadClasses( + 'cleantalk.antispam', + [ + 'Cleantalk\\Antispam\\Integrations\\IntegrationFactory' => 'lib/Cleantalk/Integrations/IntegrationFactory.php', + ] +); + /** * CleanTalk module class @@ -256,6 +264,7 @@ public static function OnPageStartHandler() $last_checked = COption::GetOptionInt( 'cleantalk.antispam', 'last_checked', 0 ); $show_review = COption::GetOptionInt( 'cleantalk.antispam', 'show_review', 0 ); $bot_detector = COption::GetOptionInt( 'cleantalk.antispam', 'bot_detector', 0 ); + $form_external_ajax = COption::GetOptionInt( 'cleantalk.antispam', 'form_external_ajax', 0 ); $is_sfw = COption::GetOptionInt( 'cleantalk.antispam', 'form_sfw', 0 ); $sfw_last_update = COption::GetOptionInt( 'cleantalk.antispam', 'sfw_last_update', 0); $sfw_last_send_log = COption::GetOptionInt( 'cleantalk.antispam', 'sfw_last_send_log', 0); @@ -273,6 +282,11 @@ public static function OnPageStartHandler() if( ! $USER->IsAdmin() ){ + // JS External protection + if ($form_external_ajax) { + Asset::getInstance()->addJs('/bitrix/js/cleantalk.antispam/cleantalk-antispam-external-protection.js'); + } + if ( $bot_detector ) { if (class_exists('COption')) { $use_custom_server = \COption::GetOptionString( 'cleantalk.antispam', 'use_custom_server', '' ); diff --git a/cleantalk.antispam/install/index.php b/cleantalk.antispam/install/index.php index a0ccfc3..ebcd0a0 100644 --- a/cleantalk.antispam/install/index.php +++ b/cleantalk.antispam/install/index.php @@ -203,50 +203,97 @@ function DoUninstall() { } function InstallFiles() { - - $results = $this->install_ct_template__in_dirs( - $this->SAR_template_file, - array( - $this->SAR_bitrix_template_dir, - $this->SAR_local_template_dir, - /** @todo (.../bitrix/templates/.default/components/bitrix/system.auth.registration/) research it */ - // Copy system.auth.registration default template from system dir to local dir and insert addon into - ), - $this->SAR_pattern, - $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 )); - } - } + // Copy JS to public Bitrix directory + global $DOCUMENT_ROOT; + $publicJsDir = $DOCUMENT_ROOT . '/bitrix/js/cleantalk.antispam'; + if (!is_dir($publicJsDir)) { + mkdir($publicJsDir, 0755, true); + } + $srcJs = dirname(__FILE__, 2) . '/js/cleantalk-antispam-external-protection.js'; + $dstJs = $publicJsDir . '/cleantalk-antispam-external-protection.js'; + if (file_exists($srcJs)) { + copy($srcJs, $dstJs); + } + + // Copy ajax_handler.php to /bitrix/tools/cleantalk.antispam/ + $toolsDir = $DOCUMENT_ROOT . '/bitrix/tools/cleantalk.antispam'; + if (!is_dir($toolsDir)) { + mkdir($toolsDir, 0755, true); + } + $srcAjax = dirname(__FILE__, 2) . '/ajax_handler.php'; + $dstAjax = $toolsDir . '/ajax_handler.php'; + if (file_exists($srcAjax)) { + copy($srcAjax, $dstAjax); + } + + $results = $this->install_ct_template__in_dirs( + $this->SAR_template_file, + array( + $this->SAR_bitrix_template_dir, + $this->SAR_local_template_dir, + /** @todo (.../bitrix/templates/.default/components/bitrix/system.auth.registration/) research it */ + // Copy system.auth.registration default template from system dir to local dir and insert addon into + ), + $this->SAR_pattern, + $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( - $this->SAR_bitrix_template_dir, - $this->SAR_local_template_dir, - /** @todo (.../bitrix/templates/.default/components/bitrix/system.auth.registration/) research it */ - // Copy system.auth.registration default template from system dir to local dir and insert addon into - ), - $this->ct_template_addon_tag - ); - foreach ($results as $dir => $result){ - if(is_dir($dir) && $result != 0){ - error_log('CLEANTALK_ERROR: UNINSTALLING_IN_TEMPLATE_FILES: ' . $dir . sprintf('%02d', $result )); - } - } + $results = $this->uninstall_ct_template__in_dirs( + $this->SAR_template_file, + array( + $this->SAR_bitrix_template_dir, + $this->SAR_local_template_dir, + /** @todo (.../bitrix/templates/.default/components/bitrix/system.auth.registration/) research it */ + // Copy system.auth.registration default template from system dir to local dir and insert addon into + ), + $this->ct_template_addon_tag + ); + foreach ($results as $dir => $result){ + if(is_dir($dir) && $result != 0){ + error_log('CLEANTALK_ERROR: UNINSTALLING_IN_TEMPLATE_FILES: ' . $dir . sprintf('%02d', $result )); + } + } - return true; + // Remove JS directory + global $DOCUMENT_ROOT; + $publicJsDir = $DOCUMENT_ROOT . '/bitrix/js/cleantalk.antispam'; + if (is_dir($publicJsDir)) { + $this->removeDirWithFiles($publicJsDir); + } + + // Remove ajax_handler.php directory + $toolsDir = $DOCUMENT_ROOT . '/bitrix/tools/cleantalk.antispam'; + if (is_dir($toolsDir)) { + $this->removeDirWithFiles($toolsDir); + } + + return true; } + function removeDirWithFiles($dir) { + if (is_dir($dir)) { + $files = glob($dir . '/*'); + foreach ($files as $file) { + if (is_file($file)) { + @unlink($file); + } + } + @rmdir($dir); + } + } + function InstallDB() { global $DB; diff --git a/cleantalk.antispam/js/cleantalk-antispam-external-protection.js b/cleantalk.antispam/js/cleantalk-antispam-external-protection.js new file mode 100644 index 0000000..dde3ef3 --- /dev/null +++ b/cleantalk.antispam/js/cleantalk-antispam-external-protection.js @@ -0,0 +1,175 @@ +/** + * Returns FormData with cleantalk_check and ct_bot_detector_event_token + * @param {HTMLFormElement} form + * @returns {FormData} + */ +function getFormDataWithToken(form) { + const formData = new FormData(form); + formData.append('cleantalk_check', '1'); + let ctToken = ''; + const ctTokenInput = form.querySelector('[name="ct_bot_detector_event_token"]'); + if (ctTokenInput && ctTokenInput.value) { + ctToken = ctTokenInput.value; + } else { + try { + const lsToken = localStorage.getItem('bot_detector_event_token'); + if (lsToken) { + const parsed = JSON.parse(lsToken); + if (parsed && parsed.value) { + ctToken = parsed.value; + } + } + } catch (e) {} + } + formData.set('ct_bot_detector_event_token', ctToken); + return formData; +} + +/** + * Shows a full-page blocking message + * @param {string} msg + */ +function cleantalkShowForbiddenMessage(msg) { + let blockDiv = document.getElementById('cleantalk-forbidden-block'); + if (!blockDiv) { + blockDiv = document.createElement('div'); + blockDiv.id = 'cleantalk-forbidden-block'; + blockDiv.style.position = 'fixed'; + blockDiv.style.top = '0'; + blockDiv.style.left = '0'; + blockDiv.style.width = '100vw'; + blockDiv.style.height = '100vh'; + blockDiv.style.background = 'rgba(0,0,0,0.7)'; + blockDiv.style.zIndex = '99999'; + blockDiv.style.display = 'flex'; + blockDiv.style.alignItems = 'center'; + blockDiv.style.justifyContent = 'center'; + blockDiv.style.color = '#fff'; + blockDiv.style.fontSize = '2em'; + blockDiv.innerHTML = '
' + msg + '

'; + document.body.appendChild(blockDiv); + document.getElementById('cleantalk-forbidden-close').onclick = function() { + blockDiv.remove(); + location.reload(); + }; + } +} + +/** + * Overrides fetch to intercept target form submissions + */ +(function() { + // Save original fetch + const defaultFetch = window.fetch; + let fetchOverridden = false; + + // Form selectors for different integrations to identify target forms + const FORM_SELECTORS = { + 'Bitrix24Integration': '.b24-form-content form', + }; + + // List of URL patterns for fetch interception + const FETCH_URL_PATTERNS = [ + '/bitrix/' + ]; + + /** + * Checks if fetch call is for target forms + * @param {Array} args + * @returns {boolean} + */ + function isTargetFormFetch(args) { + if (!args || !args[0]) return false; + const url = typeof args[0] === 'string' ? args[0] : (args[0].url || ''); + return FETCH_URL_PATTERNS.some(pattern => url.includes(pattern)); + } + + /** + * Gets the first found form and its integration class + * @returns {{form: HTMLFormElement, integration: string}|null} + */ + function getTargetFormAndIntegration() { + for (const integration in FORM_SELECTORS) { + const selector = FORM_SELECTORS[integration]; + const form = document.querySelector(selector); + if (form) return { form, integration }; + } + return null; + } + + /** + * Overrides fetch for target forms + * @returns {void} + */ + function overrideFetchForTargetForms() { + if (fetchOverridden) return; + fetchOverridden = true; + window.fetch = async function(...args) { + if (isTargetFormFetch(args)) { + const found = getTargetFormAndIntegration(); + if (found && found.form && found.integration) { + const formData = getFormDataWithToken(found.form); + formData.append('integration', found.integration); + let blocked = false; + let blockMsg = ''; + try { + const checkResult = await defaultFetch('/bitrix/tools/cleantalk.antispam/ajax_handler.php', { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + const json = await checkResult.json(); + if (json && json.apbct && +json.apbct.blocked) { + blocked = true; + blockMsg = json.apbct.comment || 'Your submission has been blocked as Cleantalk antispam.'; + } + } catch (err) { + blocked = true; + blockMsg = 'Error during spam check. Please try again later.'; + } + if (blocked) { + cleantalkShowForbiddenMessage(blockMsg); + return new Promise(() => {}); + } + } + } + return defaultFetch.apply(window, args); + }; + } + + // Override fetch only after the page is fully loaded + if (document.readyState === 'complete' || document.readyState === 'interactive') { + overrideFetchForTargetForms(); + } else { + document.addEventListener('DOMContentLoaded', overrideFetchForTargetForms); + } + + // MutationObserver for dynamically added forms + const observer = new MutationObserver((mutationsList) => { + for (const mutation of mutationsList) { + if (mutation.type === 'childList') { + for (const node of mutation.addedNodes) { + if (node.nodeType === 1) { + for (const integration in FORM_SELECTORS) { + const selector = FORM_SELECTORS[integration]; + if ( + (node.matches && node.matches(selector)) || + (node.querySelector && node.querySelector(selector)) + ) { + overrideFetchForTargetForms(); + break; + } + } + } + } + } + } + }); + if (document.body) { + observer.observe(document.body, { childList: true, subtree: true }); + } else { + document.addEventListener('DOMContentLoaded', function() { + observer.observe(document.body, { childList: true, subtree: true }); + }); + } +})(); diff --git a/cleantalk.antispam/lang/en/options.php b/cleantalk.antispam/lang/en/options.php index 99a5314..59e112b 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'; @@ -19,7 +21,7 @@ $MESS['CLEANTALK_LABEL_GLOBAL_CHECK_WITHOUT_EMAIL'] = 'Check all POST data'; $MESS['CLEANTALK_WARNING_GLOBAL_CHECK_WITHOUT_EMAIL'] = '- Warning, conflict possibility!'; $MESS['CLEANTALK_LABEL_BOT_DETECTOR'] = 'Use Anti-Spam by CleanTalk JavaScript library'; -$MESS['CLEANTALK_DESCRIPTION_BOT_DETECTOR'] = 'This option includes external Anti-Spam by CleanTalk JavaScript library to getting visitors info data'; +$MESS['CLEANTALK_DESCRIPTION_BOT_DETECTOR'] = '- This option includes external Anti-Spam by CleanTalk JavaScript library to getting visitors info data'; $MESS['CLEANTALK_LABEL_SFW'] = 'Spam FireWall'; $MESS['CLEANTALK_LABEL_UNIQ_GET_OPTION'] = 'Uniq GET option'; $MESS['CLEANTALK_LABEL_UNIQ_GET_OPTION_DESC'] = 'If a visitor gets the SpamFireWall page, the plugin will put a unique GET variable in the URL to avoid issues with caching plugins.'; diff --git a/cleantalk.antispam/lang/ru/options.php b/cleantalk.antispam/lang/ru/options.php index 47adc3e..aeb774f 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'] = 'Получить ключ вручную'; @@ -19,7 +21,7 @@ $MESS['CLEANTALK_LABEL_GLOBAL_CHECK_WITHOUT_EMAIL'] = 'Проверять все POST данные'; $MESS['CLEANTALK_WARNING_GLOBAL_CHECK_WITHOUT_EMAIL'] = '- Внимание, возможны конфликты!'; $MESS['CLEANTALK_LABEL_BOT_DETECTOR'] = 'Использовать Anti-Spam by CleanTalk JavaScript библиотеку'; -$MESS['CLEANTALK_DESCRIPTION_BOT_DETECTOR'] = 'Эта опция включает внешнюю Anti-Spam by CleanTalk JavaScript библиотеку для получения данных о посетителях'; +$MESS['CLEANTALK_DESCRIPTION_BOT_DETECTOR'] = '- Эта опция включает внешнюю Anti-Spam by CleanTalk JavaScript библиотеку для получения данных о посетителях'; $MESS['CLEANTALK_LABEL_SFW'] = 'Spam FireWall'; $MESS['CLEANTALK_LABEL_UNIQ_GET_OPTION'] = 'Добавление уникального гет параметра'; $MESS['CLEANTALK_LABEL_UNIQ_GET_OPTION_DESC'] = 'Если посетитель попадает на страницу файервола, плагин помещает уникальный GET параметр в URL-адрес, чтобы избежать проблем с кэшированием.'; diff --git a/cleantalk.antispam/lib/Cleantalk/Integrations/Bitrix24Integration.php b/cleantalk.antispam/lib/Cleantalk/Integrations/Bitrix24Integration.php new file mode 100644 index 0000000..c4da1de --- /dev/null +++ b/cleantalk.antispam/lib/Cleantalk/Integrations/Bitrix24Integration.php @@ -0,0 +1,19 @@ + [ + 'blocked' => isset($result['allow']) && $result['allow'] == 0 ? 1 : 0, + 'comment' => $result['ct_result_comment'] ?? ($result['comment'] ?? ''), + ] + ]; + } +} diff --git a/cleantalk.antispam/lib/Cleantalk/Integrations/IntegrationFactory.php b/cleantalk.antispam/lib/Cleantalk/Integrations/IntegrationFactory.php new file mode 100644 index 0000000..57059b2 --- /dev/null +++ b/cleantalk.antispam/lib/Cleantalk/Integrations/IntegrationFactory.php @@ -0,0 +1,34 @@ + 'No integration specified']; + } + if (!in_array($integrationName, self::$integrations, true)) { + return ['error' => 'Integration not supported']; + } + $class = __NAMESPACE__ . "\\$integrationName"; + if (!class_exists($class)) { + return ['error' => 'Integration class not found']; + } + $integration = new $class(); + if (!method_exists($integration, 'process')) { + return ['error' => 'Integration missing process() method']; + } + return $integration->process($fields); + } +} diff --git a/cleantalk.antispam/options.php b/cleantalk.antispam/options.php index e221eb5..f1a0c0d 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 ); @@ -547,6 +548,15 @@ function ctDisableInputLine(ct_input_line){ checked="checked"value="1" /> + + + + + + checked="checked"value="1" /> + + +