PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.7.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.7.0
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 2.7.0, at includes/api/traits/trait-rate-limiter.php

156 lines 4.8 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) {
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 // Forwarded headers are client-controlled: trusting them by default
103 // lets a caller mint a fresh rate-limit bucket per request. Only
104 // consult them when the site opts in because a trusted proxy/CDN
105 // sits in front and REMOTE_ADDR is the proxy, not the client.
106 $trusted_headers = apply_filters('thinkrank_trusted_ip_headers', []);
107
108 foreach ((array) $trusted_headers as $header) {
109 if (empty($_SERVER[$header])) {
110 continue;
111 }
112 $ip = sanitize_text_field(wp_unslash($_SERVER[$header]));
113
114 // X-Forwarded-For can contain a comma-separated chain.
115 if (strpos($ip, ',') !== false) {
116 $ip = trim(explode(',', $ip)[0]);
117 }
118
119 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
120 return $ip;
121 }
122 }
123
124 return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '0.0.0.0';
125 }
126
127 /**
128 * Get rate limit status for a user/endpoint
129 *
130 * @since 1.0.0
131 *
132 * @param string $endpoint_name Endpoint identifier
133 * @param int $user_id User ID (0 for anonymous)
134 * @return array Rate limit status
135 */
136 protected function get_rate_limit_status(string $endpoint_name, int $user_id = 0): array {
137 $identifier = $user_id > 0 ? "user_{$user_id}" : $this->get_client_ip();
138
139 $minute_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 60);
140 $hour_key = "thinkrank_rate_limit_{$endpoint_name}_{$identifier}_" . floor(time() / 3600);
141
142 $minute_attempts = get_transient($minute_key) ?: 0;
143 $hour_attempts = get_transient($hour_key) ?: 0;
144
145 return [
146 'minute_attempts' => $minute_attempts,
147 'minute_limit' => $this->rate_limit_per_minute,
148 'minute_remaining' => max(0, $this->rate_limit_per_minute - $minute_attempts),
149 'hour_attempts' => $hour_attempts,
150 'hour_limit' => $this->rate_limit_per_hour,
151 'hour_remaining' => max(0, $this->rate_limit_per_hour - $hour_attempts),
152 'reset_time' => (floor(time() / 60) + 1) * 60 // Next minute
153 ];
154 }
155 }
156