Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions cleantalkantispam.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -1645,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']);
}

////////////////////////////
Expand Down
2 changes: 1 addition & 1 deletion cleantalkantispam.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<license>GNU/GPLv2</license>
<authorEmail>welcome@cleantalk.org</authorEmail>
<authorUrl>cleantalk.org</authorUrl>
<version>3.2.6</version>
<version>3.3.0</version>
<description>PLG_SYSTEM_CLEANTALKANTISPAM_DESCRIPTION</description>
<scriptfile>updater.php</scriptfile>
<files>
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
227 changes: 227 additions & 0 deletions lib/Cleantalk/Common/RateLimiter/RateLimiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
<?php

namespace Cleantalk\Common\RateLimiter;

/**
* Abstract base class for rate limiting functionality
* Defines the core rate limiting logic and required methods for implementation
*/
abstract class RateLimiter
{
/**
* Rate limiter configuration object
*
* @var RateLimiterConfig
*/
protected $config;

/**
* Current timestamp
*
* @var int
*/
protected $current_ts;

/**
* Unique identifier for the current request
*
* @var string
*/
protected $uid;

/**
* IP address of the current request
*
* @var string
*/
protected $ip;

/**
* User agent of the current request
*
* @var string
*/
protected $ua;

/**
* Flag indicating if the process completed successfully
*
* @var bool
*/
public $process_ok = true;

/**
* RateLimiter constructor.
*
* @param RateLimiterConfig $config Configuration for the rate limiter
*/
public function __construct(RateLimiterConfig $config)
{
$this->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 && $this->isLocked($uid_data)) {
return false; // Block here by limit exceeded
}

if ($record_found) {
if (!$this->increment($uid_data)) {
throw new \Exception('INCREMENT_FAILED');
}
} else {
$uid_data = new RateLimiterDTO(
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 true;
}

/**
* Checks if the current UID has exceeded the rate limit
*
* @param RateLimiterDTO $uid_data
* @return bool True if rate limited, false otherwise
*/
protected function isLocked(RateLimiterDTO $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 RateLimiterDTO|false Rate limit data or false if not found
*/
protected function selectUIDData()
{
return new RateLimiterDTO(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 RateLimiterDTO $uid_data
* @return bool True on success, false on failure
*/
abstract protected function increment(RateLimiterDTO $uid_data): bool;

/**
* Inserts a new rate limit record
* Must be implemented by child classes
* @param RateLimiterDTO $uid_data
* @return bool True on success, false on failure
*/
abstract protected function insert(RateLimiterDTO $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;
}
50 changes: 50 additions & 0 deletions lib/Cleantalk/Common/RateLimiter/RateLimiterConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Cleantalk\Common\RateLimiter;

/**
* Configuration class for rate limiter settings
*
* Holds the configuration parameters that define how rate limiting should be applied
* for a specific type of request (e.g., login attempts, comment submissions, etc.)
*/
class RateLimiterConfig
{
/**
* Type of rate limit (e.g., 'login', 'comment', 'registration')
* Used to differentiate between different kinds of rate-limited actions
*
* @var string|null
*/
public $type = null;

/**
* Maximum number of allowed requests within the configured period
* Once this limit is exceeded, further requests will be rate limited
*
* @var int
*/
public $limit = 5;

/**
* Time period in seconds during which the request limit applies
* For example, a period of 60 with limit 10 means 10 requests per minute
*
* @var int
*/
public $period = 5;

/**
* RateLimiterConfig constructor.
*
* @param string $type The type of rate-limited action
* @param int $limit Maximum number of allowed requests
* @param int $period Time period in seconds for the limit
*/
public function __construct(string $type, int $limit, int $period)
{
$this->type = $type;
$this->limit = $limit;
$this->period = $period;
}
}
Loading