PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.27
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.27
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / vendor / wpfluent / framework / src / WPFluent / Http / UrlGenerator.php

UrlGenerator.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.27, at vendor/wpfluent/framework/src/WPFluent/Http/UrlGenerator.php

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