PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 1.95
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v1.95
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Http / Middleware / RateLimiter.php

RateLimiter.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 1.95, at vendor/wpfluent/framework/src/WPFluent/Http/Middleware/RateLimiter.php

203 lines 5.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\Framework\Http\Middleware;
4
5 use FluentBoards\Framework\Foundation\App;
6
7 /**
8 * Class RateLimiter
9 *
10 * Handles per-IP and per-endpoint rate limiting for REST requests.
11 *
12 * Features:
13 * - IP + endpoint aware
14 * - Retry-After header for 429 responses
15 * - X-RateLimit-Limit and X-RateLimit-Remaining headers
16 * - Optional bypass for admin users
17 * - Transient-based storage, safe for PHP 7.4+
18 */
19 class RateLimiter
20 {
21 /**
22 * Maximum requests allowed per interval.
23 *
24 * @var int
25 */
26 protected $limit;
27
28 /**
29 * Interval in seconds for rate limiting.
30 *
31 * @var int
32 */
33 protected $interval;
34
35 /**
36 * Constructor.
37 *
38 * @param int $limit
39 * @param int $interval
40 */
41 public function __construct($limit, $interval)
42 {
43 $this->limit = (int) $limit;
44 $this->interval = (int) $interval;
45 }
46
47 /**
48 * Handle incoming request.
49 *
50 * @param \FluentBoards\Framework\Http\Request\Request $request
51 * @param callable $next
52 * @return mixed
53 */
54 public function handle($request, $next)
55 {
56 // Bypass safe requests or admin users
57 if ($this->shouldAllow($request)) {
58 return $next($request);
59 }
60
61 $currentTime = time();
62 $settings = $this->getSettings($request, $currentTime);
63
64 // Reset interval if expired, otherwise increment
65 if ($this->isIntervalExpired($settings, $currentTime)) {
66 $settings = $this->resetRateLimit($currentTime);
67 } else {
68 $settings['count']++;
69 }
70
71 // Update transient with correct TTL
72 $this->updateSettings($request, $settings, $currentTime);
73
74 // Check if limit exceeded
75 if ($this->isRateLimitExceeded($settings)) {
76 $retryAfter = $this->interval - ($currentTime - $settings['firstTime']);
77 $response = $request->abort(429, 'Too many requests.');
78 $response->header('Retry-After', max(1, $retryAfter));
79 $response->header('X-RateLimit-Limit', $this->limit);
80 $response->header('X-RateLimit-Remaining', 0);
81 return $response;
82 }
83
84 // Inject rate limit headers into the actual route response via WP hook,
85 // because $next() in a before-middleware returns bool (permission check
86 // result), not the WP_REST_Response produced by the route callback.
87 $limit = $this->limit;
88 $remaining = max(0, $this->limit - $settings['count']);
89
90 add_filter('rest_post_dispatch', function ($response) use ($limit, $remaining) {
91 $response->header('X-RateLimit-Limit', $limit);
92 $response->header('X-RateLimit-Remaining', $remaining);
93 return $response;
94 });
95
96 return $next($request);
97 }
98
99 /**
100 * Determine if request should bypass rate limiting.
101 *
102 * @param \FluentBoards\Framework\Http\Request\Request $request
103 * @return bool
104 */
105 protected function shouldAllow($request)
106 {
107 return $this->isCookieAuthenticated() || in_array(
108 $request->method(),
109 ['HEAD', 'OPTIONS']
110 );
111 }
112
113 /**
114 * Check if user is authenticated in admin (optional bypass).
115 *
116 * @return bool
117 */
118 protected function isCookieAuthenticated()
119 {
120 return is_user_logged_in() && !empty($GLOBALS['wp_rest_auth_cookie']);
121 }
122
123 /**
124 * Get current rate limit settings from transient.
125 *
126 * @param \FluentBoards\Framework\Http\Request\Request $request
127 * @param int $currentTime
128 * @return array
129 */
130 protected function getSettings($request, $currentTime)
131 {
132 $settings = get_transient($this->makeTransientKey($request));
133
134 return $settings ?: ['count' => 0, 'firstTime' => $currentTime];
135 }
136
137 /**
138 * Check if interval expired.
139 *
140 * @param array $settings
141 * @param int $currentTime
142 * @return bool
143 */
144 protected function isIntervalExpired($settings, $currentTime)
145 {
146 return ($currentTime - $settings['firstTime']) > $this->interval;
147 }
148
149 /**
150 * Reset rate limit for a new interval.
151 *
152 * @param int $currentTime
153 * @return array
154 */
155 protected function resetRateLimit($currentTime)
156 {
157 return ['count' => 1, 'firstTime' => $currentTime];
158 }
159
160 /**
161 * Check if limit exceeded.
162 *
163 * @param array $settings
164 * @return bool
165 */
166 protected function isRateLimitExceeded($settings)
167 {
168 return $settings['count'] > $this->limit;
169 }
170
171 /**
172 * Update transient with proper TTL.
173 *
174 * @param \FluentBoards\Framework\Http\Request\Request $request
175 * @param array $settings
176 * @param int $currentTime
177 */
178 protected function updateSettings($request, $settings, $currentTime)
179 {
180 $ttl = $this->interval - ($currentTime - $settings['firstTime']);
181
182 set_transient(
183 $this->makeTransientKey($request),
184 $settings,
185 max(1, $ttl)
186 );
187 }
188
189 /**
190 * Generate a unique transient key per IP + endpoint.
191 *
192 * @param \FluentBoards\Framework\Http\Request\Request $request
193 * @return string
194 */
195 protected function makeTransientKey($request)
196 {
197 $slug = App::config()->get('app.slug');
198
199 $endpoint = $request->getRoute() ?: 'unknown';
200
201 return "{$slug}_rate_limit_" . md5($request->getIp() . '|' . $endpoint);
202 }
203 }