diff --git a/cleantalkantispam.php b/cleantalkantispam.php
index b1c6913..28f9cb7 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.
@@ -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']);
}
////////////////////////////
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/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..2f83075
--- /dev/null
+++ b/lib/Cleantalk/Common/RateLimiter/RateLimiter.php
@@ -0,0 +1,227 @@
+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;
+}
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/AltCookies.php b/lib/Cleantalk/Custom/AltCookies.php
index facc7aa..2b2e8ee 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,13 @@ 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() ) {
+ http_response_code(403);
+ die(json_encode(['error' => 'LIMIT EXCEEDED']));
+ }
+
$db = JFactory::getDbo();
$columns = array(
'id',
diff --git a/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php
new file mode 100644
index 0000000..be04476
--- /dev/null
+++ b/lib/Cleantalk/Custom/RateLimiter/RateLimiter.php
@@ -0,0 +1,151 @@
+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 = %s,
+ last_call = %s,
+ created_at = %s
+ WHERE uid = %s
+ ', [
+ $uid_data->counter,
+ $uid_data->last_call,
+ $uid_data->created_at,
+ $uid_data->uid
+ ]);
+
+ $result = $this->db_object->execute($this->db_object->getQuery());
+
+ return false !== $result;
+ }
+
+ /**
+ * @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, %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
+ ]);
+
+ $result = $this->db_object->execute($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 < %s AND type = %s;',
+ [
+ $threshold,
+ $this->config->type
+ ]
+ );
+
+ $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;
+ }
+}
diff --git a/plugin-updates.xml b/plugin-updates.xml
index c68c30e..2a96e61 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
-
+
site
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.0.sql b/sql/mariadb/updates/3.3.0.sql
new file mode 100644
index 0000000..118877d
--- /dev/null
+++ b/sql/mariadb/updates/3.3.0.sql
@@ -0,0 +1,17 @@
+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_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/updates/3.3.sql b/sql/mariadb/updates/3.3.sql
deleted file mode 100644
index 13bdd1a..0000000
--- a/sql/mariadb/updates/3.3.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-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`)
-);
\ No newline at end of file
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`;
diff --git a/sql/mysql/updates/3.3.0.sql b/sql/mysql/updates/3.3.0.sql
new file mode 100644
index 0000000..118877d
--- /dev/null
+++ b/sql/mysql/updates/3.3.0.sql
@@ -0,0 +1,17 @@
+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_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
deleted file mode 100644
index 13bdd1a..0000000
--- a/sql/mysql/updates/3.3.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-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`)
-);
\ No newline at end of file
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.0.sql b/sql/sqlsrv/updates/3.3.0.sql
new file mode 100644
index 0000000..118877d
--- /dev/null
+++ b/sql/sqlsrv/updates/3.3.0.sql
@@ -0,0 +1,17 @@
+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_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/updates/3.3.sql b/sql/sqlsrv/updates/3.3.sql
deleted file mode 100644
index 13bdd1a..0000000
--- a/sql/sqlsrv/updates/3.3.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-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`)
-);
\ No newline at end of file