PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.1.1
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.1.1
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / vendor / wpfluent / framework / src / WPFluent / Http / Middleware / RateLimiter.php

RateLimiter.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.1.1, at vendor/wpfluent/framework/src/WPFluent/Http/Middleware/RateLimiter.php

200 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 FluentBooking\Framework\Http\Middleware;
4
5 use FluentBooking\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 \FluentBooking\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 \FluentBooking\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 \FluentBooking\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 return $settings ?: ['count' => 0, 'firstTime' => $currentTime];
134 }
135
136 /**
137 * Check if interval expired.
138 *
139 * @param array $settings
140 * @param int $currentTime
141 * @return bool
142 */
143 protected function isIntervalExpired($settings, $currentTime)
144 {
145 return ($currentTime - $settings['firstTime']) > $this->interval;
146 }
147
148 /**
149 * Reset rate limit for a new interval.
150 *
151 * @param int $currentTime
152 * @return array
153 */
154 protected function resetRateLimit($currentTime)
155 {
156 return ['count' => 1, 'firstTime' => $currentTime];
157 }
158
159 /**
160 * Check if limit exceeded.
161 *
162 * @param array $settings
163 * @return bool
164 */
165 protected function isRateLimitExceeded($settings)
166 {
167 return $settings['count'] > $this->limit;
168 }
169
170 /**
171 * Update transient with proper TTL.
172 *
173 * @param \FluentBooking\Framework\Http\Request\Request $request
174 * @param array $settings
175 * @param int $currentTime
176 */
177 protected function updateSettings($request, $settings, $currentTime)
178 {
179 $ttl = $this->interval - ($currentTime - $settings['firstTime']);
180 set_transient(
181 $this->makeTransientKey($request),
182 $settings,
183 max(1, $ttl)
184 );
185 }
186
187 /**
188 * Generate a unique transient key per IP + endpoint.
189 *
190 * @param \FluentBooking\Framework\Http\Request\Request $request
191 * @return string
192 */
193 protected function makeTransientKey($request)
194 {
195 $slug = App::config()->get('app.slug');
196 $endpoint = $request->getRoute() ?: 'unknown';
197 return "{$slug}_rate_limit_" . md5($request->getIp() . '|' . $endpoint);
198 }
199 }
200