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" />
+
+ |
+
|
|