Completed
Push — master ( c57c11...1c0994 )
by Mark
04:07 queued 10s
created

Router::_applyUrlFilters()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 10
nop 1
dl 0
loc 30
rs 8.8177
c 0
b 0
f 0
1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         0.2.9
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\Routing;
16
17
use Cake\Core\Configure;
18
use Cake\Http\ServerRequest;
19
use Cake\Routing\Exception\MissingRouteException;
20
use Cake\Utility\Inflector;
21
use Exception;
22
use Psr\Http\Message\ServerRequestInterface;
23
use ReflectionFunction;
24
use ReflectionMethod;
25
use RuntimeException;
26
use Throwable;
27
28
/**
29
 * Parses the request URL into controller, action, and parameters. Uses the connected routes
30
 * to match the incoming URL string to parameters that will allow the request to be dispatched. Also
31
 * handles converting parameter lists into URL strings, using the connected routes. Routing allows you to decouple
32
 * the way the world interacts with your application (URLs) and the implementation (controllers and actions).
33
 *
34
 * ### Connecting routes
35
 *
36
 * Connecting routes is done using Router::connect(). When parsing incoming requests or reverse matching
37
 * parameters, routes are enumerated in the order they were connected. For more information on routes and
38
 * how to connect them see Router::connect().
39
 */
40
class Router
41
{
42
43
    /**
44
     * Have routes been loaded
45
     *
46
     * @var bool
47
     * @deprecated 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0
48
     */
49
    public static $initialized = false;
50
51
    /**
52
     * Default route class.
53
     *
54
     * @var string
55
     */
56
    protected static $_defaultRouteClass = 'Cake\Routing\Route\Route';
57
58
    /**
59
     * Contains the base string that will be applied to all generated URLs
60
     * For example `https://example.com`
61
     *
62
     * @var string
63
     */
64
    protected static $_fullBaseUrl;
65
66
    /**
67
     * Regular expression for action names
68
     *
69
     * @var string
70
     */
71
    const ACTION = 'index|show|add|create|edit|update|remove|del|delete|view|item';
72
73
    /**
74
     * Regular expression for years
75
     *
76
     * @var string
77
     */
78
    const YEAR = '[12][0-9]{3}';
79
80
    /**
81
     * Regular expression for months
82
     *
83
     * @var string
84
     */
85
    const MONTH = '0[1-9]|1[012]';
86
87
    /**
88
     * Regular expression for days
89
     *
90
     * @var string
91
     */
92
    const DAY = '0[1-9]|[12][0-9]|3[01]';
93
94
    /**
95
     * Regular expression for auto increment IDs
96
     *
97
     * @var string
98
     */
99
    const ID = '[0-9]+';
100
101
    /**
102
     * Regular expression for UUIDs
103
     *
104
     * @var string
105
     */
106
    const UUID = '[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}';
107
108
    /**
109
     * The route collection routes would be added to.
110
     *
111
     * @var \Cake\Routing\RouteCollection
112
     */
113
    protected static $_collection;
114
115
    /**
116
     * A hash of request context data.
117
     *
118
     * @var array
119
     */
120
    protected static $_requestContext = [];
121
122
    /**
123
     * Named expressions
124
     *
125
     * @var array
126
     */
127
    protected static $_namedExpressions = [
128
        'Action' => Router::ACTION,
129
        'Year' => Router::YEAR,
130
        'Month' => Router::MONTH,
131
        'Day' => Router::DAY,
132
        'ID' => Router::ID,
133
        'UUID' => Router::UUID
134
    ];
135
136
    /**
137
     * Maintains the request object stack for the current request.
138
     * This will contain more than one request object when requestAction is used.
139
     *
140
     * @var array
141
     */
142
    protected static $_requests = [];
143
144
    /**
145
     * Initial state is populated the first time reload() is called which is at the bottom
146
     * of this file. This is a cheat as get_class_vars() returns the value of static vars even if they
147
     * have changed.
148
     *
149
     * @var array
150
     */
151
    protected static $_initialState = [];
152
153
    /**
154
     * The stack of URL filters to apply against routing URLs before passing the
155
     * parameters to the route collection.
156
     *
157
     * @var callable[]
158
     */
159
    protected static $_urlFilters = [];
160
161
    /**
162
     * Default extensions defined with Router::extensions()
163
     *
164
     * @var array
165
     */
166
    protected static $_defaultExtensions = [];
167
168
    /**
169
     * Get or set default route class.
170
     *
171
     * @param string|null $routeClass Class name.
172
     * @return string|null
173
     */
174
    public static function defaultRouteClass($routeClass = null)
175
    {
176
        if ($routeClass === null) {
177
            return static::$_defaultRouteClass;
178
        }
179
        static::$_defaultRouteClass = $routeClass;
180
    }
181
182
    /**
183
     * Gets the named route patterns for use in config/routes.php
184
     *
185
     * @return array Named route elements
186
     * @see \Cake\Routing\Router::$_namedExpressions
187
     */
188
    public static function getNamedExpressions()
189
    {
190
        return static::$_namedExpressions;
191
    }
192
193
    /**
194
     * Connects a new Route in the router.
195
     *
196
     * Compatibility proxy to \Cake\Routing\RouteBuilder::connect() in the `/` scope.
197
     *
198
     * @param string $route A string describing the template of the route
199
     * @param array|string $defaults An array describing the default route parameters. These parameters will be used by default
200
     *   and can supply routing parameters that are not dynamic. See above.
201
     * @param array $options An array matching the named elements in the route to regular expressions which that
202
     *   element should match. Also contains additional parameters such as which routed parameters should be
203
     *   shifted into the passed arguments, supplying patterns for routing parameters and supplying the name of a
204
     *   custom routing class.
205
     * @return void
206
     * @throws \Cake\Core\Exception\Exception
207
     * @see \Cake\Routing\RouteBuilder::connect()
208
     * @see \Cake\Routing\Router::scope()
209
     */
210
    public static function connect($route, $defaults = [], $options = [])
211
    {
212
        static::$initialized = true;
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
213
        static::scope('/', function ($routes) use ($route, $defaults, $options) {
214
            /** @var \Cake\Routing\RouteBuilder $routes */
215
            $routes->connect($route, $defaults, $options);
216
        });
217
    }
218
219
    /**
220
     * Connects a new redirection Route in the router.
221
     *
222
     * Compatibility proxy to \Cake\Routing\RouteBuilder::redirect() in the `/` scope.
223
     *
224
     * @param string $route A string describing the template of the route
225
     * @param array|string $url A URL to redirect to. Can be a string or a Cake array-based URL
226
     * @param array $options An array matching the named elements in the route to regular expressions which that
227
     *   element should match. Also contains additional parameters such as which routed parameters should be
228
     *   shifted into the passed arguments. As well as supplying patterns for routing parameters.
229
     * @return void
230
     * @see \Cake\Routing\RouteBuilder::redirect()
231
     * @deprecated 3.3.0 Use Router::scope() and RouteBuilder::redirect() instead.
232
     */
233
    public static function redirect($route, $url, $options = [])
234
    {
235
        deprecationWarning(
236
            'Router::redirect() is deprecated. ' .
237
            'Use Router::scope() and RouteBuilder::redirect() instead.'
238
        );
239
        if (is_string($url)) {
240
            $url = ['redirect' => $url];
241
        }
242
        if (!isset($options['routeClass'])) {
243
            $options['routeClass'] = 'Cake\Routing\Route\RedirectRoute';
244
        }
245
        static::connect($route, $url, $options);
246
    }
247
248
    /**
249
     * Generate REST resource routes for the given controller(s).
250
     *
251
     * Compatibility proxy to \Cake\Routing\RouteBuilder::resources(). Additional, compatibility
252
     * around prefixes and plugins and prefixes is handled by this method.
253
     *
254
     * A quick way to generate a default routes to a set of REST resources (controller(s)).
255
     *
256
     * ### Usage
257
     *
258
     * Connect resource routes for an app controller:
259
     *
260
     * ```
261
     * Router::mapResources('Posts');
262
     * ```
263
     *
264
     * Connect resource routes for the Comment controller in the
265
     * Comments plugin:
266
     *
267
     * ```
268
     * Router::mapResources('Comments.Comment');
269
     * ```
270
     *
271
     * Plugins will create lower_case underscored resource routes. e.g
272
     * `/comments/comment`
273
     *
274
     * Connect resource routes for the Posts controller in the
275
     * Admin prefix:
276
     *
277
     * ```
278
     * Router::mapResources('Posts', ['prefix' => 'admin']);
279
     * ```
280
     *
281
     * Prefixes will create lower_case underscored resource routes. e.g
282
     * `/admin/posts`
283
     *
284
     * ### Options:
285
     *
286
     * - 'id' - The regular expression fragment to use when matching IDs. By default, matches
287
     *    integer values and UUIDs.
288
     * - 'prefix' - Routing prefix to use for the generated routes. Defaults to ''.
289
     *   Using this option will create prefixed routes, similar to using Routing.prefixes.
290
     * - 'only' - Only connect the specific list of actions.
291
     * - 'actions' - Override the method names used for connecting actions.
292
     * - 'map' - Additional resource routes that should be connected. If you define 'only' and 'map',
293
     *   make sure that your mapped methods are also in the 'only' list.
294
     * - 'path' - Change the path so it doesn't match the resource name. E.g ArticlesController
295
     *   is available at `/posts`
296
     *
297
     * @param string|array $controller A controller name or array of controller names (i.e. "Posts" or "ListItems")
298
     * @param array $options Options to use when generating REST routes
299
     * @see \Cake\Routing\RouteBuilder::resources()
300
     * @deprecated 3.3.0 Use Router::scope() and RouteBuilder::resources() instead.
301
     * @return void
302
     */
303
    public static function mapResources($controller, $options = [])
304
    {
305
        deprecationWarning(
306
            'Router::mapResources() is deprecated. ' .
307
            'Use Router::scope() and RouteBuilder::resources() instead.'
308
        );
309
        foreach ((array)$controller as $name) {
310
            list($plugin, $name) = pluginSplit($name);
311
312
            $prefix = $pluginUrl = false;
313
            if (!empty($options['prefix'])) {
314
                $prefix = $options['prefix'];
315
                unset($options['prefix']);
316
            }
317
            if ($plugin) {
318
                $pluginUrl = Inflector::underscore($plugin);
319
            }
320
321
            $callback = function ($routes) use ($name, $options) {
322
                /** @var \Cake\Routing\RouteBuilder $routes */
323
                $routes->resources($name, $options);
324
            };
325
326
            if ($plugin && $prefix) {
327
                $path = '/' . implode('/', [$prefix, $pluginUrl]);
328
                $params = ['prefix' => $prefix, 'plugin' => $plugin];
329
                static::scope($path, $params, $callback);
330
331
                return;
332
            }
333
334
            if ($prefix) {
335
                static::prefix($prefix, $callback);
336
337
                return;
338
            }
339
340
            if ($plugin) {
341
                static::plugin($plugin, $callback);
342
343
                return;
344
            }
345
346
            static::scope('/', $callback);
347
348
            return;
349
        }
350
    }
351
352
    /**
353
     * Parses given URL string. Returns 'routing' parameters for that URL.
354
     *
355
     * @param string $url URL to be parsed.
356
     * @param string $method The HTTP method being used.
357
     * @return array Parsed elements from URL.
358
     * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
359
     * @deprecated 3.4.0 Use Router::parseRequest() instead.
360
     */
361
    public static function parse($url, $method = '')
362
    {
363
        deprecationWarning(
364
            'Router::parse() is deprecated. ' .
365
            'Use Router::parseRequest() instead. This will require adopting the Http\Server library.'
366
        );
367
        if (!static::$initialized) {
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
368
            static::_loadRoutes();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Routing\Router::_loadRoutes() has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
369
        }
370
        if (strpos($url, '/') !== 0) {
371
            $url = '/' . $url;
372
        }
373
374
        return static::$_collection->parse($url, $method);
375
    }
376
377
    /**
378
     * Get the routing parameters for the request is possible.
379
     *
380
     * @param \Psr\Http\Message\ServerRequestInterface $request The request to parse request data from.
381
     * @return array Parsed elements from URL.
382
     * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
383
     */
384
    public static function parseRequest(ServerRequestInterface $request)
385
    {
386
        if (!static::$initialized) {
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
387
            static::_loadRoutes();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Routing\Router::_loadRoutes() has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
388
        }
389
390
        return static::$_collection->parseRequest($request);
391
    }
392
393
    /**
394
     * Takes parameter and path information back from the Dispatcher, sets these
395
     * parameters as the current request parameters that are merged with URL arrays
396
     * created later in the request.
397
     *
398
     * Nested requests will create a stack of requests. You can remove requests using
399
     * Router::popRequest(). This is done automatically when using Object::requestAction().
400
     *
401
     * Will accept either a Cake\Http\ServerRequest object or an array of arrays. Support for
402
     * accepting arrays may be removed in the future.
403
     *
404
     * @param \Cake\Http\ServerRequest|array $request Parameters and path information or a Cake\Http\ServerRequest object.
405
     * @return void
406
     * @deprecatd 3.6.0 Support for arrays will be removed in 4.0.0
407
     */
408
    public static function setRequestInfo($request)
409
    {
410
        if ($request instanceof ServerRequest) {
411
            static::pushRequest($request);
412
        } else {
413
            deprecationWarning(
414
                'Passing an array into Router::setRequestInfo() is deprecated. ' .
415
                'Pass an instance of ServerRequest instead.'
416
            );
417
418
            $requestData = $request;
419
            $requestData += [[], []];
420
            $requestData[0] += [
421
                'controller' => false,
422
                'action' => false,
423
                'plugin' => null
424
            ];
425
            $request = new ServerRequest([
426
                'params' => $requestData[0],
427
                'url' => isset($requestData[1]['here']) ? $requestData[1]['here'] : '/',
428
                'base' => isset($requestData[1]['base']) ? $requestData[1]['base'] : '',
429
                'webroot' => isset($requestData[1]['webroot']) ? $requestData[1]['webroot'] : '/',
430
            ]);
431
            static::pushRequest($request);
432
        }
433
    }
434
435
    /**
436
     * Push a request onto the request stack. Pushing a request
437
     * sets the request context used when generating URLs.
438
     *
439
     * @param \Cake\Http\ServerRequest $request Request instance.
440
     * @return void
441
     */
442
    public static function pushRequest(ServerRequest $request)
443
    {
444
        static::$_requests[] = $request;
445
        static::setRequestContext($request);
446
    }
447
448
    /**
449
     * Store the request context for a given request.
450
     *
451
     * @param \Psr\Http\Message\ServerRequestInterface $request The request instance.
452
     * @return void
453
     * @throws \InvalidArgumentException When parameter is an incorrect type.
454
     */
455
    public static function setRequestContext(ServerRequestInterface $request)
456
    {
457
        $uri = $request->getUri();
458
        static::$_requestContext = [
459
            '_base' => $request->getAttribute('base'),
460
            '_port' => $uri->getPort(),
461
            '_scheme' => $uri->getScheme(),
462
            '_host' => $uri->getHost(),
463
        ];
464
    }
465
466
    /**
467
     * Pops a request off of the request stack. Used when doing requestAction
468
     *
469
     * @return \Cake\Http\ServerRequest The request removed from the stack.
470
     * @see \Cake\Routing\Router::pushRequest()
471
     * @see \Cake\Routing\RequestActionTrait::requestAction()
472
     */
473
    public static function popRequest()
474
    {
475
        $removed = array_pop(static::$_requests);
476
        $last = end(static::$_requests);
477
        if ($last) {
478
            static::setRequestContext($last);
479
            reset(static::$_requests);
480
        }
481
482
        return $removed;
483
    }
484
485
    /**
486
     * Get the current request object, or the first one.
487
     *
488
     * @param bool $current True to get the current request, or false to get the first one.
489
     * @return \Cake\Http\ServerRequest|null
490
     */
491
    public static function getRequest($current = false)
492
    {
493
        if ($current) {
494
            $request = end(static::$_requests);
495
496
            return $request ?: null;
497
        }
498
499
        return isset(static::$_requests[0]) ? static::$_requests[0] : null;
500
    }
501
502
    /**
503
     * Reloads default Router settings. Resets all class variables and
504
     * removes all connected routes.
505
     *
506
     * @return void
507
     */
508
    public static function reload()
509
    {
510
        if (empty(static::$_initialState)) {
511
            static::$_collection = new RouteCollection();
512
            static::$_initialState = get_class_vars(get_called_class());
513
514
            return;
515
        }
516
        foreach (static::$_initialState as $key => $val) {
517
            if ($key !== '_initialState') {
518
                static::${$key} = $val;
519
            }
520
        }
521
        static::$_collection = new RouteCollection();
522
    }
523
524
    /**
525
     * Reset routes and related state.
526
     *
527
     * Similar to reload() except that this doesn't reset all global state,
528
     * as that leads to incorrect behavior in some plugin test case scenarios.
529
     *
530
     * This method will reset:
531
     *
532
     * - routes
533
     * - URL Filters
534
     * - the initialized property
535
     *
536
     * Extensions and default route classes will not be modified
537
     *
538
     * @internal
539
     * @return void
540
     */
541
    public static function resetRoutes()
542
    {
543
        static::$_collection = new RouteCollection();
544
        static::$_urlFilters = [];
545
        static::$initialized = false;
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
546
    }
547
548
    /**
549
     * Add a URL filter to Router.
550
     *
551
     * URL filter functions are applied to every array $url provided to
552
     * Router::url() before the URLs are sent to the route collection.
553
     *
554
     * Callback functions should expect the following parameters:
555
     *
556
     * - `$params` The URL params being processed.
557
     * - `$request` The current request.
558
     *
559
     * The URL filter function should *always* return the params even if unmodified.
560
     *
561
     * ### Usage
562
     *
563
     * URL filters allow you to easily implement features like persistent parameters.
564
     *
565
     * ```
566
     * Router::addUrlFilter(function ($params, $request) {
567
     *  if ($request->getParam('lang') && !isset($params['lang'])) {
568
     *    $params['lang'] = $request->getParam('lang');
569
     *  }
570
     *  return $params;
571
     * });
572
     * ```
573
     *
574
     * @param callable $function The function to add
575
     * @return void
576
     */
577
    public static function addUrlFilter(callable $function)
578
    {
579
        static::$_urlFilters[] = $function;
580
    }
581
582
    /**
583
     * Applies all the connected URL filters to the URL.
584
     *
585
     * @param array $url The URL array being modified.
586
     * @return array The modified URL.
587
     * @see \Cake\Routing\Router::url()
588
     * @see \Cake\Routing\Router::addUrlFilter()
589
     */
590
    protected static function _applyUrlFilters($url)
591
    {
592
        $request = static::getRequest(true);
593
        $e = null;
594
        foreach (static::$_urlFilters as $filter) {
595
            try {
596
                $url = $filter($url, $request);
597
            } catch (Exception $e) {
598
                // fall through
599
            } catch (Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
600
                // fall through
601
            }
602
            if ($e !== null) {
603
                if (is_array($filter)) {
604
                    $ref = new ReflectionMethod($filter[0], $filter[1]);
605
                } else {
606
                    $ref = new ReflectionFunction($filter);
607
                }
608
                $message = sprintf(
609
                    'URL filter defined in %s on line %s could not be applied. The filter failed with: %s',
610
                    $ref->getFileName(),
611
                    $ref->getStartLine(),
612
                    $e->getMessage()
613
                );
614
                throw new RuntimeException($message, $e->getCode(), $e);
615
            }
616
        }
617
618
        return $url;
619
    }
620
621
    /**
622
     * Finds URL for specified action.
623
     *
624
     * Returns a URL pointing to a combination of controller and action.
625
     *
626
     * ### Usage
627
     *
628
     * - `Router::url('/posts/edit/1');` Returns the string with the base dir prepended.
629
     *   This usage does not use reverser routing.
630
     * - `Router::url(['controller' => 'posts', 'action' => 'edit']);` Returns a URL
631
     *   generated through reverse routing.
632
     * - `Router::url(['_name' => 'custom-name', ...]);` Returns a URL generated
633
     *   through reverse routing. This form allows you to leverage named routes.
634
     *
635
     * There are a few 'special' parameters that can change the final URL string that is generated
636
     *
637
     * - `_base` - Set to false to remove the base path from the generated URL. If your application
638
     *   is not in the root directory, this can be used to generate URLs that are 'cake relative'.
639
     *   cake relative URLs are required when using requestAction.
640
     * - `_scheme` - Set to create links on different schemes like `webcal` or `ftp`. Defaults
641
     *   to the current scheme.
642
     * - `_host` - Set the host to use for the link. Defaults to the current host.
643
     * - `_port` - Set the port if you need to create links on non-standard ports.
644
     * - `_full` - If true output of `Router::fullBaseUrl()` will be prepended to generated URLs.
645
     * - `#` - Allows you to set URL hash fragments.
646
     * - `_ssl` - Set to true to convert the generated URL to https, or false to force http.
647
     * - `_name` - Name of route. If you have setup named routes you can use this key
648
     *   to specify it.
649
     *
650
     * @param string|array|null $url An array specifying any of the following:
651
     *   'controller', 'action', 'plugin' additionally, you can provide routed
652
     *   elements or query string parameters. If string it can be name any valid url
653
     *   string.
654
     * @param bool $full If true, the full base URL will be prepended to the result.
655
     *   Default is false.
656
     * @return string Full translated URL with base path.
657
     * @throws \Cake\Core\Exception\Exception When the route name is not found
658
     */
659
    public static function url($url = null, $full = false)
660
    {
661
        if (!static::$initialized) {
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
662
            static::_loadRoutes();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Routing\Router::_loadRoutes() has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
663
        }
664
665
        $params = [
666
            'plugin' => null,
667
            'controller' => null,
668
            'action' => 'index',
669
            '_ext' => null,
670
        ];
671
        $here = $output = $frag = null;
672
673
        $context = static::$_requestContext;
674
        // In 4.x this should be replaced with state injected via setRequestContext
675
        $request = static::getRequest(true);
676
        if ($request) {
677
            $params = $request->getAttribute('params');
678
            $here = $request->getRequestTarget();
679
            $context['_base'] = $request->getAttribute('base');
680
        } elseif (!isset($context['_base'])) {
681
            $context['_base'] = Configure::read('App.base');
682
        }
683
684
        if (empty($url)) {
685
            $output = $context['_base'] . (isset($here) ? $here : '/');
686
            if ($full) {
687
                $output = static::fullBaseUrl() . $output;
688
            }
689
690
            return $output;
691
        }
692
        if (is_array($url)) {
693
            if (isset($url['_ssl'])) {
694
                $url['_scheme'] = ($url['_ssl'] === true) ? 'https' : 'http';
695
            }
696
697
            if (isset($url['_full']) && $url['_full'] === true) {
698
                $full = true;
699
            }
700
            if (isset($url['#'])) {
701
                $frag = '#' . $url['#'];
702
            }
703
            unset($url['_ssl'], $url['_full'], $url['#']);
704
705
            $url = static::_applyUrlFilters($url);
706
707
            if (!isset($url['_name'])) {
708
                // Copy the current action if the controller is the current one.
709
                if (empty($url['action']) &&
710
                    (empty($url['controller']) || $params['controller'] === $url['controller'])
711
                ) {
712
                    $url['action'] = $params['action'];
713
                }
714
715
                // Keep the current prefix around if none set.
716
                if (isset($params['prefix']) && !isset($url['prefix'])) {
717
                    $url['prefix'] = $params['prefix'];
718
                }
719
720
                $url += [
721
                    'plugin' => $params['plugin'],
722
                    'controller' => $params['controller'],
723
                    'action' => 'index',
724
                    '_ext' => null
725
                ];
726
            }
727
728
            // If a full URL is requested with a scheme the host should default
729
            // to App.fullBaseUrl to avoid corrupt URLs
730
            if ($full && isset($url['_scheme']) && !isset($url['_host'])) {
731
                $url['_host'] = parse_url(static::fullBaseUrl(), PHP_URL_HOST);
732
            }
733
            $context['params'] = $params;
734
735
            $output = static::$_collection->match($url, $context);
736
        } else {
737
            $plainString = (
738
                strpos($url, 'javascript:') === 0 ||
739
                strpos($url, 'mailto:') === 0 ||
740
                strpos($url, 'tel:') === 0 ||
741
                strpos($url, 'sms:') === 0 ||
742
                strpos($url, '#') === 0 ||
743
                strpos($url, '?') === 0 ||
744
                strpos($url, '//') === 0 ||
745
                strpos($url, '://') !== false
746
            );
747
748
            if ($plainString) {
749
                return $url;
750
            }
751
            $output = $context['_base'] . $url;
752
        }
753
        $protocol = preg_match('#^[a-z][a-z0-9+\-.]*\://#i', $output);
754
        if ($protocol === 0) {
755
            $output = str_replace('//', '/', '/' . $output);
756
            if ($full) {
757
                $output = static::fullBaseUrl() . $output;
758
            }
759
        }
760
761
        return $output . $frag;
762
    }
763
764
    /**
765
     * Finds URL for specified action.
766
     *
767
     * Returns a bool if the url exists
768
     *
769
     * ### Usage
770
     *
771
     * @see Router::url()
772
     *
773
     * @param string|array|null $url An array specifying any of the following:
774
     *   'controller', 'action', 'plugin' additionally, you can provide routed
775
     *   elements or query string parameters. If string it can be name any valid url
776
     *   string.
777
     * @param bool $full If true, the full base URL will be prepended to the result.
778
     *   Default is false.
779
     * @return bool
780
     */
781
    public static function routeExists($url = null, $full = false)
782
    {
783
        try {
784
            $route = static::url($url, $full);
785
786
            return true;
787
        } catch (MissingRouteException $e) {
788
            return false;
789
        }
790
    }
791
792
    /**
793
     * Sets the full base URL that will be used as a prefix for generating
794
     * fully qualified URLs for this application. If no parameters are passed,
795
     * the currently configured value is returned.
796
     *
797
     * ### Note:
798
     *
799
     * If you change the configuration value `App.fullBaseUrl` during runtime
800
     * and expect the router to produce links using the new setting, you are
801
     * required to call this method passing such value again.
802
     *
803
     * @param string|null $base the prefix for URLs generated containing the domain.
804
     * For example: `http://example.com`
805
     * @return string
806
     */
807
    public static function fullBaseUrl($base = null)
808
    {
809
        if ($base !== null) {
810
            static::$_fullBaseUrl = $base;
811
            Configure::write('App.fullBaseUrl', $base);
812
        }
813
        if (empty(static::$_fullBaseUrl)) {
814
            static::$_fullBaseUrl = Configure::read('App.fullBaseUrl');
815
        }
816
817
        return static::$_fullBaseUrl;
818
    }
819
820
    /**
821
     * Reverses a parsed parameter array into an array.
822
     *
823
     * Works similarly to Router::url(), but since parsed URL's contain additional
824
     * 'pass' as well as 'url.url' keys. Those keys need to be specially
825
     * handled in order to reverse a params array into a string URL.
826
     *
827
     * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those
828
     * are used for CakePHP internals and should not normally be part of an output URL.
829
     *
830
     * @param \Cake\Http\ServerRequest|array $params The params array or
831
     *     Cake\Http\ServerRequest object that needs to be reversed.
832
     * @return array The URL array ready to be used for redirect or HTML link.
833
     */
834
    public static function reverseToArray($params)
835
    {
836
        $url = [];
837
        if ($params instanceof ServerRequest) {
838
            $url = $params->getQueryParams();
839
            $params = $params->getAttribute('params');
840
        } elseif (isset($params['url'])) {
841
            $url = $params['url'];
842
        }
843
        $pass = isset($params['pass']) ? $params['pass'] : [];
844
845
        unset(
846
            $params['pass'],
847
            $params['paging'],
848
            $params['models'],
849
            $params['url'],
850
            $url['url'],
851
            $params['autoRender'],
852
            $params['bare'],
853
            $params['requested'],
854
            $params['return'],
855
            $params['_Token'],
856
            $params['_matchedRoute'],
857
            $params['_name']
858
        );
859
        $params = array_merge($params, $pass);
860
        if (!empty($url)) {
861
            $params['?'] = $url;
862
        }
863
864
        return $params;
865
    }
866
867
    /**
868
     * Reverses a parsed parameter array into a string.
869
     *
870
     * Works similarly to Router::url(), but since parsed URL's contain additional
871
     * 'pass' as well as 'url.url' keys. Those keys need to be specially
872
     * handled in order to reverse a params array into a string URL.
873
     *
874
     * This will strip out 'autoRender', 'bare', 'requested', and 'return' param names as those
875
     * are used for CakePHP internals and should not normally be part of an output URL.
876
     *
877
     * @param \Cake\Http\ServerRequest|array $params The params array or
878
     *     Cake\Http\ServerRequest object that needs to be reversed.
879
     * @param bool $full Set to true to include the full URL including the
880
     *     protocol when reversing the URL.
881
     * @return string The string that is the reversed result of the array
882
     */
883
    public static function reverse($params, $full = false)
884
    {
885
        $params = static::reverseToArray($params);
886
887
        return static::url($params, $full);
888
    }
889
890
    /**
891
     * Normalizes a URL for purposes of comparison.
892
     *
893
     * Will strip the base path off and replace any double /'s.
894
     * It will not unify the casing and underscoring of the input value.
895
     *
896
     * @param array|string $url URL to normalize Either an array or a string URL.
897
     * @return string Normalized URL
898
     */
899
    public static function normalize($url = '/')
900
    {
901
        if (is_array($url)) {
902
            $url = static::url($url);
903
        }
904
        if (preg_match('/^[a-z\-]+:\/\//', $url)) {
905
            return $url;
906
        }
907
        $request = static::getRequest();
908
909
        if ($request) {
910
            $base = $request->getAttribute('base');
911
            if (strlen($base) && stristr($url, $base)) {
912
                $url = preg_replace('/^' . preg_quote($base, '/') . '/', '', $url, 1);
913
            }
914
        }
915
        $url = '/' . $url;
916
917
        while (strpos($url, '//') !== false) {
918
            $url = str_replace('//', '/', $url);
919
        }
920
        $url = preg_replace('/(?:(\/$))/', '', $url);
921
922
        if (empty($url)) {
923
            return '/';
924
        }
925
926
        return $url;
927
    }
928
929
    /**
930
     * Get or set valid extensions for all routes connected later.
931
     *
932
     * Instructs the router to parse out file extensions
933
     * from the URL. For example, http://example.com/posts.rss would yield a file
934
     * extension of "rss". The file extension itself is made available in the
935
     * controller as `$this->request->getParam('_ext')`, and is used by the RequestHandler
936
     * component to automatically switch to alternate layouts and templates, and
937
     * load helpers corresponding to the given content, i.e. RssHelper. Switching
938
     * layouts and helpers requires that the chosen extension has a defined mime type
939
     * in `Cake\Http\Response`.
940
     *
941
     * A string or an array of valid extensions can be passed to this method.
942
     * If called without any parameters it will return current list of set extensions.
943
     *
944
     * @param array|string|null $extensions List of extensions to be added.
945
     * @param bool $merge Whether to merge with or override existing extensions.
946
     *   Defaults to `true`.
947
     * @return array Array of extensions Router is configured to parse.
948
     */
949
    public static function extensions($extensions = null, $merge = true)
950
    {
951
        $collection = static::$_collection;
952
        if ($extensions === null) {
953
            if (!static::$initialized) {
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
954
                static::_loadRoutes();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Routing\Router::_loadRoutes() has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
955
            }
956
957
            return array_unique(array_merge(static::$_defaultExtensions, $collection->getExtensions()));
958
        }
959
        $extensions = (array)$extensions;
960
        if ($merge) {
961
            $extensions = array_unique(array_merge(static::$_defaultExtensions, $extensions));
962
        }
963
964
        return static::$_defaultExtensions = $extensions;
965
    }
966
967
    /**
968
     * Provides legacy support for named parameters on incoming URLs.
969
     *
970
     * Checks the passed parameters for elements containing `$options['separator']`
971
     * Those parameters are split and parsed as if they were old style named parameters.
972
     *
973
     * The parsed parameters will be moved from params['pass'] to params['named'].
974
     *
975
     * ### Options
976
     *
977
     * - `separator` The string to use as a separator. Defaults to `:`.
978
     *
979
     * @param \Cake\Http\ServerRequest $request The request object to modify.
980
     * @param array $options The array of options.
981
     * @return \Cake\Http\ServerRequest The modified request
982
     * @deprecated 3.3.0 Named parameter backwards compatibility will be removed in 4.0.
983
     */
984
    public static function parseNamedParams(ServerRequest $request, array $options = [])
985
    {
986
        deprecationWarning(
987
            'Router::parseNamedParams() is deprecated. ' .
988
            '2.x backwards compatible named parameter support will be removed in 4.0'
989
        );
990
        $options += ['separator' => ':'];
991
        if (!$request->getParam('pass')) {
992
            return $request->withParam('named', []);
993
        }
994
        $named = [];
995
        $pass = $request->getParam('pass');
996
        foreach ((array)$pass as $key => $value) {
997
            if (strpos($value, $options['separator']) === false) {
998
                continue;
999
            }
1000
            unset($pass[$key]);
1001
            list($key, $value) = explode($options['separator'], $value, 2);
1002
1003
            if (preg_match_all('/\[([A-Za-z0-9_-]+)?\]/', $key, $matches, PREG_SET_ORDER)) {
1004
                $matches = array_reverse($matches);
1005
                $parts = explode('[', $key);
1006
                $key = array_shift($parts);
1007
                $arr = $value;
1008
                foreach ($matches as $match) {
1009
                    if (empty($match[1])) {
1010
                        $arr = [$arr];
1011
                    } else {
1012
                        $arr = [
1013
                            $match[1] => $arr
1014
                        ];
1015
                    }
1016
                }
1017
                $value = $arr;
1018
            }
1019
            $named = array_merge_recursive($named, [$key => $value]);
1020
        }
1021
1022
        return $request
1023
            ->withParam('pass', $pass)
1024
            ->withParam('named', $named);
1025
    }
1026
1027
    /**
1028
     * Create a RouteBuilder for the provided path.
1029
     *
1030
     * @param string $path The path to set the builder to.
1031
     * @param array $options The options for the builder
1032
     * @return \Cake\Routing\RouteBuilder
1033
     */
1034
    public static function createRouteBuilder($path, array $options = [])
1035
    {
1036
        $defaults = [
1037
            'routeClass' => static::defaultRouteClass(),
1038
            'extensions' => static::$_defaultExtensions,
1039
        ];
1040
        $options += $defaults;
1041
1042
        return new RouteBuilder(static::$_collection, $path, [], [
1043
            'routeClass' => $options['routeClass'],
1044
            'extensions' => $options['extensions'],
1045
        ]);
1046
    }
1047
1048
    /**
1049
     * Create a routing scope.
1050
     *
1051
     * Routing scopes allow you to keep your routes DRY and avoid repeating
1052
     * common path prefixes, and or parameter sets.
1053
     *
1054
     * Scoped collections will be indexed by path for faster route parsing. If you
1055
     * re-open or re-use a scope the connected routes will be merged with the
1056
     * existing ones.
1057
     *
1058
     * ### Options
1059
     *
1060
     * The `$params` array allows you to define options for the routing scope.
1061
     * The options listed below *are not* available to be used as routing defaults
1062
     *
1063
     * - `routeClass` The route class to use in this scope. Defaults to
1064
     *   `Router::defaultRouteClass()`
1065
     * - `extensions` The extensions to enable in this scope. Defaults to the globally
1066
     *   enabled extensions set with `Router::extensions()`
1067
     *
1068
     * ### Example
1069
     *
1070
     * ```
1071
     * Router::scope('/blog', ['plugin' => 'Blog'], function ($routes) {
1072
     *    $routes->connect('/', ['controller' => 'Articles']);
1073
     * });
1074
     * ```
1075
     *
1076
     * The above would result in a `/blog/` route being created, with both the
1077
     * plugin & controller default parameters set.
1078
     *
1079
     * You can use `Router::plugin()` and `Router::prefix()` as shortcuts to creating
1080
     * specific kinds of scopes.
1081
     *
1082
     * @param string $path The path prefix for the scope. This path will be prepended
1083
     *   to all routes connected in the scoped collection.
1084
     * @param array|callable $params An array of routing defaults to add to each connected route.
1085
     *   If you have no parameters, this argument can be a callable.
1086
     * @param callable|null $callback The callback to invoke with the scoped collection.
1087
     * @throws \InvalidArgumentException When an invalid callable is provided.
1088
     * @return void
1089
     */
1090
    public static function scope($path, $params = [], $callback = null)
1091
    {
1092
        $options = [];
1093
        if (is_array($params)) {
1094
            $options = $params;
1095
            unset($params['routeClass'], $params['extensions']);
1096
        }
1097
        $builder = static::createRouteBuilder('/', $options);
1098
        $builder->scope($path, $params, $callback);
1099
    }
1100
1101
    /**
1102
     * Create prefixed routes.
1103
     *
1104
     * This method creates a scoped route collection that includes
1105
     * relevant prefix information.
1106
     *
1107
     * The path parameter is used to generate the routing parameter name.
1108
     * For example a path of `admin` would result in `'prefix' => 'admin'` being
1109
     * applied to all connected routes.
1110
     *
1111
     * The prefix name will be inflected to the underscore version to create
1112
     * the routing path. If you want a custom path name, use the `path` option.
1113
     *
1114
     * You can re-open a prefix as many times as necessary, as well as nest prefixes.
1115
     * Nested prefixes will result in prefix values like `admin/api` which translates
1116
     * to the `Controller\Admin\Api\` namespace.
1117
     *
1118
     * @param string $name The prefix name to use.
1119
     * @param array|callable $params An array of routing defaults to add to each connected route.
1120
     *   If you have no parameters, this argument can be a callable.
1121
     * @param callable|null $callback The callback to invoke that builds the prefixed routes.
1122
     * @return void
1123
     */
1124
    public static function prefix($name, $params = [], $callback = null)
1125
    {
1126
        if ($callback === null) {
1127
            $callback = $params;
1128
            $params = [];
1129
        }
1130
        $name = Inflector::underscore($name);
1131
1132
        if (empty($params['path'])) {
1133
            $path = '/' . $name;
1134
        } else {
1135
            $path = $params['path'];
1136
            unset($params['path']);
1137
        }
1138
1139
        $params = array_merge($params, ['prefix' => $name]);
1140
        static::scope($path, $params, $callback);
1141
    }
1142
1143
    /**
1144
     * Add plugin routes.
1145
     *
1146
     * This method creates a scoped route collection that includes
1147
     * relevant plugin information.
1148
     *
1149
     * The plugin name will be inflected to the underscore version to create
1150
     * the routing path. If you want a custom path name, use the `path` option.
1151
     *
1152
     * Routes connected in the scoped collection will have the correct path segment
1153
     * prepended, and have a matching plugin routing key set.
1154
     *
1155
     * @param string $name The plugin name to build routes for
1156
     * @param array|callable $options Either the options to use, or a callback
1157
     * @param callable|null $callback The callback to invoke that builds the plugin routes.
1158
     *   Only required when $options is defined
1159
     * @return void
1160
     */
1161
    public static function plugin($name, $options = [], $callback = null)
1162
    {
1163
        if ($callback === null) {
1164
            $callback = $options;
1165
            $options = [];
1166
        }
1167
        $params = ['plugin' => $name];
1168
        if (empty($options['path'])) {
1169
            $options['path'] = '/' . Inflector::underscore($name);
1170
        }
1171
        if (isset($options['_namePrefix'])) {
1172
            $params['_namePrefix'] = $options['_namePrefix'];
1173
        }
1174
        static::scope($options['path'], $params, $callback);
1175
    }
1176
1177
    /**
1178
     * Get the route scopes and their connected routes.
1179
     *
1180
     * @return \Cake\Routing\Route\Route[]
1181
     */
1182
    public static function routes()
1183
    {
1184
        if (!static::$initialized) {
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1185
            static::_loadRoutes();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Routing\Router::_loadRoutes() has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1186
        }
1187
1188
        return static::$_collection->routes();
1189
    }
1190
1191
    /**
1192
     * Get the RouteCollection inside the Router
1193
     *
1194
     * @return \Cake\Routing\RouteCollection
1195
     */
1196
    public static function getRouteCollection()
1197
    {
1198
        return static::$_collection;
1199
    }
1200
1201
    /**
1202
     * Set the RouteCollection inside the Router
1203
     *
1204
     * @param RouteCollection $routeCollection route collection
1205
     * @return void
1206
     */
1207
    public static function setRouteCollection($routeCollection)
1208
    {
1209
        static::$_collection = $routeCollection;
1210
        static::$initialized = true;
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1211
    }
1212
1213
    /**
1214
     * Loads route configuration
1215
     *
1216
     * @deprecated 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0
1217
     * @return void
1218
     */
1219
    protected static function _loadRoutes()
1220
    {
1221
        static::$initialized = true;
0 ignored issues
show
Deprecated Code introduced by
The property Cake\Routing\Router::$initialized has been deprecated with message: 3.5.0 Routes will be loaded via the Application::routes() hook in 4.0.0

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
1222
        include CONFIG . 'routes.php';
1223
    }
1224
}
1225