PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.2
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / api / traits / trait-rate-limiter.php

trait-rate-limiter.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.2, at includes/api/traits/trait-rate-limiter.php

163 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Rate Limiter Trait
4 *
5 * Provides basic rate limiting functionality for API endpoints
6 * to prevent abuse and DoS attacks.
7 *
8 * @package ThinkRank
9 * @subpackage API\Traits
10 * @since 1.0.0
11 */
12
13 declare(strict_types=1);
14
15 namespace ThinkRank\API\Traits;
16
17 use WP_Error;
18
19 // Prevent direct access
20 if (!defined('ABSPATH')) {
21 exit;
22 }
23
24 /**
25 * Rate Limiter Trait
26 *
27 * Simple rate limiting implementation using WordPress transients
28 * for API endpoint protection.
29 *
30 * @since 1.0.0
31 */
32 trait Rate_Limiter {
33
34 /**
35 * Rate limit per minute
36 *
37 * @since 1.0.0
38 * @var int
39 */
40 private int $rate_limit_per_minute = 60;
41
42 /**
43 * Rate limit per hour
44 *
45 * @since 1.0.0
46 * @var int
47 */
48 private int $rate_limit_per_hour = 1000;
49
50 /**
51 * Check if request is within rate limits
52 *
53 * @since 1.0.0
54 *
55 * @param string $endpoint_name Endpoint identifier
56 * @param int $user_id User ID (0 for anonymous)
57 * @return true|WP_Error True if within limits, WP_Error if exceeded
58 */
59 protected function check_rate_limit(string $endpoint_name, int $user_id = 0): bool|WP_Error {
60 // Get user identifier (IP for anonymous, user ID for authenticated)
61 $identifier = $user_id > 0 ? "user_{$user_id}" : $this->get_client_ip();
62
63 // Check minute-based rate limit
64 $minute_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 60);
65 $minute_attempts = get_transient($minute_key) ?: 0;
66
67 if ($minute_attempts >= $this->rate_limit_per_minute) {
68 return new WP_Error(
69 'rate_limit_exceeded',
70 'Too many requests per minute. Please slow down.',
71 ['status' => 429]
72 );
73 }
74
75 // Check hour-based rate limit
76 $hour_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 3600);
77 $hour_attempts = get_transient($hour_key) ?: 0;
78
79 if ($hour_attempts >= $this->rate_limit_per_hour) {
80 return new WP_Error(
81 'rate_limit_exceeded',
82 'Too many requests per hour. Please try again later.',
83 ['status' => 429]
84 );
85 }
86
87 // Increment counters
88 set_transient($minute_key, $minute_attempts + 1, 60);
89 set_transient($hour_key, $hour_attempts + 1, 3600);
90
91 return true;
92 }
93
94 /**
95 * Get client IP address
96 *
97 * @since 1.0.0
98 *
99 * @return string Client IP address
100 */
101 private function get_client_ip(): string {
102 // Check for various headers that might contain the real IP
103 $headers = [
104 'HTTP_CF_CONNECTING_IP', // Cloudflare
105 'HTTP_CLIENT_IP', // Proxy
106 'HTTP_X_FORWARDED_FOR', // Load balancer/proxy
107 'HTTP_X_FORWARDED', // Proxy
108 'HTTP_X_CLUSTER_CLIENT_IP', // Cluster
109 'HTTP_FORWARDED_FOR', // Proxy
110 'HTTP_FORWARDED', // Proxy
111 'REMOTE_ADDR' // Standard
112 ];
113
114 foreach ($headers as $header) {
115 if (!empty($_SERVER[$header])) {
116 $ip = $_SERVER[$header];
117
118 // Handle comma-separated IPs (X-Forwarded-For can contain multiple IPs)
119 if (strpos($ip, ',') !== false) {
120 $ip = trim(explode(',', $ip)[0]);
121 }
122
123 // Validate IP address
124 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
125 return $ip;
126 }
127 }
128 }
129
130 // Fallback to REMOTE_ADDR even if it's a private IP
131 return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
132 }
133
134 /**
135 * Get rate limit status for a user/endpoint
136 *
137 * @since 1.0.0
138 *
139 * @param string $endpoint_name Endpoint identifier
140 * @param int $user_id User ID (0 for anonymous)
141 * @return array Rate limit status
142 */
143 protected function get_rate_limit_status(string $endpoint_name, int $user_id = 0): array {
144 $identifier = $user_id > 0 ? "user_{$user_id}" : $this->get_client_ip();
145
146 $minute_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 60);
147 $hour_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 3600);
148
149 $minute_attempts = get_transient($minute_key) ?: 0;
150 $hour_attempts = get_transient($hour_key) ?: 0;
151
152 return [
153 'minute_attempts' => $minute_attempts,
154 'minute_limit' => $this->rate_limit_per_minute,
155 'minute_remaining' => max(0, $this->rate_limit_per_minute - $minute_attempts),
156 'hour_attempts' => $hour_attempts,
157 'hour_limit' => $this->rate_limit_per_hour,
158 'hour_remaining' => max(0, $this->rate_limit_per_hour - $hour_attempts),
159 'reset_time' => (floor(time() / 60) + 1) * 60 // Next minute
160 ];
161 }
162 }
163