From e7d40c45f78d41678f7f26579f3e1fc9f5b22e34 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Fri, 24 Apr 2026 15:37:32 +0300 Subject: [PATCH 01/13] Upd. Alt cookies. Alt cookies logic refactored. --- cleantalkantispam.php | 128 +++---------------- lib/Cleantalk/Custom/AltCookies.php | 184 ++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 114 deletions(-) create mode 100644 lib/Cleantalk/Custom/AltCookies.php diff --git a/cleantalkantispam.php b/cleantalkantispam.php index e89c499..f8027ca 100644 --- a/cleantalkantispam.php +++ b/cleantalkantispam.php @@ -34,10 +34,6 @@ jimport('joomla.application.web'); jimport('joomla.application.component.helper'); -// Sessions -define('APBCT_SESSION__LIVE_TIME', 86400*2); -define('APBCT_SESSION__CHANCE_TO_CLEAN', 100); - // Autoload require_once(dirname(__FILE__) . '/lib/autoload.php'); @@ -50,6 +46,7 @@ use Cleantalk\Common\Variables\Cookie; use Cleantalk\Common\Variables\Server; +use Cleantalk\Custom\AltCookies; use Cleantalk\Custom\ConnectionReports; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -1577,7 +1574,7 @@ public function onJCommentsCommentBeforeAdd(&$comment) /** * The spot to handle all ajax request for the plugin * - * @return string[]|void + * @return string * * @throws Exception * @since version @@ -1612,37 +1609,9 @@ public function onAjaxCleantalkantispam() { $users_checker = new \Cleantalk\Custom\FindSpam\UsersChecker\UsersChecker($data); return $users_checker->getResponse(); case 'set_alt_cookies' : - self::_apbct_alt_sessions__remove_old(); - - // To database - $db = JFactory::getDbo(); - $columns = array( - 'id', - 'name', - 'value', - 'last_update' - ); - $values = array(); - $query = $db->getQuery(true); - $query->insert($db->quoteName('#__cleantalk_sessions')); - $query->columns($db->quoteName($columns)); - unset($data['action']); - - foreach ($data as $cookie_name => $cookie_value) { - $values[] = implode(',', array( - $db->quote(self::_apbct_alt_session__id__get()), - $db->quote($cookie_name), - $db->quote($cookie_value), - $db->quote(date('Y-m-d H:i:s')) - )); - } - - $query->values($values); - - $db->setQuery($query . ' ON DUPLICATE KEY UPDATE value=VALUES(value), last_update=VALUES(last_update);'); - $db->execute(); - - return ('XHR OK'); + unset($data['action']); + AltCookies::removeOld(); + return AltCookies::setFromRemote($data); case 'check_ajax': $ctResponse = $this->ctSendRequest('check_newuser', array()); @@ -2325,96 +2294,27 @@ private function ct_cookies_test() } } - private function ct_setcookie( $name, $value ) + private function ct_setcookie($name, $value) { if( $this->params->get('ct_use_alternative_cookies') || $this->params->get('ct_set_cookies') == 2 ) { - - self::_apbct_alt_sessions__remove_old(); - + AltCookies::removeOld(); // To database - $db = JFactory::getDbo(); - $query = $db->getQuery(true); - - $columns = array('id', 'name', 'value', 'last_update'); - $values = array($db->quote(self::_apbct_alt_session__id__get()), $db->quote($name), $db->quote($value), $db->quote(date('Y-m-d H:i:s'))); - $query - ->insert($db->quoteName('#__cleantalk_sessions')) - ->columns($db->quoteName($columns)) - ->values(implode(',', $values));$db->setQuery($query . ' ON DUPLICATE KEY UPDATE ' . $db->quoteName('value') . ' = '.$db->quote($value).', ' . $db->quoteName('last_update') . ' = ' . $db->quote(date('Y-m-d H:i:s'))); - $db->execute(); - - } else { + AltCookies::set($name, $value); + } else { // To cookies Cookie::set($name, $value); } } - private function ct_getcookie( $name ) + private function ct_getcookie($name) { if ( $this->params->get('ct_use_alternative_cookies') || $this->params->get('ct_set_cookies') == 2 ) { - - // From database - $db = JFactory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName(array('value'))); - $query->from($db->quoteName('#__cleantalk_sessions')); - $query->where($db->quoteName('id') . ' = '. $db->quote(self::_apbct_alt_session__id__get())); - $query->where($db->quoteName('name') . ' = '. $db->quote($name)); - $db->setQuery($query); - $value = $db->loadResult(); - - if ( ! is_null($value) ) { - return $value; - } else { - return null; - } - - } else { - - // From cookies - if (isset($_COOKIE[$name])) { - return $_COOKIE[$name]; - } else { - return null; - } - - } - } - - /** - * Clean 'cleantalk_sessions' table - */ - static private function _apbct_alt_sessions__remove_old() - { - if (rand(0, 1000) < APBCT_SESSION__CHANCE_TO_CLEAN) { - - $db = JFactory::getDbo(); - $query = $db->getQuery(true); - - $query->delete($db->quoteName('#__cleantalk_sessions')); - $query->where($db->quoteName('last_update') . ' < NOW() - INTERVAL '. APBCT_SESSION__LIVE_TIME .' SECOND'); - - $db->setQuery($query); - $db->execute(); - + // From database + return AltCookies::get($name); } - } - - /** - * Get hash session ID - * - * @return string - */ - static private function _apbct_alt_session__id__get() - { - /** @var \Cleantalk\Common\Helper\Helper $helper_class */ - $helper_class = Mloader::get('Helper'); - $id = $helper_class::ipGet('real') - . filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') - . filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE'); - return hash('sha256', $id); + // From cookies + return $_COOKIE[$name] ?? null; } private function get_spam_comments($offset = 0, $on_page = 20, $improved_check = false) diff --git a/lib/Cleantalk/Custom/AltCookies.php b/lib/Cleantalk/Custom/AltCookies.php new file mode 100644 index 0000000..facc7aa --- /dev/null +++ b/lib/Cleantalk/Custom/AltCookies.php @@ -0,0 +1,184 @@ + 'int', + 'ct_fkp_timestamp' => 'int', + 'ct_pointer_data' => 'string', + 'ct_timezone' => 'int', + 'ct_visible_fields' => 'string', + 'ct_visible_fields_count' => 'int', + 'ct_event_token' => 'hash', + 'apbct_cookies_test' => 'json', + 'apbct_timestamp' => 'int', + 'apbct_prev_referer' => 'url', + ]; + + public static function removeOld() + { + $db = JFactory::getDbo(); + $query = $db->getQuery(true); + + $query->delete($db->quoteName('#__' . self::SESSION_TABLE__NAME)); + $query->where($db->quoteName('last_update') . ' < NOW() - INTERVAL '. self::SESSION_LIFE_TIME .' SECOND'); + + $db->setQuery($query); + $db->execute(); + } + + public static function set($name, $value) + { + $validated = self::validate([$name => $value]); + if ( count($validated) > 0 && isset($validated[$name]) ) { + // Replace value by validated value + $value = $validated[$name]; + + $db = JFactory::getDbo(); + $query = $db->getQuery(true); + + $columns = array('id', 'name', 'value', 'last_update'); + $values = array($db->quote(self::getId()), $db->quote($name), $db->quote($value), $db->quote(date('Y-m-d H:i:s'))); + $query + ->insert($db->quoteName('#__' . self::SESSION_TABLE__NAME)) + ->columns($db->quoteName($columns)) + ->values(implode(',', $values)); + $db->setQuery($query . ' ON DUPLICATE KEY UPDATE ' . $db->quoteName('value') . ' = '.$db->quote($value).', ' . $db->quoteName('last_update') . ' = ' . $db->quote(date('Y-m-d H:i:s'))); + $db->execute(); + } + } + + public static function get($name) + { + $db = JFactory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName(array('value'))); + $query->from($db->quoteName('#__' . self::SESSION_TABLE__NAME)); + $query->where($db->quoteName('id') . ' = '. $db->quote(self::getId())); + $query->where($db->quoteName('name') . ' = '. $db->quote($name)); + $db->setQuery($query); + $value = $db->loadResult(); + + if ( ! is_null($value) ) { + return $value; + } + + return null; + } + + public static function setFromRemote($data) + { + $db = JFactory::getDbo(); + $columns = array( + 'id', + 'name', + 'value', + 'last_update' + ); + $values = array(); + $query = $db->getQuery(true); + $query->insert($db->quoteName('#__' . self::SESSION_TABLE__NAME)); + $query->columns($db->quoteName($columns)); + + $data = self::validate($data); + + foreach ($data as $cookie_name => $cookie_value) { + $values[] = implode(',', array( + $db->quote(self::getId()), + $db->quote($cookie_name), + $db->quote($cookie_value), + $db->quote(date('Y-m-d H:i:s')) + )); + } + + $query->values($values); + + $db->setQuery($query . ' ON DUPLICATE KEY UPDATE value=VALUES(value), last_update=VALUES(last_update);'); + $db->execute(); + + return ('XHR OK'); + } + + /** + * Get hash session ID + * + * @return string + */ + private static function getId() + { + /** @var \Cleantalk\Common\Helper\Helper $helper_class */ + $helper_class = Mloader::get('Helper'); + + $id = $helper_class::ipGet() + . filter_input(INPUT_SERVER, 'HTTP_USER_AGENT') + . filter_input(INPUT_SERVER, 'HTTP_ACCEPT_LANGUAGE'); + return hash('sha256', $id); + } + + /** + * Incoming data validation against allowed alt cookies and theirs types + * + * @param array $cookies_array + * + * @return array + */ + private static function validate($cookies_array) + { + // Incoming data validation against allowed alt cookies + foreach ($cookies_array as $name => $value) { + if ( ! array_key_exists($name, self::$allowed_alt_cookies) ) { + unset($cookies_array[$name]); + continue; + } + + // Validate value type + switch (self::$allowed_alt_cookies[$name]) { + case 'int': + $cookies_array[$name] = (int)$value; + break; + case 'bool': + $cookies_array[$name] = (bool)$value; + break; + case 'string': + if (is_array($value) || is_object($value)) { + unset($cookies_array[$name]); + break; + } + $cookies_array[$name] = (string)$value; + break; + case 'json': + if ( ! is_string($value) || json_decode($value) === null ) { + unset($cookies_array[$name]); + } + break; + case 'url': + if ( ! filter_var($value, FILTER_VALIDATE_URL) ) { + unset($cookies_array[$name]); + } + break; + case 'hash': + if ( ! preg_match('/^[a-f0-9]{32,128}$/', $value) ) { + unset($cookies_array[$name]); + } + break; + default: + // If the type is not recognized, remove the cookie + unset($cookies_array[$name]); + } + } + return $cookies_array; + } +} From 5cbc712fcceee0b1fedf5803a8fec75a5204e2dd Mon Sep 17 00:00:00 2001 From: Glomberg Date: Thu, 7 May 2026 12:08:04 +0300 Subject: [PATCH 02/13] Upd. Code. Cleantalk common lib updated. --- lib/Cleantalk/Common/Localize/Localize.php | 51 ++++ .../Common/Queue/Exceptions/QueueError.php | 7 + .../Common/Queue/Exceptions/QueueExit.php | 7 + lib/Cleantalk/Common/Queue/Queue.php | 248 +++++++++++++++++ .../Exceptions/RemoteCallsException.php | 7 + .../Common/RemoteCalls/RemoteCalls.php | 261 ++++++++++++++++++ .../Common/StorageHandler/StorageHandler.php | 16 ++ lib/Cleantalk/Common/Variables/Cookie.php | 100 +++++++ lib/Cleantalk/Common/Variables/Get.php | 42 +++ lib/Cleantalk/Common/Variables/Post.php | 43 +++ lib/Cleantalk/Common/Variables/Request.php | 55 ++++ lib/Cleantalk/Common/Variables/Server.php | 141 ++++++++++ .../Common/Variables/ServerVariables.php | 140 ++++++++++ 13 files changed, 1118 insertions(+) create mode 100644 lib/Cleantalk/Common/Localize/Localize.php create mode 100644 lib/Cleantalk/Common/Queue/Exceptions/QueueError.php create mode 100644 lib/Cleantalk/Common/Queue/Exceptions/QueueExit.php create mode 100644 lib/Cleantalk/Common/Queue/Queue.php create mode 100644 lib/Cleantalk/Common/RemoteCalls/Exceptions/RemoteCallsException.php create mode 100644 lib/Cleantalk/Common/RemoteCalls/RemoteCalls.php create mode 100644 lib/Cleantalk/Common/StorageHandler/StorageHandler.php create mode 100644 lib/Cleantalk/Common/Variables/Cookie.php create mode 100644 lib/Cleantalk/Common/Variables/Get.php create mode 100644 lib/Cleantalk/Common/Variables/Post.php create mode 100644 lib/Cleantalk/Common/Variables/Request.php create mode 100644 lib/Cleantalk/Common/Variables/Server.php create mode 100644 lib/Cleantalk/Common/Variables/ServerVariables.php diff --git a/lib/Cleantalk/Common/Localize/Localize.php b/lib/Cleantalk/Common/Localize/Localize.php new file mode 100644 index 0000000..b6603ad --- /dev/null +++ b/lib/Cleantalk/Common/Localize/Localize.php @@ -0,0 +1,51 @@ +lang_dir = $lang_dir; + $this->locale = $locale; + } + + public function translate($string) + { + $lang_file = $this->lang_dir . '/' . $this->locale . '.lang'; + + if ( ! file_exists($lang_file) ) { + return $string; + } + + $phrases = file($lang_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + if ( $phrases === false ) { + return $string; + } + + // Loop through the lines to find the target string + foreach ($phrases as $index => $line) { + // Check if the next line exists + if ( ( strpos($line, $string) !== false ) && isset($phrases[$index + 1]) ) { + return $phrases[$index + 1]; + } + } + + return $string; + } +} diff --git a/lib/Cleantalk/Common/Queue/Exceptions/QueueError.php b/lib/Cleantalk/Common/Queue/Exceptions/QueueError.php new file mode 100644 index 0000000..a960e93 --- /dev/null +++ b/lib/Cleantalk/Common/Queue/Exceptions/QueueError.php @@ -0,0 +1,7 @@ +api_key = $api_key; + $this->pid = mt_rand(0, mt_getrandmax()); + + $queue = $this->getQueue(); + if ( $queue !== false && isset($queue['stages']) ) { + $this->queue = $queue; + } else { + $this->queue = array( + 'started' => time(), + 'finished' => '', + 'stages' => array(), + ); + } + } + + public function getQueue() + { + /** @var \Cleantalk\Common\StorageHandler\StorageHandler $storage_handler_class */ + $storage_handler_class = Mloader::get('StorageHandler'); + $storage_handler_class = new $storage_handler_class(); + return $storage_handler_class->getSetting(self::QUEUE_NAME); + } + + public static function clearQueue() + { + /** @var \Cleantalk\Common\StorageHandler\StorageHandler $storage_handler_class */ + $storage_handler_class = Mloader::get('StorageHandler'); + $storage_handler_class = new $storage_handler_class(); + return $storage_handler_class->deleteSetting(self::QUEUE_NAME); + } + + public function saveQueue($queue) + { + /** @var \Cleantalk\Common\StorageHandler\StorageHandler $storage_handler_class */ + $storage_handler_class = Mloader::get('StorageHandler'); + $storage_handler_class = new $storage_handler_class(); + return $storage_handler_class->saveSetting(self::QUEUE_NAME, $queue); + } + + /** + * Refreshes the $this->queue from the DB + * + * @return void + */ + public function refreshQueue() + { + $this->queue = $this->getQueue(); + } + + /** + * @param string|array $stage_name + * @param array $args + */ + public function addStage($stage_name, $args = array(), $accepted_tries = 3) + { + $this->queue['stages'][] = array( + 'name' => $stage_name, + 'status' => 'NULL', + 'tries' => '0', + 'accepted_tries' => $accepted_tries, + 'args' => $args, + 'pid' => null, + ); + $this->saveQueue($this->queue); + } + + /** + * @throws QueueExit + */ + public function executeStage() + { + // @ToDo need to replace this Firewall dependency + $fw_stats = Firewall::getFwStats(); + $stage_to_execute = null; + + if ( $this->hasUnstartedStages() ) { + $this->queue['stages'][$this->unstarted_stage]['status'] = 'IN_PROGRESS'; + $this->queue['stages'][$this->unstarted_stage]['start'] = time(); + $this->queue['stages'][$this->unstarted_stage]['pid'] = $this->pid; + + $this->saveQueue($this->queue); + + sleep(2); + + $this->refreshQueue(); + + if ( $this->queue['stages'][$this->unstarted_stage]['pid'] !== $this->pid ) { + throw new QueueExit( + 'Queue pid is wrong for the stage ' . $this->queue['stages'][$this->unstarted_stage]['name'] + ); + } + + $stage_to_execute = &$this->queue['stages'][$this->unstarted_stage]; + } + + if ( $stage_to_execute ) { + if ( is_array($stage_to_execute['name']) ) { + $class_to_execute = $stage_to_execute['name'][0]; + $method_to_execute = $stage_to_execute['name'][1]; + if ( is_callable(array($class_to_execute, $method_to_execute)) ) { + ++$stage_to_execute['tries']; + + if ( !empty($stage_to_execute['args']) ) { + $result = $class_to_execute::$method_to_execute($this->api_key, $stage_to_execute['args']); + } else { + $result = $class_to_execute::$method_to_execute($this->api_key); + } + } else { + throw new QueueError( + $class_to_execute . '::' . $method_to_execute . ' is not a callable function.' + ); + } + } else { + if ( is_callable($stage_to_execute['name']) ) { + ++$stage_to_execute['tries']; + + if ( !empty($stage_to_execute['args']) ) { + $result = $stage_to_execute['name']($stage_to_execute['args']); + } else { + $result = $stage_to_execute['name'](); + } + } else { + throw new QueueError($stage_to_execute['name'] . ' is not a callable function.'); + } + } + + if ( isset($result['error']) ) { + $stage_to_execute['status'] = 'NULL'; + $stage_to_execute['error'][] = $result['error']; + if ( isset($result['update_args']['args']) ) { + $stage_to_execute['args'] = $result['update_args']['args']; + } + $this->saveQueue($this->queue); + $accepted_tries = isset($stage_to_execute['accepted_tries']) ? $stage_to_execute['accepted_tries'] : 3; + if ( $stage_to_execute['tries'] >= $accepted_tries ) { + $stage_to_execute['status'] = 'FINISHED'; + $this->saveQueue($this->queue); + return $result; + } + + /** @var \Cleantalk\Common\RemoteCalls\RemoteCalls $remote_calls_class */ + $remote_calls_class = Mloader::get('RemoteCalls'); + return $remote_calls_class::perform( + 'sfw_update', + 'apbct', + $this->api_key, + array( + 'firewall_updating_id' => $fw_stats->updating_id, + 'worker' => 1, + 'stage' => 'Repeat ' . + is_array($stage_to_execute['name']) + ? $stage_to_execute['name'][0] . '::' . $stage_to_execute['name'][1] + : $stage_to_execute['name'] + ), + array('async') + ); + } + + if ( isset($result['next_stage']) ) { + $this->addStage( + $result['next_stage']['name'], + isset($result['next_stage']['args']) ? $result['next_stage']['args'] : array(), + isset($result['next_stage']['accepted_tries']) ? $result['next_stage']['accepted_tries'] : 3 + ); + } + + if ( isset($result['next_stages']) && count($result['next_stages']) ) { + foreach ( $result['next_stages'] as $next_stage ) { + $this->addStage( + $next_stage['name'], + isset($next_stage['args']) ? $next_stage['args'] : array(), + isset($result['next_stage']['accepted_tries']) ? $result['next_stage']['accepted_tries'] : 3 + ); + } + } + + $stage_to_execute['status'] = 'FINISHED'; + $this->saveQueue($this->queue); + + return $result; + } + + throw new QueueExit('No stage to execute. Exit.'); + } + + public function isQueueInProgress() + { + if ( count($this->queue['stages']) > 0 ) { + $this->unstarted_stage = array_search('IN_PROGRESS', array_column($this->queue['stages'], 'status'), true); + + return is_int($this->unstarted_stage); + } + + return false; + } + + public function isQueueFinished() + { + return !$this->isQueueInProgress() && !$this->hasUnstartedStages(); + } + + /** + * Checks if the queue is over + * + * @return bool + */ + public function hasUnstartedStages() + { + if ( count($this->queue['stages']) > 0 ) { + $this->unstarted_stage = array_search('NULL', array_column($this->queue['stages'], 'status'), true); + + return is_int($this->unstarted_stage); + } + + return false; + } +} diff --git a/lib/Cleantalk/Common/RemoteCalls/Exceptions/RemoteCallsException.php b/lib/Cleantalk/Common/RemoteCalls/Exceptions/RemoteCallsException.php new file mode 100644 index 0000000..a19165a --- /dev/null +++ b/lib/Cleantalk/Common/RemoteCalls/Exceptions/RemoteCallsException.php @@ -0,0 +1,7 @@ +api_key = $api_key; + $this->storage_handler_class = $storage_handler_class; + $this->available_rc_actions = $this->getAvailableRcActions(); + } + + /** + * @param $name + * @return mixed + * @psalm-taint-source input + */ + public static function getVariable($name) + { + return Request::get($name); + } + + /** + * Checking if the current request is the Remote Call + * + * @return bool + */ + public static function check() + { + return + static::getVariable('spbc_remote_call_token') && + static::getVariable('spbc_remote_call_action') && + static::getVariable('plugin_name') && + in_array(static::getVariable('plugin_name'), array('antispam', 'anti-spam', 'apbct')); + } + + /** + * Execute corresponding method of RemoteCalls if exists + * + * @throws RemoteCallsException + * + * @return string + */ + public function process() + { + $token = strtolower(static::getVariable('spbc_remote_call_token')); + + if ( $token !== strtolower(md5($this->api_key)) ) { + throw new RemoteCallsException('WRONG_TOKEN'); + } + + $action = strtolower(static::getVariable('spbc_remote_call_action')); + $actions = $this->available_rc_actions; + + if ( ! count($actions) ) { + throw new RemoteCallsException('Available RC actions did not loaded.'); + } + + if ( ! array_key_exists($action, $actions) ) { + throw new RemoteCallsException('Not available RC action was provided.'); + } + + // Return OK for test remote calls + if ( static::getVariable('test') ) { + return 'OK'; + } + + $cooldown = isset($actions[$action]['cooldown']) ? $actions[$action]['cooldown'] : self::COOLDOWN; + + if ( time() - $actions[$action]['last_call'] < $cooldown ) { + throw new RemoteCallsException('TOO_MANY_ATTEMPTS'); + } + + $this->setLastCall($action); + // Flag to let plugin know that Remote Call is running. + $this->rc_running = true; + + $action_method = 'action__' . $action; + + if ( ! method_exists(static::class, $action_method) ) { + throw new RemoteCallsException('UNKNOWN_ACTION_METHOD: ' . $action_method); + } + + // Delay before perform action; + if ( static::getVariable('delay') ) { + sleep(static::getVariable('delay')); + } + + try { + $action_result = static::$action_method(); + + // Supports old results returned an array ['error'=>'Error text'] + if ( is_array($action_result) && isset($action_result['error']) ) { + throw new RemoteCallsException($action_result['error']); + } + + // @ToDo we can returning the RC result instead of simple 'OK' + return 'OK'; + } catch ( \Exception $exception ) { + throw new RemoteCallsException('RC result error: ' . $exception->getMessage()); + } + } + + /** + * Get available remote calls from the storage. + * + * @return array + */ + protected function getAvailableRcActions() + { + $actions = $this->storage_handler_class->getSetting(static::OPTION_NAME); + return $actions ?: $this->available_rc_actions; + } + + /** + * Set last call timestamp and save it to the storage. + * + * @param string $action + * @return bool + */ + protected function setLastCall($action) + { + $this->available_rc_actions[$action]['last_call'] = time(); + return $this->storage_handler_class->saveSetting(static::OPTION_NAME, $this->available_rc_actions); + } + + /************************ Making Request Methods ************************/ + // @ToDo methods below must be replaced to the another class + + public static function getSiteUrl() + { + return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . (isset($_SERVER['SCRIPT_URL']) ? $_SERVER['SCRIPT_URL'] : ''); + } + + public static function buildParameters($rc_action, $plugin_name, $api_key, $additional_params) + { + return array_merge( + array( + 'spbc_remote_call_token' => md5($api_key), + 'spbc_remote_call_action' => $rc_action, + 'plugin_name' => $plugin_name, + ), + $additional_params + ); + } + + /** + * Performs remote call to the current website + * + * @param string $host + * @param string $rc_action + * @param string $plugin_name + * @param string $api_key + * @param array $params + * @param array $patterns + * @param bool $do_check Perform check before main remote call or not + * + * @return bool|string[] + * @psalm-suppress PossiblyUnusedMethod + */ + public static function perform($rc_action, $plugin_name, $api_key, $params, $patterns = array(), $do_check = true) + { + $host = static::getSiteUrl(); + $params = static::buildParameters($rc_action, $plugin_name, $api_key, $params); + + if ( $do_check ) { + $result__rc_check_website = static::performTest($host, $params, $patterns); + if ( !empty($result__rc_check_website['error']) ) { + return $result__rc_check_website; + } + } + + $http = new \Cleantalk\Common\Http\Request(); + + return $http + ->setUrl($host) + ->setData($params) + ->setPresets($patterns) + ->request(); + } + + /** + * Performs test remote call to the current website + * Expects 'OK' string as good response + * + * @param string $host + * @param array $params + * @param array $patterns + * + * @return array|bool|string + */ + public static function performTest($host, $params, $patterns = array()) + { + // Delete async pattern to get the result in this process + $key = array_search('async', $patterns, true); + if ( $key ) { + unset($patterns[$key]); + } + + // Adding test flag + $params = array_merge($params, array('test' => 'test')); + + // Perform test request + $http = new \Cleantalk\Common\Http\Request(); + $result = $http + ->setUrl($host) + ->setData($params) + ->setPresets($patterns) + ->request(); + + // Considering empty response as error + if ( $result === '' ) { + $result = array('error' => 'WRONG_SITE_RESPONSE TEST ACTION : ' . $params['spbc_remote_call_action'] . ' ERROR: EMPTY_RESPONSE'); + // Wrap and pass error + } elseif ( !empty($result['error']) ) { + $result = array('error' => 'WRONG_SITE_RESPONSE TEST ACTION: ' . $params['spbc_remote_call_action'] . ' ERROR: ' . $result['error']); + // Expects 'OK' string as good response otherwise - error + } elseif ( is_string($result) && !preg_match('@^.*?OK$@', $result) ) { + $result = array( + 'error' => 'WRONG_SITE_RESPONSE ACTION: ' + . $params['spbc_remote_call_action'] + . ' RESPONSE: ' + . '"' + . htmlspecialchars(substr($result, 0, 400)) + . '"' + ); + } + + return $result; + } +} diff --git a/lib/Cleantalk/Common/StorageHandler/StorageHandler.php b/lib/Cleantalk/Common/StorageHandler/StorageHandler.php new file mode 100644 index 0000000..91dd823 --- /dev/null +++ b/lib/Cleantalk/Common/StorageHandler/StorageHandler.php @@ -0,0 +1,16 @@ +variables + if (! isset(static::$instance->variables[$name])) { + if ( isset($_COOKIE[$name]) ) { + $value = $this->getAndSanitize($_COOKIE[$name]); + } else { + $value = ''; + } + + // Remember for further calls + static::getInstance()->rememberVariable($name, $value); + + return $value; + } + + return static::$instance->variables[$name]; + } + + /** + * Universal method to adding cookies + * Wrapper for setcookie() Conisdering PHP version + * + * @see https://www.php.net/manual/ru/function.setcookie.php + * + * @param string $name Cookie name + * @param string $value Cookie value + * @param int $expires Expiration timestamp. 0 - expiration with session + * @param string $path + * @param string $domain + * @param bool $secure + * @param bool $httponly + * @param string $samesite + * + * @return void + * @psalm-suppress PossiblyUnusedMethod + */ + public static function set( + $name, + $value = '', + $expires = 0, + $path = '', + $domain = '', + $secure = null, + $httponly = false, + $samesite = 'Lax' + ) { + if (headers_sent()) { + return; + } + + $secure = ! is_null($secure) ? $secure : ! in_array(Server::get('HTTPS'), ['off', '']) || Server::get('SERVER_PORT') == 443; + + // For PHP 7.3+ and above + if ( version_compare(phpversion(), '7.3.0', '>=') ) { + $params = array( + 'expires' => $expires, + 'path' => $path, + 'domain' => $domain, + 'secure' => $secure, + 'httponly' => $httponly, + ); + + if ($samesite) { + $params['samesite'] = $samesite; + } + + /** + * @psalm-suppress InvalidArgument + */ + setcookie($name, $value, $params); + // For PHP 5.6 - 7.2 + } else { + setcookie($name, $value, $expires, $path, $domain, $secure, $httponly); + } + } +} diff --git a/lib/Cleantalk/Common/Variables/Get.php b/lib/Cleantalk/Common/Variables/Get.php new file mode 100644 index 0000000..ab29d21 --- /dev/null +++ b/lib/Cleantalk/Common/Variables/Get.php @@ -0,0 +1,42 @@ +variables + if (! isset(static::$instance->variables[$name])) { + if ( isset($_GET[$name]) ) { + $value = $this->getAndSanitize($_GET[$name]); + } else { + $value = ''; + } + + // Remember for further calls + static::getInstance()->rememberVariable($name, $value); + + return $value; + } + + return static::$instance->variables[$name]; + } +} diff --git a/lib/Cleantalk/Common/Variables/Post.php b/lib/Cleantalk/Common/Variables/Post.php new file mode 100644 index 0000000..da98cb9 --- /dev/null +++ b/lib/Cleantalk/Common/Variables/Post.php @@ -0,0 +1,43 @@ +variables + if (! isset(static::$instance->variables[$name])) { + if ( isset($_POST[$name]) ) { + $value = $this->getAndSanitize($_POST[$name]); + } else { + $value = ''; + } + + // Remember for further calls + static::getInstance()->rememberVariable($name, $value); + + return $value; + } + + return static::$instance->variables[$name]; + } +} diff --git a/lib/Cleantalk/Common/Variables/Request.php b/lib/Cleantalk/Common/Variables/Request.php new file mode 100644 index 0000000..941885d --- /dev/null +++ b/lib/Cleantalk/Common/Variables/Request.php @@ -0,0 +1,55 @@ +variables + if (isset(static::$instance->variables[$name])) { + return static::$instance->variables[$name]; + } + + $value = ''; + + $class_name = get_class(self::getInstance()); + $reflection_class = new \ReflectionClass($class_name); + $namespace = $reflection_class->getNamespaceName(); + + $post_class = $namespace . '\\Post'; + $get_class = $namespace . '\\Get'; + $cookie_class = $namespace . '\\Cookie'; + + if ( $post_class::get($name) ) { + $value = $post_class::get($name); + } elseif ( $get_class::get($name) ) { + $value = $get_class::get($name); + } elseif ( $cookie_class::get($name) ) { + $value = $cookie_class::get($name); + } + + // Remember for further calls + static::getInstance()->rememberVariable($name, $value); + + return $value; + } +} diff --git a/lib/Cleantalk/Common/Variables/Server.php b/lib/Cleantalk/Common/Variables/Server.php new file mode 100644 index 0000000..b1a7750 --- /dev/null +++ b/lib/Cleantalk/Common/Variables/Server.php @@ -0,0 +1,141 @@ +server + if (isset(static::$instance->variables[$name])) { + return static::$instance->variables[$name]; + } + + $name = strtoupper($name); + + if ( isset($_SERVER[$name]) ) { + $value = $this->getAndSanitize($_SERVER[$name]); + } else { + $value = ''; + } + + // Convert to upper case for REQUEST_METHOD + if ($name === 'REQUEST_METHOD') { + $value = strtoupper($value); + } + + // Convert HTML chars for HTTP_USER_AGENT, HTTP_USER_AGENT, SERVER_NAME + if (in_array($name, array('HTTP_USER_AGENT', 'HTTP_USER_AGENT', 'SERVER_NAME'))) { + $value = htmlspecialchars($value); + } + + // Remember for further calls + static::getInstance()->rememberVariable($name, $value); + + return $value; + } + + /** + * Checks if $_SERVER['REQUEST_URI'] contains string + * + * @param string $needle + * + * @return bool + * @psalm-suppress PossiblyUnusedMethod + */ + public static function inUri($needle) + { + return self::hasString('REQUEST_URI', $needle); + } + + /** + * Is the host contains the string + * + * @param string $needle + * + * @return bool + * @psalm-suppress PossiblyUnusedMethod + */ + public static function inHost($needle) + { + return self::hasString('HTTP_HOST', $needle); + } + + /** + * Getting domain name + * + * @return false|string + * @psalm-suppress PossiblyUnusedMethod + */ + public static function getDomain() + { + preg_match('@\S+\.(\S+)\/?$@', self::get('HTTP_HOST'), $matches); + + return isset($matches[1]) ? $matches[1] : false; + } + + /** + * Checks if $_SERVER['REQUEST_URI'] contains string + * + * @param string $needle needle + * + * @return bool + * @psalm-suppress PossiblyUnusedMethod + */ + public static function inReferer($needle) + { + return self::hasString('HTTP_REFERER', $needle); + } + + /** + * Checks if the current request method is POST + * + * @return bool + * @psalm-suppress PossiblyUnusedMethod + */ + public static function isPost() + { + return self::get('REQUEST_METHOD') === 'POST'; + } + + /** + * Checks if the current request method is GET + * + * @return bool + * @psalm-suppress PossiblyUnusedMethod + */ + public static function isGet() + { + return self::get('REQUEST_METHOD') === 'GET'; + } + + /** + * Determines if SSL is used. + * + * @return bool True if SSL, otherwise false. + * @psalm-suppress PossiblyUnusedMethod + */ + public static function isSSL() + { + return self::get('HTTPS') === 'on' || + self::get('HTTPS') === '1' || + self::get('SERVER_PORT') == '443'; + } +} diff --git a/lib/Cleantalk/Common/Variables/ServerVariables.php b/lib/Cleantalk/Common/Variables/ServerVariables.php new file mode 100644 index 0000000..80e7c29 --- /dev/null +++ b/lib/Cleantalk/Common/Variables/ServerVariables.php @@ -0,0 +1,140 @@ +getVariable($name); + + // Validate variable + if ( $validation_filter && ! Validate::validate($variable, $validation_filter) ) { + return false; + } + + if ( $sanitize_filter ) { + $variable = Sanitize::sanitize($variable, $sanitize_filter); + } + + return $variable; + } + + /** + * BLUEPRINT + * Gets given ${_SOMETHING} variable and save it to memory + * + * @param $name + * + * @return mixed|string + */ + protected function getVariable($name){} + + /** + * Save variable to $this->variables[] + * + * @param string $name + * @param string $value + */ + protected function rememberVariable($name, $value) + { + static::$instance->variables[$name] = $value; + } + + /** + * Checks if variable contains given string + * + * @param string $var Haystack to search in + * @param string $string Needle to search + * + * @return bool + */ + public static function hasString($var, $string) + { + return stripos(self::get($var), $string) !== false; + } + + /** + * Checks if variable equal to $param + * + * @param string $var Variable to compare + * @param string $param Param to compare + * + * @return bool + * @psalm-suppress PossiblyUnusedMethod + */ + public static function equal($var, $param) + { + return self::get($var) === $param; + } + + /** + * @param $value + * @param $nesting + * + * @return string|array + */ + public function getAndSanitize($value, $nesting = 0) + { + if ( is_array($value) ) { + foreach ( $value as $_key => & $val ) { + if ( is_array($val) ) { + if ( $nesting > 20 ) { + return $value; + } + $this->getAndSanitize($val, ++$nesting); + } else { + $val = $this->sanitizeDefault($val); + } + } + } else { + $value = $this->sanitizeDefault($value); + } + return $value; + } + + /** + * Sanitize gathering data. + * No sanitizing by default. + * Override this method in the internal class! + * + * @param string $value + * + * @return string + */ + protected function sanitizeDefault($value) + { + return $value; + } +} From 4d93911286f3b363f1e7aa9a2da6cdcd257ac56f Mon Sep 17 00:00:00 2001 From: Glomberg Date: Thu, 7 May 2026 12:27:26 +0300 Subject: [PATCH 03/13] New. Code. Rate limiter package added. --- composer.json | 3 +- .../Common/RateLimiter/RateLimiter.php | 223 ++++++++++++++++++ .../Common/RateLimiter/RateLimiterConfig.php | 50 ++++ .../Common/RateLimiter/RateLimiterDto.php | 94 ++++++++ .../Custom/RateLimiter/RateLimiter.php | 130 ++++++++++ 5 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 lib/Cleantalk/Common/RateLimiter/RateLimiter.php create mode 100644 lib/Cleantalk/Common/RateLimiter/RateLimiterConfig.php create mode 100644 lib/Cleantalk/Common/RateLimiter/RateLimiterDto.php create mode 100644 lib/Cleantalk/Custom/RateLimiter/RateLimiter.php diff --git a/composer.json b/composer.json index de67488..729d4fd 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,8 @@ "cleantalk/api": "*", "cleantalk/cron": "*", "cleantalk/remote-calls": "*", - "cleantalk/storage-handler": "*" + "cleantalk/storage-handler": "*", + "cleantalk/rate-limiter": "*" }, "require-dev": { "phpunit/phpunit": "^8.5.52", diff --git a/lib/Cleantalk/Common/RateLimiter/RateLimiter.php b/lib/Cleantalk/Common/RateLimiter/RateLimiter.php new file mode 100644 index 0000000..da3d047 --- /dev/null +++ b/lib/Cleantalk/Common/RateLimiter/RateLimiter.php @@ -0,0 +1,223 @@ +config = $config; + $this->current_ts = time(); + + $this->setIP(); + $this->setUA(); + $this->setUID(); + } + + /** + * Sets the unique identifier for the current request + * Must be implemented by child classes + * + * @return void + */ + protected function setUID(): void + { + $this->uid = md5($this->ip . $this->ua . $this->config->type); + } + + /** + * Determines if the current request should be rate limited + * + * @return bool True if request should be allowed or error occurred, false if rate limited + */ + public function checkPassed(): bool + { + try { + if (!$this->healthCheck()) { + throw new \Exception('HEALTH_CHECK_FAILED'); + } + + if (!$this->cleanUp()) { + throw new \Exception('CLEANUP_FAILED'); + } + + $uid_data = $this->selectUIDData(); + $record_found = false !== $uid_data; + + if ($record_found && !$uid_data->data_ok) { + throw new \Exception('UID_DATA_INVALID'); + } + + if ($record_found) { + if (!$this->increment($uid_data)) { + throw new \Exception('INCREMENT_FAILED'); + } + } else { + $uid_data = new RateLimitDTO( + array( + 'uid' => $this->uid, + 'type' => $this->config->type, + 'counter' => 1, + 'last_call' => $this->current_ts, + 'created_at' => $this->current_ts, + 'ip' => $this->ip, + 'ua' => $this->ua, + ) + ); + if (!$uid_data->data_ok) { + throw new \Exception('UID_DATA_INVALID__INSERT'); + } + if (!$this->insert($uid_data)) { + throw new \Exception('INSERT_FAILED'); + } + } + } catch (\Exception $e) { + $this->process_ok = false; + $this->handleErrors($e->getMessage()); + return true; + } + + return !$this->isLocked($uid_data); + } + + /** + * Checks if the current UID has exceeded the rate limit + * + * @param RateLimitDTO $uid_data + * @return bool True if rate limited, false otherwise + */ + protected function isLocked(RateLimitDTO $uid_data): bool + { + return $uid_data->data_ok && ($uid_data->counter > $this->config->limit); + } + + /** + * Performs basic health check on configuration + * + * @return bool True if configuration is valid, false otherwise + */ + protected function healthCheck(): bool + { + if ($this->config->limit < 1) { + return false; + } + if ($this->config->period < 1) { + return false; + } + if ($this->config->type === null) { + return false; + } + return true; + } + + /** + * Retrieves rate limit data for the current UID + * Default implementation returns empty data + * + * @return RateLimitDTO|false Rate limit data or false if not found + */ + protected function selectUIDData() + { + return new RateLimitDTO(array()); + } + + /** + * Sets the IP address for the current request + * Must be implemented by child classes + * + * @return void + */ + abstract protected function setIP(): void; + + /** + * Sets the IP address for the current request + * Must be implemented by child classes + * + * @return void + */ + abstract protected function setUA(): void; + + /** + * Handles errors that occur during rate limiting + * Must be implemented by child classes + * + * @param string $msg Error message + * @return void + */ + abstract protected function handleErrors(string $msg): void; + + /** + * Increments the counter for an existing rate limit record + * Must be implemented by child classes + * @param RateLimitDTO $uid_data + * @return bool True on success, false on failure + */ + abstract protected function increment(RateLimitDTO $uid_data): bool; + + /** + * Inserts a new rate limit record + * Must be implemented by child classes + * @param RateLimitDTO $uid_data + * @return bool True on success, false on failure + */ + abstract protected function insert(RateLimitDTO $uid_data): bool; + + /** + * Cleans up expired rate limit records + * Must be implemented by child classes + * + * @return bool True on success, false on failure + */ + abstract protected function cleanUp(): bool; +} diff --git a/lib/Cleantalk/Common/RateLimiter/RateLimiterConfig.php b/lib/Cleantalk/Common/RateLimiter/RateLimiterConfig.php new file mode 100644 index 0000000..93f673b --- /dev/null +++ b/lib/Cleantalk/Common/RateLimiter/RateLimiterConfig.php @@ -0,0 +1,50 @@ +type = $type; + $this->limit = $limit; + $this->period = $period; + } +} diff --git a/lib/Cleantalk/Common/RateLimiter/RateLimiterDto.php b/lib/Cleantalk/Common/RateLimiter/RateLimiterDto.php new file mode 100644 index 0000000..59a8372 --- /dev/null +++ b/lib/Cleantalk/Common/RateLimiter/RateLimiterDto.php @@ -0,0 +1,94 @@ +data_ok = true; + } catch (\Exception $_e) { + $this->data_ok = false; + } + } +} diff --git a/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php new file mode 100644 index 0000000..2d5990c --- /dev/null +++ b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php @@ -0,0 +1,130 @@ +db_object = $db_class::getInstance(); + $this->table_name = $this->db_object->prefix . APBCT_RATE_LIMITS; + } + + + /** + * @inheritDoc + */ + protected function setIP(): void + { + /** @var \Cleantalk\Common\Helper\Helper $helper_class */ + $helper_class = Mloader::get('Helper'); + $this->ip = $helper_class::ipGet(); + } + + /** + * @inheritDoc + */ + protected function setUA(): void + { + $this->ua = (string) Server::get('HTTP_USER_AGENT', 'default_ua'); + } + + /** + * @inheritDoc + */ + protected function handleErrors(string $msg): void + { + error_log('CleanTalk RateLimiter error: ' . $msg); + } + + /** + * @inheritDoc + */ + protected function increment(RateLimiterDto $uid_data): bool + { + $is_expired = ($this->current_ts - $uid_data->created_at) > $this->config->period; + + $uid_data->counter = $is_expired ? 1 : $uid_data->counter + 1; + $uid_data->created_at = $is_expired ? $this->current_ts : $uid_data->created_at; + $uid_data->last_call = $this->current_ts; + + $this->db_object->prepare( + ' + UPDATE ' . $this->table_name . ' SET + counter = %d, + last_call = %d, + created_at = %d + WHERE uid = %s + ', [ + $uid_data->counter, + $uid_data->last_call, + $uid_data->created_at, + $uid_data->uid + ]); + + return false !== $this->db_object->fetch($this->db_object->getQuery()); + } + + /** + * @inheritDoc + */ + protected function insert(RateLimiterDto $uid_data): bool + { + $this->db_object->prepare( + ' + INSERT INTO ' . $this->table_name . ' + (uid, type, ip, ua, counter, last_call, created_at) + VALUES (%s, %s, %s, %s, 1, %d, %d) + ON DUPLICATE KEY UPDATE last_call = %s, counter = counter + 1; + ',[ + $uid_data->uid, + $uid_data->type, + $uid_data->ip, + $uid_data->ua, + $uid_data->last_call, + $uid_data->created_at, + $uid_data->last_call + ]); + + $result = $this->db_object->fetch($this->db_object->getQuery()); + + return false !== $result; + } + + /** + * @inheritDoc + */ + protected function cleanUp(): bool + { + $threshold = $this->current_ts - ($this->config->period + 10); + + $this->db_object->prepare( + 'DELETE FROM ' . $this->table_name . ' WHERE created_at < %d AND type = %s;', + [ + $threshold, + $this->config->type + ] + ); + + $result = $this->db_object->fetch($this->db_object->getQuery()); + + return false !== $result; + } +} From 6a666d137fd5a6d78501d037d7be94b78b730c98 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Thu, 7 May 2026 12:27:58 +0300 Subject: [PATCH 04/13] New. Rate Limiter. New schemas added. --- sql/mysql/install.mysql.utf8.sql | 10 ++++++++++ sql/mysql/uninstall.mysql.utf8.sql | 1 + 2 files changed, 11 insertions(+) diff --git a/sql/mysql/install.mysql.utf8.sql b/sql/mysql/install.mysql.utf8.sql index 0327b48..e91d7d4 100644 --- a/sql/mysql/install.mysql.utf8.sql +++ b/sql/mysql/install.mysql.utf8.sql @@ -49,3 +49,13 @@ CREATE TABLE IF NOT EXISTS `#__cleantalk_custom_storage` ( `value` MEDIUMTEXT NULL DEFAULT NULL, PRIMARY KEY (`name`) ); +CREATE TABLE IF NOT EXISTS `#__cleantalk_rate_limits` ( + uid VARCHAR(32) NOT NULL, + type VARCHAR(32) NOT NULL, + ip VARCHAR(45) NOT NULL, + ua VARCHAR(200) NOT NULL, + counter INT NOT NULL DEFAULT 1, + last_call INT NOT NULL, + created_at INT NOT NULL, + PRIMARY KEY (uid) +); diff --git a/sql/mysql/uninstall.mysql.utf8.sql b/sql/mysql/uninstall.mysql.utf8.sql index 449f811..340b8cd 100644 --- a/sql/mysql/uninstall.mysql.utf8.sql +++ b/sql/mysql/uninstall.mysql.utf8.sql @@ -4,3 +4,4 @@ DROP TABLE IF EXISTS `#__cleantalk_sessions`; DROP TABLE IF EXISTS `#__cleantalk_ua_bl`; DROP TABLE IF EXISTS `#__cleantalk_usermeta`; DROP TABLE IF EXISTS `#__cleantalk_custom_storage`; +DROP TABLE IF EXISTS `#__cleantalk_rate_limits`; From d9df1906f38330222a53fb8474c501746bca8a74 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Thu, 7 May 2026 15:55:38 +0300 Subject: [PATCH 05/13] Upd. Rate Limiter. Common library updated. --- .../Common/RateLimiter/RateLimiter.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/Cleantalk/Common/RateLimiter/RateLimiter.php b/lib/Cleantalk/Common/RateLimiter/RateLimiter.php index da3d047..699f48f 100644 --- a/lib/Cleantalk/Common/RateLimiter/RateLimiter.php +++ b/lib/Cleantalk/Common/RateLimiter/RateLimiter.php @@ -104,7 +104,7 @@ public function checkPassed(): bool throw new \Exception('INCREMENT_FAILED'); } } else { - $uid_data = new RateLimitDTO( + $uid_data = new RateLimiterDTO( array( 'uid' => $this->uid, 'type' => $this->config->type, @@ -134,10 +134,10 @@ public function checkPassed(): bool /** * Checks if the current UID has exceeded the rate limit * - * @param RateLimitDTO $uid_data + * @param RateLimiterDTO $uid_data * @return bool True if rate limited, false otherwise */ - protected function isLocked(RateLimitDTO $uid_data): bool + protected function isLocked(RateLimiterDTO $uid_data): bool { return $uid_data->data_ok && ($uid_data->counter > $this->config->limit); } @@ -165,11 +165,11 @@ protected function healthCheck(): bool * Retrieves rate limit data for the current UID * Default implementation returns empty data * - * @return RateLimitDTO|false Rate limit data or false if not found + * @return RateLimiterDTO|false Rate limit data or false if not found */ protected function selectUIDData() { - return new RateLimitDTO(array()); + return new RateLimiterDTO(array()); } /** @@ -200,18 +200,18 @@ abstract protected function handleErrors(string $msg): void; /** * Increments the counter for an existing rate limit record * Must be implemented by child classes - * @param RateLimitDTO $uid_data + * @param RateLimiterDTO $uid_data * @return bool True on success, false on failure */ - abstract protected function increment(RateLimitDTO $uid_data): bool; + abstract protected function increment(RateLimiterDTO $uid_data): bool; /** * Inserts a new rate limit record * Must be implemented by child classes - * @param RateLimitDTO $uid_data + * @param RateLimiterDTO $uid_data * @return bool True on success, false on failure */ - abstract protected function insert(RateLimitDTO $uid_data): bool; + abstract protected function insert(RateLimiterDTO $uid_data): bool; /** * Cleans up expired rate limit records From 5d76dcb367ae78ae3bbfae22d922956609787da5 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Thu, 7 May 2026 15:58:08 +0300 Subject: [PATCH 06/13] New. Rate Limiter. Rate limit implemented for AltCookies set. --- cleantalkantispam.php | 1 + lib/Cleantalk/Custom/AltCookies.php | 12 +++++++++ .../Custom/RateLimiter/RateLimiter.php | 25 ++++++++++++++++--- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/cleantalkantispam.php b/cleantalkantispam.php index b1c6913..45a78fd 100644 --- a/cleantalkantispam.php +++ b/cleantalkantispam.php @@ -58,6 +58,7 @@ define('APBCT_TBL_AC_LOG', 'cleantalk_ac_log'); // Table with firewall logs. define('APBCT_TBL_AC_UA_BL', 'cleantalk_ua_bl'); // Table with User-Agents blacklist. define('APBCT_TBL_SESSIONS', 'cleantalk_sessions'); // Table with session data. +define('APBCT_RATE_LIMITS', 'cleantalk_rate_limits'); // Table with different rate limits data. !defined('APBCT_TBL_STORAGE') && define('APBCT_TBL_STORAGE', 'cleantalk_custom_storage'); // Table with session data. define('APBCT_SFW_SEND_LOGS_LIMIT', 1000); define('APBCT_SPAMSCAN_LOGS', 'cleantalk_spamscan_logs'); // Table with session data. diff --git a/lib/Cleantalk/Custom/AltCookies.php b/lib/Cleantalk/Custom/AltCookies.php index facc7aa..5da9426 100644 --- a/lib/Cleantalk/Custom/AltCookies.php +++ b/lib/Cleantalk/Custom/AltCookies.php @@ -3,6 +3,8 @@ namespace Cleantalk\Custom; use Cleantalk\Common\Mloader\Mloader; +use Cleantalk\Common\RateLimiter\RateLimiterConfig; +use Cleantalk\Custom\RateLimiter\RateLimiter; use JFactory; class AltCookies @@ -11,6 +13,10 @@ class AltCookies private const SESSION_TABLE__NAME = 'cleantalk_sessions'; + private const LIMITER_NAME = 'alt_cookie_limit'; + + private const LIMITER_LIMIT = 10; // 10 requests per minute allowed + /** * @var string[] */ @@ -81,6 +87,12 @@ public static function get($name) public static function setFromRemote($data) { + $config = new RateLimiterConfig(self::LIMITER_NAME, self::LIMITER_LIMIT, 60); + $rate_limiter = new RateLimiter($config); + if ( ! $rate_limiter->checkPassed() ) { + return ('LIMIT EXCEEDED'); + } + $db = JFactory::getDbo(); $columns = array( 'id', diff --git a/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php index 2d5990c..7df7abc 100644 --- a/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php +++ b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php @@ -79,7 +79,7 @@ protected function increment(RateLimiterDto $uid_data): bool $uid_data->uid ]); - return false !== $this->db_object->fetch($this->db_object->getQuery()); + return false !== $this->db_object->execute($this->db_object->getQuery()); } /** @@ -103,7 +103,7 @@ protected function insert(RateLimiterDto $uid_data): bool $uid_data->last_call ]); - $result = $this->db_object->fetch($this->db_object->getQuery()); + $result = $this->db_object->execute($this->db_object->getQuery()); return false !== $result; } @@ -123,8 +123,27 @@ protected function cleanUp(): bool ] ); - $result = $this->db_object->fetch($this->db_object->getQuery()); + $result = $this->db_object->execute($this->db_object->getQuery()); return false !== $result; } + + /** + * Retrieves rate limit data for the current UID from database + * + * @return RateLimiterDTO|false Rate limit data object or false if not found + */ + public function selectUIDData() + { + $this->db_object->prepare( + ' + SELECT uid, type, ip, ua, counter, last_call, created_at FROM ' . $this->table_name . ' + WHERE uid = %s LIMIT 1; + ', + [$this->uid] + ); + $result = $this->db_object->fetch($this->db_object->getQuery()); + + return !empty($result) ? new RateLimiterDTO($result) : false; + } } From f8fb19566d7be93d12643811e0b0cc3e24a890a1 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Mon, 25 May 2026 16:16:13 +0300 Subject: [PATCH 07/13] Fix. Rate Limiter. Rate limiter insert/increment/cleanUp logics fixed. --- .../Custom/RateLimiter/RateLimiter.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php index 7df7abc..be04476 100644 --- a/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php +++ b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php @@ -68,9 +68,9 @@ protected function increment(RateLimiterDto $uid_data): bool $this->db_object->prepare( ' UPDATE ' . $this->table_name . ' SET - counter = %d, - last_call = %d, - created_at = %d + counter = %s, + last_call = %s, + created_at = %s WHERE uid = %s ', [ $uid_data->counter, @@ -79,7 +79,9 @@ protected function increment(RateLimiterDto $uid_data): bool $uid_data->uid ]); - return false !== $this->db_object->execute($this->db_object->getQuery()); + $result = $this->db_object->execute($this->db_object->getQuery()); + + return false !== $result; } /** @@ -91,15 +93,15 @@ protected function insert(RateLimiterDto $uid_data): bool ' INSERT INTO ' . $this->table_name . ' (uid, type, ip, ua, counter, last_call, created_at) - VALUES (%s, %s, %s, %s, 1, %d, %d) + VALUES (%s, %s, %s, %s, 1, %s, %s) ON DUPLICATE KEY UPDATE last_call = %s, counter = counter + 1; ',[ $uid_data->uid, $uid_data->type, $uid_data->ip, $uid_data->ua, - $uid_data->last_call, - $uid_data->created_at, + $uid_data->last_call, + $uid_data->created_at, $uid_data->last_call ]); @@ -116,7 +118,7 @@ protected function cleanUp(): bool $threshold = $this->current_ts - ($this->config->period + 10); $this->db_object->prepare( - 'DELETE FROM ' . $this->table_name . ' WHERE created_at < %d AND type = %s;', + 'DELETE FROM ' . $this->table_name . ' WHERE created_at < %s AND type = %s;', [ $threshold, $this->config->type From 60343ee2cf0a982da0a4bbefc8463b9d64089c5c Mon Sep 17 00:00:00 2001 From: Glomberg Date: Fri, 29 May 2026 07:57:11 +0300 Subject: [PATCH 08/13] Fix. Rate Limiter. Rate limiter logics fixed. --- lib/Cleantalk/Common/RateLimiter/RateLimiter.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Cleantalk/Common/RateLimiter/RateLimiter.php b/lib/Cleantalk/Common/RateLimiter/RateLimiter.php index 699f48f..2f83075 100644 --- a/lib/Cleantalk/Common/RateLimiter/RateLimiter.php +++ b/lib/Cleantalk/Common/RateLimiter/RateLimiter.php @@ -99,6 +99,10 @@ public function checkPassed(): bool throw new \Exception('UID_DATA_INVALID'); } + if ($record_found && $this->isLocked($uid_data)) { + return false; // Block here by limit exceeded + } + if ($record_found) { if (!$this->increment($uid_data)) { throw new \Exception('INCREMENT_FAILED'); @@ -128,7 +132,7 @@ public function checkPassed(): bool return true; } - return !$this->isLocked($uid_data); + return true; } /** From 25092f9c07ebd2093cb3598e9ef1643d3394fcb0 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Fri, 29 May 2026 08:02:10 +0300 Subject: [PATCH 09/13] Fix. Updater. Updating schema for Rate limiter. --- cleantalkantispam.xml | 2 +- sql/mariadb/install.mariadb.utf8.sql | 10 +++++ sql/mariadb/uninstall.mariadb.utf8.sql | 1 + sql/mariadb/updates/3.3.sql | 12 +++++- sql/mysql/updates/3.3.sql | 12 +++++- sql/sqlsrv/install.mysql.utf8.sql | 51 -------------------------- sql/sqlsrv/install.sqlsrv.utf8.sql | 10 +++++ sql/sqlsrv/uninstall.mysql.utf8.sql | 6 --- sql/sqlsrv/uninstall.sqlsrv.utf8.sql | 1 + sql/sqlsrv/updates/3.3.sql | 12 +++++- 10 files changed, 56 insertions(+), 61 deletions(-) delete mode 100644 sql/sqlsrv/install.mysql.utf8.sql delete mode 100644 sql/sqlsrv/uninstall.mysql.utf8.sql diff --git a/cleantalkantispam.xml b/cleantalkantispam.xml index 177cf8d..d74bc7a 100644 --- a/cleantalkantispam.xml +++ b/cleantalkantispam.xml @@ -7,7 +7,7 @@ GNU/GPLv2 welcome@cleantalk.org cleantalk.org - 3.2.6 + 3.3.0 PLG_SYSTEM_CLEANTALKANTISPAM_DESCRIPTION updater.php diff --git a/sql/mariadb/install.mariadb.utf8.sql b/sql/mariadb/install.mariadb.utf8.sql index 0865ba0..ba1b969 100644 --- a/sql/mariadb/install.mariadb.utf8.sql +++ b/sql/mariadb/install.mariadb.utf8.sql @@ -49,3 +49,13 @@ CREATE TABLE IF NOT EXISTS `#__cleantalk_custom_storage` ( `value` MEDIUMTEXT NULL DEFAULT NULL, PRIMARY KEY (`name`) ); +CREATE TABLE IF NOT EXISTS `#__cleantalk_rate_limits` ( + uid VARCHAR(32) NOT NULL, + type VARCHAR(32) NOT NULL, + ip VARCHAR(45) NOT NULL, + ua VARCHAR(200) NOT NULL, + counter INT NOT NULL DEFAULT 1, + last_call INT NOT NULL, + created_at INT NOT NULL, + PRIMARY KEY (uid) +); diff --git a/sql/mariadb/uninstall.mariadb.utf8.sql b/sql/mariadb/uninstall.mariadb.utf8.sql index 449f811..340b8cd 100644 --- a/sql/mariadb/uninstall.mariadb.utf8.sql +++ b/sql/mariadb/uninstall.mariadb.utf8.sql @@ -4,3 +4,4 @@ DROP TABLE IF EXISTS `#__cleantalk_sessions`; DROP TABLE IF EXISTS `#__cleantalk_ua_bl`; DROP TABLE IF EXISTS `#__cleantalk_usermeta`; DROP TABLE IF EXISTS `#__cleantalk_custom_storage`; +DROP TABLE IF EXISTS `#__cleantalk_rate_limits`; diff --git a/sql/mariadb/updates/3.3.sql b/sql/mariadb/updates/3.3.sql index 13bdd1a..118877d 100644 --- a/sql/mariadb/updates/3.3.sql +++ b/sql/mariadb/updates/3.3.sql @@ -4,4 +4,14 @@ CREATE TABLE IF NOT EXISTS `#__cleantalk_usermeta` ( `meta_key` varchar(255) DEFAULT NULL, `meta_value` longtext DEFAULT NULL, PRIMARY KEY (`id`) -); \ No newline at end of file +); +CREATE TABLE IF NOT EXISTS `#__cleantalk_rate_limits` ( + uid VARCHAR(32) NOT NULL, + type VARCHAR(32) NOT NULL, + ip VARCHAR(45) NOT NULL, + ua VARCHAR(200) NOT NULL, + counter INT NOT NULL DEFAULT 1, + last_call INT NOT NULL, + created_at INT NOT NULL, + PRIMARY KEY (uid) +); diff --git a/sql/mysql/updates/3.3.sql b/sql/mysql/updates/3.3.sql index 13bdd1a..118877d 100644 --- a/sql/mysql/updates/3.3.sql +++ b/sql/mysql/updates/3.3.sql @@ -4,4 +4,14 @@ CREATE TABLE IF NOT EXISTS `#__cleantalk_usermeta` ( `meta_key` varchar(255) DEFAULT NULL, `meta_value` longtext DEFAULT NULL, PRIMARY KEY (`id`) -); \ No newline at end of file +); +CREATE TABLE IF NOT EXISTS `#__cleantalk_rate_limits` ( + uid VARCHAR(32) NOT NULL, + type VARCHAR(32) NOT NULL, + ip VARCHAR(45) NOT NULL, + ua VARCHAR(200) NOT NULL, + counter INT NOT NULL DEFAULT 1, + last_call INT NOT NULL, + created_at INT NOT NULL, + PRIMARY KEY (uid) +); diff --git a/sql/sqlsrv/install.mysql.utf8.sql b/sql/sqlsrv/install.mysql.utf8.sql deleted file mode 100644 index 0865ba0..0000000 --- a/sql/sqlsrv/install.mysql.utf8.sql +++ /dev/null @@ -1,51 +0,0 @@ -CREATE TABLE IF NOT EXISTS `#__cleantalk_sfw` ( - `id` INT(11) NOT NULL AUTO_INCREMENT, - `network` int(11) unsigned NOT NULL, - `mask` int(11) unsigned NOT NULL, - `status` tinyint(1) NOT NULL DEFAULT 0, - `source` tinyint(1) NOT NULL DEFAULT 0, - PRIMARY KEY (`id`), - INDEX ( `network` , `mask` ) -); -CREATE TABLE IF NOT EXISTS `#__cleantalk_sfw_logs` ( - `id` VARCHAR(40) NOT NULL, - `ip` VARCHAR(15) NOT NULL, - `status` ENUM('PASS_SFW','DENY_SFW','PASS_SFW__BY_WHITELIST','PASS_SFW__BY_COOKIE','DENY_ANTICRAWLER','PASS_ANTICRAWLER','DENY_ANTICRAWLER_UA','PASS_ANTICRAWLER_UA','DENY_ANTIFLOOD','PASS_ANTIFLOOD') NULL DEFAULT NULL, - `all_entries` INT NOT NULL, - `blocked_entries` INT NOT NULL, - `entries_timestamp` INT NOT NULL, - `ua_id` INT(11) NULL DEFAULT NULL, - `ua_name` VARCHAR(1024) NOT NULL, - `source` TINYINT NULL DEFAULT NULL, - `network` VARCHAR(20) NULL DEFAULT NULL, - `first_url`VARCHAR(100) NULL DEFAULT NULL, - `last_url` VARCHAR(100) NULL DEFAULT NULL, - PRIMARY KEY (`id`) -); -CREATE TABLE IF NOT EXISTS `#__cleantalk_sessions` ( - `id` varchar(64) NOT NULL, - `name` varchar(40) NOT NULL, - `value` text NULL DEFAULT NULL, - `last_update` datetime NULL DEFAULT NULL, - PRIMARY KEY (`name`(40), `id`(64)) -); -CREATE TABLE IF NOT EXISTS `#__cleantalk_ua_bl` ( - `id` int(11) NOT NULL, - `ua_template` varchar(255) DEFAULT NULL, - `ua_status` tinyint(4) DEFAULT NULL, - PRIMARY KEY (`id`) -); -UPDATE `#__extensions` SET params = '{"ct_check_register":1,"ct_check_contact_forms":1,"check_search":1,"ct_jcomments_check_comments":1,"roles_exclusions":"administrator,super users","ct_set_cookies":1}' -WHERE element = 'cleantalkantispam' AND folder = 'system'; -CREATE TABLE IF NOT EXISTS `#__cleantalk_usermeta` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL, - `meta_key` varchar(255) DEFAULT NULL, - `meta_value` longtext DEFAULT NULL, - PRIMARY KEY (`id`) -); -CREATE TABLE IF NOT EXISTS `#__cleantalk_custom_storage` ( - `name` VARCHAR(100) NOT NULL DEFAULT '', - `value` MEDIUMTEXT NULL DEFAULT NULL, - PRIMARY KEY (`name`) -); diff --git a/sql/sqlsrv/install.sqlsrv.utf8.sql b/sql/sqlsrv/install.sqlsrv.utf8.sql index 0865ba0..ba1b969 100644 --- a/sql/sqlsrv/install.sqlsrv.utf8.sql +++ b/sql/sqlsrv/install.sqlsrv.utf8.sql @@ -49,3 +49,13 @@ CREATE TABLE IF NOT EXISTS `#__cleantalk_custom_storage` ( `value` MEDIUMTEXT NULL DEFAULT NULL, PRIMARY KEY (`name`) ); +CREATE TABLE IF NOT EXISTS `#__cleantalk_rate_limits` ( + uid VARCHAR(32) NOT NULL, + type VARCHAR(32) NOT NULL, + ip VARCHAR(45) NOT NULL, + ua VARCHAR(200) NOT NULL, + counter INT NOT NULL DEFAULT 1, + last_call INT NOT NULL, + created_at INT NOT NULL, + PRIMARY KEY (uid) +); diff --git a/sql/sqlsrv/uninstall.mysql.utf8.sql b/sql/sqlsrv/uninstall.mysql.utf8.sql deleted file mode 100644 index 449f811..0000000 --- a/sql/sqlsrv/uninstall.mysql.utf8.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP TABLE IF EXISTS `#__cleantalk_sfw`; -DROP TABLE IF EXISTS `#__cleantalk_sfw_logs`; -DROP TABLE IF EXISTS `#__cleantalk_sessions`; -DROP TABLE IF EXISTS `#__cleantalk_ua_bl`; -DROP TABLE IF EXISTS `#__cleantalk_usermeta`; -DROP TABLE IF EXISTS `#__cleantalk_custom_storage`; diff --git a/sql/sqlsrv/uninstall.sqlsrv.utf8.sql b/sql/sqlsrv/uninstall.sqlsrv.utf8.sql index 449f811..340b8cd 100644 --- a/sql/sqlsrv/uninstall.sqlsrv.utf8.sql +++ b/sql/sqlsrv/uninstall.sqlsrv.utf8.sql @@ -4,3 +4,4 @@ DROP TABLE IF EXISTS `#__cleantalk_sessions`; DROP TABLE IF EXISTS `#__cleantalk_ua_bl`; DROP TABLE IF EXISTS `#__cleantalk_usermeta`; DROP TABLE IF EXISTS `#__cleantalk_custom_storage`; +DROP TABLE IF EXISTS `#__cleantalk_rate_limits`; diff --git a/sql/sqlsrv/updates/3.3.sql b/sql/sqlsrv/updates/3.3.sql index 13bdd1a..118877d 100644 --- a/sql/sqlsrv/updates/3.3.sql +++ b/sql/sqlsrv/updates/3.3.sql @@ -4,4 +4,14 @@ CREATE TABLE IF NOT EXISTS `#__cleantalk_usermeta` ( `meta_key` varchar(255) DEFAULT NULL, `meta_value` longtext DEFAULT NULL, PRIMARY KEY (`id`) -); \ No newline at end of file +); +CREATE TABLE IF NOT EXISTS `#__cleantalk_rate_limits` ( + uid VARCHAR(32) NOT NULL, + type VARCHAR(32) NOT NULL, + ip VARCHAR(45) NOT NULL, + ua VARCHAR(200) NOT NULL, + counter INT NOT NULL DEFAULT 1, + last_call INT NOT NULL, + created_at INT NOT NULL, + PRIMARY KEY (uid) +); From d3e632da319d3e6b05612e317938b7a78e9915e3 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Fri, 29 May 2026 08:07:39 +0300 Subject: [PATCH 10/13] Fix. Updater. Testing updater. --- plugin-updates.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin-updates.xml b/plugin-updates.xml index c68c30e..417b480 100644 --- a/plugin-updates.xml +++ b/plugin-updates.xml @@ -6,15 +6,15 @@ cleantalkantispam plugin system - 3.2.6 + 3.3.0 - https://github.com/CleanTalk/joomla3.x-4.x-antispam/archive/3.2.6.zip + https://github.com/CleanTalk/joomla3.x-4.x-antispam/archive/refs/heads/Upd-Alt-cookie-rate-limit-VI.zip stable
Updates
- + site From 3b4f6d52e9938319ccc7189749a912a32f6b8ec6 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Fri, 29 May 2026 08:32:25 +0300 Subject: [PATCH 11/13] Fix. Updater. Testing updater. --- sql/mariadb/updates/{3.3.sql => 3.3.0.sql} | 0 sql/mysql/updates/{3.3.sql => 3.3.0.sql} | 0 sql/sqlsrv/updates/{3.3.sql => 3.3.0.sql} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename sql/mariadb/updates/{3.3.sql => 3.3.0.sql} (100%) rename sql/mysql/updates/{3.3.sql => 3.3.0.sql} (100%) rename sql/sqlsrv/updates/{3.3.sql => 3.3.0.sql} (100%) diff --git a/sql/mariadb/updates/3.3.sql b/sql/mariadb/updates/3.3.0.sql similarity index 100% rename from sql/mariadb/updates/3.3.sql rename to sql/mariadb/updates/3.3.0.sql diff --git a/sql/mysql/updates/3.3.sql b/sql/mysql/updates/3.3.0.sql similarity index 100% rename from sql/mysql/updates/3.3.sql rename to sql/mysql/updates/3.3.0.sql diff --git a/sql/sqlsrv/updates/3.3.sql b/sql/sqlsrv/updates/3.3.0.sql similarity index 100% rename from sql/sqlsrv/updates/3.3.sql rename to sql/sqlsrv/updates/3.3.0.sql From 55b1efb546c7144b17fd7cf9c3dde681156e58d0 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Fri, 29 May 2026 08:45:58 +0300 Subject: [PATCH 12/13] Fix. Ajax requests. Ajax handlers response fixed. --- cleantalkantispam.php | 6 +++--- lib/Cleantalk/Custom/AltCookies.php | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cleantalkantispam.php b/cleantalkantispam.php index 45a78fd..28f9cb7 100644 --- a/cleantalkantispam.php +++ b/cleantalkantispam.php @@ -1646,13 +1646,13 @@ public function onAjaxCleantalkantispam() { return json_encode(['allow' => 1, 'msg' => '']); } - return ['error' => 'Not working']; + return json_encode(['error' => 'Not working']); default : - return ['error' => 'Wrong action was provided']; + return json_encode(['error' => 'Wrong action was provided']); } } - return ['error' => 'No action was provided']; + return json_encode(['error' => 'No action was provided']); } //////////////////////////// diff --git a/lib/Cleantalk/Custom/AltCookies.php b/lib/Cleantalk/Custom/AltCookies.php index 5da9426..2b2e8ee 100644 --- a/lib/Cleantalk/Custom/AltCookies.php +++ b/lib/Cleantalk/Custom/AltCookies.php @@ -90,7 +90,8 @@ public static function setFromRemote($data) $config = new RateLimiterConfig(self::LIMITER_NAME, self::LIMITER_LIMIT, 60); $rate_limiter = new RateLimiter($config); if ( ! $rate_limiter->checkPassed() ) { - return ('LIMIT EXCEEDED'); + http_response_code(403); + die(json_encode(['error' => 'LIMIT EXCEEDED'])); } $db = JFactory::getDbo(); From ae942c13cb1f5068c21f4ee90843a9a4dff52790 Mon Sep 17 00:00:00 2001 From: Glomberg Date: Mon, 1 Jun 2026 09:20:51 +0300 Subject: [PATCH 13/13] Fix. Update platform fixed. --- plugin-updates.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin-updates.xml b/plugin-updates.xml index 417b480..2a96e61 100644 --- a/plugin-updates.xml +++ b/plugin-updates.xml @@ -14,7 +14,7 @@ stable
Updates
- + site