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

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

343 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 FluentBoards\Framework\Http;
4
5 use InvalidArgumentException;
6 use FluentBoards\Framework\Foundation\App;
7 use FluentBoards\Framework\Support\DateTime;
8
9 class UrlGenerator
10 {
11 /**
12 * The application instance.
13 *
14 * @var \FluentBoards\Framework\Foundation\App|null
15 */
16 protected $app = null;
17
18
19 /**
20 * The encrypter instance.
21 * @var \FluentBoards\Framework\Encryption\Encrypter|null
22 */
23 protected $encrypter = null;
24
25 /**
26 * Create a new URL Generator instance.
27 *
28 * @param \FluentBoards\Framework\Foundation\App|null $app
29 * @param \FluentBoards\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 public function validateExpiryTime(array $params): array
118 {
119 if (!isset($params['expires_at'])) {
120 return $params; // Nothing to validate
121 }
122
123 $expiresAt = $params['expires_at'];
124
125 // Convert string date/time to timestamp
126 if (is_string($expiresAt)) {
127 $expiresAt = strtotime($expiresAt);
128 if ($expiresAt === false) {
129 throw new InvalidArgumentException(
130 'The expiry time string is invalid.'
131 );
132 }
133
134 // Convert DateTime object to timestamp
135 } elseif ($expiresAt instanceof DateTime) {
136 $expiresAt = $expiresAt->getTimestamp();
137
138 // Numeric values are treated as absolute timestamps
139 } elseif (is_numeric($expiresAt)) {
140 $expiresAt = (int) $expiresAt;
141
142 // Anything else is invalid
143 } else {
144 throw new InvalidArgumentException(
145 'The expiry time must be a string, DateTime, or numeric timestamp.'
146 );
147 }
148
149 // Check if the expiry time is in the past
150 if ($expiresAt <= time()) {
151 throw new InvalidArgumentException('The expiry time has already passed.');
152 }
153
154 $params['expires_at'] = $expiresAt;
155
156 return $params;
157 }
158
159 /**
160 * Validate a URL
161 *
162 * @param string $url
163 * @return mixed (false or array)
164 */
165 public function validate($url)
166 {
167 if (!$query = $this->parseUrlAndGetQuery($url)) {
168 return false;
169 }
170
171 if (!isset($query['_data']) || !isset($query['_signature'])) {
172 return false;
173 }
174
175 return $this->verifySignature($query['_data'], $query['_signature']);
176 }
177
178 /**
179 * Parse query string from the url.
180 *
181 * @param string $url
182 * @return mixed (bool or array)
183 */
184 public function parseUrlAndGetQuery($url)
185 {
186 $parts = parse_url($url);
187
188 if (!isset($parts['query'])) {
189 return false;
190 }
191
192 parse_str($parts['query'], $query);
193
194 return $query;
195 }
196
197 /**
198 * Verify the signature.
199 *
200 * @param array $data
201 * @param string $signature
202 * @return mixed (bool or array)
203 */
204 public function verifySignature($data, $signature)
205 {
206 $encrypter = $this->resolveEncrypter();
207
208 $expected = hash_hmac('sha256', $data, $encrypter->getKey());
209
210 if (!hash_equals($expected, $signature)) return false;
211
212 parse_str($encrypter->decrypt($data), $params);
213
214 $expiresAt = $params['expires_at'] ?? null;
215
216 if (is_numeric($expiresAt) && time() > (int) $expiresAt) {
217 return false;
218 }
219
220 return empty($params) ? true : $params;
221 }
222
223 /**
224 * Generate a full REST URL from a named route.
225 *
226 * @param string $nameOrPath The name of the route or path.
227 * @param array $params Optional parameters to fill in the placeholders.
228 * @param array $query Optional query parameters to append to the URL.
229 * @return string|null The full REST URL or null if the route doesn't exist.
230 */
231 public function route($nameOrPath, $params = [], $query = [])
232 {
233 // @phpstan-ignore-next-line
234 $route = $this->app->router->getByName($nameOrPath);
235
236 if (!$route) {
237 if (!str_contains($nameOrPath, '/')) {
238 return;
239 }
240 $path = '/' . trim($nameOrPath, '/');
241 } else {
242 $path = $this->buildPath($route->uri(), $params);
243 }
244
245 $fullUrl = $this->buildFullUrl($path);
246
247 return $this->appendQueryString($fullUrl, $query);
248 }
249
250 /**
251 * Resolve the encrypter.
252 *
253 * @return \FluentBoards\Framework\Encryption\Encrypter
254 */
255 protected function resolveEncrypter()
256 {
257 if (!$this->encrypter) {
258 $this->encrypter = App::make('encrypter');
259 }
260
261 return $this->encrypter;
262 }
263
264 /**
265 * Build the full URL including base REST path,
266 * namespace, version, and route path.
267 *
268 * @param string $path
269 * @return string
270 */
271 protected function buildFullUrl($path)
272 {
273 $restUrl = $this->buildRestBaseUrl();
274
275 $namespaceSegment = $this->buildNamespaceSegment();
276
277 return $restUrl . $namespaceSegment . $path;
278 }
279
280 /**
281 * Get the base REST URL (e.g., https://wpfluent.org/wp-json).
282 *
283 * @return string
284 */
285 protected function buildRestBaseUrl()
286 {
287 return rtrim(site_url('/wp-json'), '/');
288 }
289
290 /**
291 * Build the namespace and version segment of the REST URL.
292 *
293 * @return string
294 */
295 protected function buildNamespaceSegment()
296 {
297 // @phpstan-ignore-next-line
298 $ns = trim($this->app->config->get('app.rest_namespace'), '/');
299
300 // @phpstan-ignore-next-line
301 $ver = trim($this->app->config->get('app.rest_version'), '/');
302
303 return "/{$ns}/{$ver}";
304 }
305
306 /**
307 * Replace route placeholders in the URI with the provided parameters.
308 *
309 * @param string $template
310 * @param array $params
311 * @return string
312 */
313 protected function buildPath($template, &$params)
314 {
315 $replaced = preg_replace_callback(
316 '/\{([^}]+)\??\}/',
317 function ($m) use (&$params) {
318 $key = rtrim($m[1], '?');
319 return isset($params[$key]) ? $params[$key] : '';
320 },
321 $template
322 );
323
324 return '/' . trim($replaced, '/');
325 }
326
327 /**
328 * Append query parameters to a URL.
329 *
330 * @param string $url
331 * @param array $query
332 * @return string
333 */
334 protected function appendQueryString($url, $query)
335 {
336 if (empty($query)) {
337 return $url;
338 }
339
340 return $url . '?' . http_build_query($query);
341 }
342 }
343