UrlGenerator::secureAsset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 1
cp 0
crap 2
rs 10
1
<?php
2
3
/**
4
 * This file is part of Blitz PHP framework.
5
 *
6
 * (c) 2022 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace BlitzPHP\Http;
13
14
use BlitzPHP\Contracts\Router\RouteCollectionInterface;
15
use BlitzPHP\Exceptions\HttpException;
16
use BlitzPHP\Exceptions\RouterException;
17
use BlitzPHP\Session\Store;
18
use BlitzPHP\Traits\Macroable;
19
use BlitzPHP\Utilities\Iterable\Arr;
20
use BlitzPHP\Utilities\String\Text;
21
use Closure;
22
23
/**
24
 * @credit <a href="http://laravel.com">Laravel - \Illuminate\Routing\UrlGenerator</a>
25
 */
26
class UrlGenerator
27
{
28
    use Macroable;
0 ignored issues
show
Bug introduced by
The trait BlitzPHP\Traits\Macroable requires the property $name which is not provided by BlitzPHP\Http\UrlGenerator.
Loading history...
29
30
    /**
31
     * The forced URL root.
32
     */
33
    protected string $forcedRoot = '';
34
35
    /**
36
     * The forced scheme for URLs.
37
     */
38
    protected string $forceScheme = '';
39
40
    /**
41
     * A cached copy of the URL root for the current request.
42
     */
43
    protected ?string $cachedRoot = null;
44
45
    /**
46
     * A cached copy of the URL scheme for the current request.
47
     */
48
    protected ?string $cachedScheme = null;
49
50
    /**
51
     * The root namespace being applied to controller actions.
52
     */
53
    protected string $rootNamespace = '';
54
55
    /**
56
     * The session resolver callable.
57
     *
58
     * @var callable
59
     */
60
    protected $sessionResolver;
61
62
    /**
63
     * The encryption key resolver callable.
64
     *
65
     * @var callable
66
     */
67
    protected $keyResolver;
68
69
    /**
70
     * The callback to use to format hosts.
71
     *
72
     * @var Closure
73
     */
74
    protected $formatHostUsing;
75
76
    /**
77
     * The callback to use to format paths.
78
     *
79
     * @var Closure
80
     */
81
    protected $formatPathUsing;
82
83
    /**
84
     * Create a new URL Generator instance.
85
     *
86
     * @param RouteCollectionInterface $routes    The route collection.
87
     * @param Request                  $request   The request instance.
88
     * @param string|null              $assetRoot The asset root URL.
89
     *
90
     * @return void
91
     */
92
    public function __construct(protected RouteCollectionInterface $routes, protected Request $request, protected ?string $assetRoot = null)
93
    {
94 10
        $this->setRequest($request);
95
    }
96
97
    /**
98
     * Get the full URL for the current request.
99
     */
100
    public function full(): string
101
    {
102 2
        return $this->request->fullUrl();
103
    }
104
105
    /**
106
     * Get the current URL for the request.
107
     */
108
    public function current(): string
109
    {
110
        return $this->to($this->request->getUri()->getPath());
111
    }
112
113
    /**
114
     * Get the URL for the previous request.
115
     *
116
     * @param mixed $fallback
117
     */
118
    public function previous($fallback = false): string
119
    {
120 2
        $referrer = $this->request->getHeaderLine('Referer');
121
122 2
        $url = $referrer !== '' ? $this->to($referrer) : $this->getPreviousUrlFromSession();
123
124
        if ($url !== null && $url !== '') {
125 2
            return $url;
126
        }
127
        if ($fallback) {
128
            return $this->to($fallback);
129
        }
130
131
        return $this->to('/');
132
    }
133
134
    /**
135
     * Get the previous URL from the session if possible.
136
     */
137
    protected function getPreviousUrlFromSession(): ?string
138
    {
139
        return $this->getSession()?->previousUrl();
140
    }
141
142
    /**
143
     * Generate an absolute URL to the given path.
144
     */
145
    public function to(string $path, mixed $extra = [], ?bool $secure = null): string
146
    {
147
        // First we will check if the URL is already a valid URL. If it is we will not
148
        // try to generate a new one but will simply return the URL as is, which is
149
        // convenient since developers do not always have to check if it's valid.
150
        if ($this->isValidUrl($path)) {
151 8
            return $path;
152
        }
153
154
        $tail = implode(
155
            '/',
156
            array_map(
157
                'rawurlencode',
158
                $this->formatParameters($extra)
159
            )
160 4
        );
161
162
        // Once we have the scheme we will compile the "tail" by collapsing the values
163
        // into a single string delimited by slashes. This just makes it convenient
164
        // for passing the array of parameters to this URL as a list of segments.
165 4
        $root = $this->formatRoot($this->formatScheme($secure));
166
167 4
        [$path, $query] = $this->extractQueryString($path);
168
169
        return $this->format(
170
            $root,
171
            '/' . trim($path . '/' . $tail, '/')
172 4
        ) . $query;
173
    }
174
175
    /**
176
     * Generate a secure, absolute URL to the given path.
177
     */
178
    public function secure(string $path, array $parameters = []): string
179
    {
180
        return $this->to($path, $parameters, true);
181
    }
182
183
    /**
184
     * Generate the URL to an application asset.
185
     */
186
    public function asset(string $path, ?bool $secure = null): string
187
    {
188
        if ($this->isValidUrl($path)) {
189
            return $path;
190
        }
191
192
        // Once we get the root URL, we will check to see if it contains an index.php
193
        // file in the paths. If it does, we will remove it since it is not needed
194
        // for asset paths, but only for routes to endpoints in the application.
195
        $root = $this->assetRoot ?: $this->formatRoot($this->formatScheme($secure));
196
197
        return $this->removeIndex($root) . '/' . trim($path, '/');
198
    }
199
200
    /**
201
     * Generate the URL to a secure asset.
202
     */
203
    public function secureAsset(string $path): string
204
    {
205
        return $this->asset($path, true);
206
    }
207
208
    /**
209
     * Generate the URL to an asset from a custom root domain such as CDN, etc.
210
     */
211
    public function assetFrom(string $root, string $path, ?bool $secure = null): string
212
    {
213
        // Once we get the root URL, we will check to see if it contains an index.php
214
        // file in the paths. If it does, we will remove it since it is not needed
215
        // for asset paths, but only for routes to endpoints in the application.
216
        $root = $this->formatRoot($this->formatScheme($secure), $root);
217
218
        return $this->removeIndex($root) . '/' . trim($path, '/');
219
    }
220
221
    /**
222
     * Remove the index.php file from a path.
223
     */
224
    protected function removeIndex(string $root): string
225
    {
226
        $i = 'index.php';
227
228
        return Text::contains($root, /** @scrutinizer ignore-type */ $i) ? str_replace('/' . $i, '', $root) : $root;
229
    }
230
231
    /**
232
     * Get the default scheme for a raw URL.
233
     */
234
    public function formatScheme(?bool $secure = null): string
235
    {
236
        if (null !== $secure) {
237 2
            return $secure ? 'https://' : 'http://';
238
        }
239
240
        if (null === $this->cachedScheme) {
241 4
            $this->cachedScheme = $this->forceScheme ?: $this->request->getScheme() . '://';
242
        }
243
244 4
        return $this->cachedScheme;
245
    }
246
247
    /**
248
     * Get the URL to a named route.
249
     */
250
    public function route(string $name, array $parameters = [], bool $absolute = true): string
251
    {
252
        if (false === $route = $this->routes->reverseRoute($name, ...$parameters)) {
253 4
            throw HttpException::invalidRedirectRoute($name);
254
        }
255
256 4
        return $absolute ? site_url($route) : $route;
257
    }
258
259
    /**
260
     * Get the URL to a controller action.
261
     *
262
     * @return false|string
263
     */
264
    public function action(array|string $action, array $parameters = [], bool $absolute = true)
265
    {
266
        if (is_array($action)) {
0 ignored issues
show
introduced by
The condition is_array($action) is always true.
Loading history...
267 2
            $action = implode('::', $action);
268
        }
269
270 2
        $route = $this->routes->reverseRoute($action, ...$parameters);
271
272
        if (! $route) {
273 2
            throw RouterException::actionNotDefined($action);
274
        }
275
276 2
        return $absolute ? site_url($route) : $route;
277
    }
278
279
    /**
280
     * Format the array of URL parameters.
281
     */
282
    public function formatParameters(mixed $parameters): array
283
    {
284 4
        return Arr::wrap($parameters);
285
    }
286
287
    /**
288
     * Extract the query string from the given path.
289
     */
290
    protected function extractQueryString(string $path): array
291
    {
292
        if (($queryPosition = strpos($path, '?')) !== false) {
293
            return [
294
                substr($path, 0, $queryPosition),
295
                substr($path, $queryPosition),
296 4
            ];
297
        }
298
299 4
        return [$path, ''];
300
    }
301
302
    /**
303
     * Get the base URL for the request.
304
     */
305
    public function formatRoot(string $scheme, ?string $root = null): string
306
    {
307
        if (null === $root) {
308
            if (null === $this->cachedRoot) {
309 4
                $this->cachedRoot = $this->forcedRoot ?: $this->request->root();
310
            }
311
312 4
            $root = $this->cachedRoot;
313
        }
314
315 4
        $start = Text::startsWith($root, /** @scrutinizer ignore-type */ 'http://') ? 'http://' : 'https://';
316
317 4
        return preg_replace('~' . $start . '~', $scheme, $root, 1);
318
    }
319
320
    /**
321
     * Format the given URL segments into a single URL.
322
     */
323
    public function format(string $root, string $path, mixed $route = null): string
324
    {
325 4
        $path = '/' . trim($path, '/');
326
327
        if ($this->formatHostUsing) {
328 4
            $root = ($this->formatHostUsing)($root, $route);
329
        }
330
331
        if ($this->formatPathUsing) {
332 4
            $path = ($this->formatPathUsing)($path, $route);
333
        }
334
335 4
        return trim($root . $path, '/');
336
    }
337
338
    /**
339
     * Determine if the given path is a valid URL.
340
     */
341
    public function isValidUrl(string $path): bool
342
    {
343
        if (! preg_match('~^(#|//|https?://|(mailto|tel|sms):)~', $path)) {
344 4
            return filter_var($path, FILTER_VALIDATE_URL) !== false;
345
        }
346
347 8
        return true;
348
    }
349
350
    /**
351
     * Force the scheme for URLs.
352
     */
353
    public function forceScheme(?string $scheme): void
354
    {
355
        $this->cachedScheme = null;
356
357
        $this->forceScheme = $scheme ? $scheme . '://' : null;
358
    }
359
360
    /**
361
     * Set the forced root URL.
362
     */
363
    public function forceRootUrl(?string $root): void
364
    {
365
        $this->forcedRoot = $root ? rtrim($root, '/') : null;
366
367
        $this->cachedRoot = null;
368
    }
369
370
    /**
371
     * Set a callback to be used to format the host of generated URLs.
372
     *
373
     * @return $this
374
     */
375
    public function formatHostUsing(Closure $callback)
376
    {
377
        $this->formatHostUsing = $callback;
378
379
        return $this;
380
    }
381
382
    /**
383
     * Set a callback to be used to format the path of generated URLs.
384
     *
385
     * @return $this
386
     */
387
    public function formatPathUsing(Closure $callback)
388
    {
389
        $this->formatPathUsing = $callback;
390
391
        return $this;
392
    }
393
394
    /**
395
     * Get the path formatter being used by the URL generator.
396
     *
397
     * @return Closure
398
     */
399
    public function pathFormatter()
400
    {
401
        return $this->formatPathUsing ?: static fn ($path) => $path;
402
    }
403
404
    /**
405
     * Get the request instance.
406
     */
407
    public function getRequest(): Request
408
    {
409 10
        return $this->request;
410
    }
411
412
    /**
413
     * Set the current request instance.
414
     */
415
    public function setRequest(Request $request): self
416
    {
417 10
        $this->request = $request;
418
419 10
        $this->cachedRoot   = null;
420 10
        $this->cachedScheme = null;
421
422 10
        return $this;
423
    }
424
425
    /**
426
     * Set the route collection.
427
     */
428
    public function setRoutes(RouteCollectionInterface $routes): self
429
    {
430
        $this->routes = $routes;
431
432
        return $this;
433
    }
434
435
    /**
436
     * Get the session implementation from the resolver.
437
     */
438
    protected function getSession(): ?Store
439
    {
440
        if ($this->sessionResolver) {
441
            return ($this->sessionResolver)();
442
        }
443
444
        return session();
445
    }
446
447
    /**
448
     * Set the session resolver for the generator.
449
     *
450
     * @return $this
451
     */
452
    public function setSessionResolver(callable $sessionResolver)
453
    {
454
        $this->sessionResolver = $sessionResolver;
455
456
        return $this;
457
    }
458
459
    /**
460
     * Set the encryption key resolver.
461
     *
462
     * @return $this
463
     */
464
    public function setKeyResolver(callable $keyResolver)
465
    {
466
        $this->keyResolver = $keyResolver;
467
468
        return $this;
469
    }
470
471
    /**
472
     * Set the root controller namespace.
473
     *
474
     * @param string $rootNamespace
475
     *
476
     * @return $this
477
     */
478
    public function setRootControllerNamespace($rootNamespace)
479
    {
480
        $this->rootNamespace = $rootNamespace;
481
482
        return $this;
483
    }
484
}
485