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 / UrlGenerator.php

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

345 lines 8.8 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;
4
5 use InvalidArgumentException;
6 use FluentBooking\Framework\Foundation\App;
7 use FluentBooking\Framework\Support\DateTime;
8
9 class UrlGenerator
10 {
11 /**
12 * The application instance.
13 *
14 * @var \FluentBooking\Framework\Foundation\App|null
15 */
16 protected $app = null;
17
18
19 /**
20 * The encrypter instance.
21 * @var \FluentBooking\Framework\Encryption\Encrypter|null
22 */
23 protected $encrypter = null;
24
25 /**
26 * Create a new URL Generator instance.
27 *
28 * @param \FluentBooking\Framework\Foundation\App|null $app
29 * @param \FluentBooking\Framework\Encryption\Encrypter|null $encrypter
30 */
31 public function __construct($app = null, $encrypter = null)
32 {
33 $this->app = $app ?: App::getInstance();
34 $this->encrypter = $encrypter;
35 }
36
37 /**
38 * Sign a URL
39 *
40 * @param string $url
41 * @param array $params
42 * @return string
43 */
44 public function sign($url, $params = [])
45 {
46 $encrypter = $this->resolveEncrypter();
47
48 $url = $this->normalizeUrl($url);
49
50 [$baseUrl, $query] = $this->extractUrlParts($url);
51
52 $params = $this->validateExpiryTime(array_merge($query, $params));
53
54 $payload = $encrypter->encrypt(http_build_query($params));
55
56 $signature = hash_hmac('sha256', $payload, $encrypter->getKey());
57
58 // URL-encode via http_build_query so payload/signature stay safe
59 // even if their alphabet ever includes '+', '/', or '='. Old-style
60 // URLs (raw values) keep verifying because parse_str on the verify
61 // side decodes both forms identically.
62 return $baseUrl . '?' . http_build_query([
63 '_data' => $payload,
64 '_signature' => $signature,
65 ]);
66 }
67
68 /**
69 * Normalize URL — handle relative routes, slugs, and REST routes.
70 *
71 * @param string $url
72 * @return string
73 */
74 protected function normalizeUrl($url)
75 {
76 if (preg_match('#^(http|https)://#', $url)) {
77 return $url;
78 }
79
80 $config = App::config();
81 $slug = trim($config->get('app.slug'), '/');
82 $version = trim($config->get('app.rest_version'), '/');
83 $relative = ltrim($url, '/');
84
85 $base = rest_url();
86
87 if (str_contains($base, 'index.php?rest_route=')) {
88 $base = site_url('/wp-json/');
89 }
90
91 return rtrim($base, '/') . '/' . $slug . '/' . $version . '/' . $relative;
92 }
93
94 /**
95 * Extract base URL and query array from a full URL.
96 *
97 * @param string $url
98 * @return array
99 */
100 protected function extractUrlParts($url)
101 {
102 $parts = parse_url($url);
103
104 $base = $parts['scheme'] . '://' . $parts['host'] . ($parts['path'] ?? '');
105 parse_str($parts['query'] ?? '', $query);
106
107 return [$base, $query];
108 }
109
110 /**
111 * Normalize the expiry time.
112 *
113 * @param array $params
114 * @return array
115 * @throws InvalidArgumentException
116 *
117 * @phpstan-ignore-next-line
118 */
119 public function validateExpiryTime(array $params): array
120 {
121 if (!isset($params['expires_at'])) {
122 return $params; // Nothing to validate
123 }
124
125 $expiresAt = $params['expires_at'];
126
127 // Convert string date/time to timestamp
128 if (is_string($expiresAt)) {
129 $expiresAt = strtotime($expiresAt);
130 if ($expiresAt === false) {
131 throw new InvalidArgumentException(
132 'The expiry time string is invalid.'
133 );
134 }
135
136 // Convert DateTime object to timestamp
137 } elseif ($expiresAt instanceof DateTime) {
138 $expiresAt = $expiresAt->getTimestamp();
139
140 // Numeric values are treated as absolute timestamps
141 } elseif (is_numeric($expiresAt)) {
142 $expiresAt = (int) $expiresAt;
143
144 // Anything else is invalid
145 } else {
146 throw new InvalidArgumentException(
147 'The expiry time must be a string, DateTime, or numeric timestamp.'
148 );
149 }
150
151 // Check if the expiry time is in the past
152 if ($expiresAt <= time()) {
153 throw new InvalidArgumentException('The expiry time has already passed.');
154 }
155
156 $params['expires_at'] = $expiresAt;
157
158 return $params;
159 }
160
161 /**
162 * Validate a URL
163 *
164 * @param string $url
165 * @return mixed (false or array)
166 */
167 public function validate($url)
168 {
169 if (!$query = $this->parseUrlAndGetQuery($url)) {
170 return false;
171 }
172
173 if (!isset($query['_data']) || !isset($query['_signature'])) {
174 return false;
175 }
176
177 return $this->verifySignature($query['_data'], $query['_signature']);
178 }
179
180 /**
181 * Parse query string from the url.
182 *
183 * @param string $url
184 * @return mixed (bool or array)
185 */
186 public function parseUrlAndGetQuery($url)
187 {
188 $parts = parse_url($url);
189
190 if (!isset($parts['query'])) {
191 return false;
192 }
193
194 parse_str($parts['query'], $query);
195
196 return $query;
197 }
198
199 /**
200 * Verify the signature.
201 *
202 * @param array $data
203 * @param string $signature
204 * @return mixed (bool or array)
205 */
206 public function verifySignature($data, $signature)
207 {
208 $encrypter = $this->resolveEncrypter();
209
210 $expected = hash_hmac('sha256', $data, $encrypter->getKey());
211
212 if (!hash_equals($expected, $signature)) return false;
213
214 parse_str($encrypter->decrypt($data), $params);
215
216 $expiresAt = $params['expires_at'] ?? null;
217
218 if (is_numeric($expiresAt) && time() > (int) $expiresAt) {
219 return false;
220 }
221
222 return empty($params) ? true : $params;
223 }
224
225 /**
226 * Generate a full REST URL from a named route.
227 *
228 * @param string $nameOrPath The name of the route or path.
229 * @param array $params Optional parameters to fill in the placeholders.
230 * @param array $query Optional query parameters to append to the URL.
231 * @return string|null The full REST URL or null if the route doesn't exist.
232 */
233 public function route($nameOrPath, $params = [], $query = [])
234 {
235 // @phpstan-ignore-next-line
236 $route = $this->app->router->getByName($nameOrPath);
237
238 if (!$route) {
239 if (!str_contains($nameOrPath, '/')) {
240 return;
241 }
242 $path = '/' . trim($nameOrPath, '/');
243 } else {
244 $path = $this->buildPath($route->uri(), $params);
245 }
246
247 $fullUrl = $this->buildFullUrl($path);
248
249 return $this->appendQueryString($fullUrl, $query);
250 }
251
252 /**
253 * Resolve the encrypter.
254 *
255 * @return \FluentBooking\Framework\Encryption\Encrypter
256 */
257 protected function resolveEncrypter()
258 {
259 if (!$this->encrypter) {
260 $this->encrypter = App::make('encrypter');
261 }
262
263 return $this->encrypter;
264 }
265
266 /**
267 * Build the full URL including base REST path,
268 * namespace, version, and route path.
269 *
270 * @param string $path
271 * @return string
272 */
273 protected function buildFullUrl($path)
274 {
275 $restUrl = $this->buildRestBaseUrl();
276
277 $namespaceSegment = $this->buildNamespaceSegment();
278
279 return $restUrl . $namespaceSegment . $path;
280 }
281
282 /**
283 * Get the base REST URL (e.g., https://wpfluent.org/wp-json).
284 *
285 * @return string
286 */
287 protected function buildRestBaseUrl()
288 {
289 return rtrim(site_url('/wp-json'), '/');
290 }
291
292 /**
293 * Build the namespace and version segment of the REST URL.
294 *
295 * @return string
296 */
297 protected function buildNamespaceSegment()
298 {
299 // @phpstan-ignore-next-line
300 $ns = trim($this->app->config->get('app.rest_namespace'), '/');
301
302 // @phpstan-ignore-next-line
303 $ver = trim($this->app->config->get('app.rest_version'), '/');
304
305 return "/{$ns}/{$ver}";
306 }
307
308 /**
309 * Replace route placeholders in the URI with the provided parameters.
310 *
311 * @param string $template
312 * @param array $params
313 * @return string
314 */
315 protected function buildPath($template, &$params)
316 {
317 $replaced = preg_replace_callback(
318 '/\{([^}]+)\??\}/',
319 function ($m) use (&$params) {
320 $key = rtrim($m[1], '?');
321 return isset($params[$key]) ? $params[$key] : '';
322 },
323 $template
324 );
325
326 return '/' . trim($replaced, '/');
327 }
328
329 /**
330 * Append query parameters to a URL.
331 *
332 * @param string $url
333 * @param array $query
334 * @return string
335 */
336 protected function appendQueryString($url, $query)
337 {
338 if (empty($query)) {
339 return $url;
340 }
341
342 return $url . '?' . http_build_query($query);
343 }
344 }
345