188 lines
4.7 KiB
PHP
188 lines
4.7 KiB
PHP
<?php
|
|
/**
|
|
* Rate Limiter for Maple Local Fonts.
|
|
*
|
|
* @package Maple_Local_Fonts
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class MLF_Rate_Limiter
|
|
*
|
|
* Provides rate limiting functionality to prevent abuse of AJAX endpoints.
|
|
*/
|
|
class MLF_Rate_Limiter {
|
|
|
|
/**
|
|
* Default rate limit (requests per window).
|
|
*
|
|
* @var int
|
|
*/
|
|
private $limit = 10;
|
|
|
|
/**
|
|
* Default time window in seconds.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $window = 60;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param int $limit Maximum requests allowed per window.
|
|
* @param int $window Time window in seconds.
|
|
*/
|
|
public function __construct($limit = 10, $window = 60) {
|
|
$this->limit = absint($limit);
|
|
$this->window = absint($window);
|
|
}
|
|
|
|
/**
|
|
* Check if the current user/IP is rate limited.
|
|
*
|
|
* @param string $action The action being rate limited.
|
|
* @return bool True if rate limited (should block), false if allowed.
|
|
*/
|
|
public function is_limited($action) {
|
|
$key = $this->get_rate_limit_key($action);
|
|
$data = get_transient($key);
|
|
|
|
if ($data === false) {
|
|
// First request, not limited
|
|
return false;
|
|
}
|
|
|
|
return $data['count'] >= $this->limit;
|
|
}
|
|
|
|
/**
|
|
* Record a request for rate limiting purposes.
|
|
*
|
|
* @param string $action The action being recorded.
|
|
* @return void
|
|
*/
|
|
public function record_request($action) {
|
|
$key = $this->get_rate_limit_key($action);
|
|
$data = get_transient($key);
|
|
|
|
if ($data === false) {
|
|
// First request in this window
|
|
$data = [
|
|
'count' => 1,
|
|
'start' => time(),
|
|
];
|
|
} else {
|
|
$data['count']++;
|
|
}
|
|
|
|
// Set/update transient with remaining window time
|
|
$elapsed = time() - $data['start'];
|
|
$remaining = max(1, $this->window - $elapsed);
|
|
|
|
set_transient($key, $data, $remaining);
|
|
}
|
|
|
|
/**
|
|
* Check rate limit and record request in one call.
|
|
*
|
|
* @param string $action The action being checked.
|
|
* @return bool True if request is allowed, false if rate limited.
|
|
*/
|
|
public function check_and_record($action) {
|
|
if ($this->is_limited($action)) {
|
|
return false;
|
|
}
|
|
|
|
$this->record_request($action);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the number of remaining requests in the current window.
|
|
*
|
|
* @param string $action The action to check.
|
|
* @return int Number of remaining requests.
|
|
*/
|
|
public function get_remaining($action) {
|
|
$key = $this->get_rate_limit_key($action);
|
|
$data = get_transient($key);
|
|
|
|
if ($data === false) {
|
|
return $this->limit;
|
|
}
|
|
|
|
return max(0, $this->limit - $data['count']);
|
|
}
|
|
|
|
/**
|
|
* Get the rate limit key for the current user/IP.
|
|
*
|
|
* @param string $action The action being rate limited.
|
|
* @return string Transient key.
|
|
*/
|
|
private function get_rate_limit_key($action) {
|
|
// Use user ID if logged in, otherwise IP
|
|
$user_id = get_current_user_id();
|
|
|
|
if ($user_id > 0) {
|
|
$identifier = 'user_' . $user_id;
|
|
} else {
|
|
// Sanitize and hash IP for privacy
|
|
$ip = $this->get_client_ip();
|
|
$identifier = 'ip_' . md5($ip);
|
|
}
|
|
|
|
return 'mlf_rate_' . sanitize_key($action) . '_' . $identifier;
|
|
}
|
|
|
|
/**
|
|
* Get the client IP address.
|
|
*
|
|
* @return string Client IP address.
|
|
*/
|
|
private function get_client_ip() {
|
|
$ip = '';
|
|
|
|
// Check for various headers (in order of reliability)
|
|
$headers = [
|
|
'HTTP_CLIENT_IP',
|
|
'HTTP_X_FORWARDED_FOR',
|
|
'HTTP_X_FORWARDED',
|
|
'HTTP_X_CLUSTER_CLIENT_IP',
|
|
'HTTP_FORWARDED_FOR',
|
|
'HTTP_FORWARDED',
|
|
'REMOTE_ADDR',
|
|
];
|
|
|
|
foreach ($headers as $header) {
|
|
if (!empty($_SERVER[$header])) {
|
|
// X-Forwarded-For can contain multiple IPs, get the first one
|
|
$ips = explode(',', sanitize_text_field(wp_unslash($_SERVER[$header])));
|
|
$ip = trim($ips[0]);
|
|
|
|
// Validate IP
|
|
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to localhost if no valid IP found
|
|
return $ip ?: '127.0.0.1';
|
|
}
|
|
|
|
/**
|
|
* Clear rate limit for a specific action and user/IP.
|
|
*
|
|
* @param string $action The action to clear.
|
|
* @return void
|
|
*/
|
|
public function clear($action) {
|
|
$key = $this->get_rate_limit_key($action);
|
|
delete_transient($key);
|
|
}
|
|
}
|