ci4

Форк
0
/
Router.php 
743 строки · 22.0 Кб
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Router;
15

16
use Closure;
17
use CodeIgniter\Exceptions\PageNotFoundException;
18
use CodeIgniter\HTTP\Exceptions\BadRequestException;
19
use CodeIgniter\HTTP\Exceptions\RedirectException;
20
use CodeIgniter\HTTP\Method;
21
use CodeIgniter\HTTP\Request;
22
use CodeIgniter\HTTP\ResponseInterface;
23
use CodeIgniter\Router\Exceptions\RouterException;
24
use Config\App;
25
use Config\Feature;
26
use Config\Routing;
27

28
/**
29
 * Request router.
30
 *
31
 * @see \CodeIgniter\Router\RouterTest
32
 */
33
class Router implements RouterInterface
34
{
35
    /**
36
     * List of allowed HTTP methods (and CLI for command line use).
37
     */
38
    public const HTTP_METHODS = [
39
        Method::GET,
40
        Method::HEAD,
41
        Method::POST,
42
        Method::PATCH,
43
        Method::PUT,
44
        Method::DELETE,
45
        Method::OPTIONS,
46
        Method::TRACE,
47
        Method::CONNECT,
48
        'CLI',
49
    ];
50

51
    /**
52
     * A RouteCollection instance.
53
     *
54
     * @var RouteCollectionInterface
55
     */
56
    protected $collection;
57

58
    /**
59
     * Sub-directory that contains the requested controller class.
60
     * Primarily used by 'autoRoute'.
61
     *
62
     * @var string|null
63
     */
64
    protected $directory;
65

66
    /**
67
     * The name of the controller class.
68
     *
69
     * @var (Closure(mixed...): (ResponseInterface|string|void))|string
70
     */
71
    protected $controller;
72

73
    /**
74
     * The name of the method to use.
75
     *
76
     * @var string
77
     */
78
    protected $method;
79

80
    /**
81
     * An array of binds that were collected
82
     * so they can be sent to closure routes.
83
     *
84
     * @var array
85
     */
86
    protected $params = [];
87

88
    /**
89
     * The name of the front controller.
90
     *
91
     * @var string
92
     */
93
    protected $indexPage = 'index.php';
94

95
    /**
96
     * Whether dashes in URI's should be converted
97
     * to underscores when determining method names.
98
     *
99
     * @var bool
100
     */
101
    protected $translateURIDashes = false;
102

103
    /**
104
     * The route that was matched for this request.
105
     *
106
     * @var array|null
107
     */
108
    protected $matchedRoute;
109

110
    /**
111
     * The options set for the matched route.
112
     *
113
     * @var array|null
114
     */
115
    protected $matchedRouteOptions;
116

117
    /**
118
     * The locale that was detected in a route.
119
     *
120
     * @var string
121
     */
122
    protected $detectedLocale;
123

124
    /**
125
     * The filter info from Route Collection
126
     * if the matched route should be filtered.
127
     *
128
     * @var list<string>
129
     */
130
    protected $filtersInfo = [];
131

132
    protected ?AutoRouterInterface $autoRouter = null;
133

134
    /**
135
     * Permitted URI chars
136
     *
137
     * The default value is `''` (do not check) for backward compatibility.
138
     */
139
    protected string $permittedURIChars = '';
140

141
    /**
142
     * Stores a reference to the RouteCollection object.
143
     */
144
    public function __construct(RouteCollectionInterface $routes, ?Request $request = null)
145
    {
146
        $config = config(App::class);
147
        if (isset($config->permittedURIChars)) {
148
            $this->permittedURIChars = $config->permittedURIChars;
149
        }
150

151
        $this->collection = $routes;
152

153
        // These are only for auto-routing
154
        $this->controller = $this->collection->getDefaultController();
155
        $this->method     = $this->collection->getDefaultMethod();
156

157
        $this->collection->setHTTPVerb($request->getMethod() ?? $_SERVER['REQUEST_METHOD']);
158

159
        $this->translateURIDashes = $this->collection->shouldTranslateURIDashes();
160

161
        if ($this->collection->shouldAutoRoute()) {
162
            $autoRoutesImproved = config(Feature::class)->autoRoutesImproved ?? false;
163
            if ($autoRoutesImproved) {
164
                $this->autoRouter = new AutoRouterImproved(
165
                    $this->collection->getRegisteredControllers('*'),
166
                    $this->collection->getDefaultNamespace(),
167
                    $this->collection->getDefaultController(),
168
                    $this->collection->getDefaultMethod(),
169
                    $this->translateURIDashes
170
                );
171
            } else {
172
                $this->autoRouter = new AutoRouter(
173
                    $this->collection->getRoutes('CLI', false), // @phpstan-ignore-line
174
                    $this->collection->getDefaultNamespace(),
175
                    $this->collection->getDefaultController(),
176
                    $this->collection->getDefaultMethod(),
177
                    $this->translateURIDashes
178
                );
179
            }
180
        }
181
    }
182

183
    /**
184
     * Finds the controller corresponding to the URI.
185
     *
186
     * @param string|null $uri URI path relative to baseURL
187
     *
188
     * @return (Closure(mixed...): (ResponseInterface|string|void))|string Controller classname or Closure
189
     *
190
     * @throws BadRequestException
191
     * @throws PageNotFoundException
192
     * @throws RedirectException
193
     */
194
    public function handle(?string $uri = null)
195
    {
196
        // If we cannot find a URI to match against, then set it to root (`/`).
197
        if ($uri === null || $uri === '') {
198
            $uri = '/';
199
        }
200

201
        // Decode URL-encoded string
202
        $uri = urldecode($uri);
203

204
        $this->checkDisallowedChars($uri);
205

206
        // Restart filterInfo
207
        $this->filtersInfo = [];
208

209
        // Checks defined routes
210
        if ($this->checkRoutes($uri)) {
211
            if ($this->collection->isFiltered($this->matchedRoute[0])) {
212
                $this->filtersInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
213
            }
214

215
            return $this->controller;
216
        }
217

218
        // Still here? Then we can try to match the URI against
219
        // Controllers/directories, but the application may not
220
        // want this, like in the case of API's.
221
        if (! $this->collection->shouldAutoRoute()) {
222
            throw new PageNotFoundException(
223
                "Can't find a route for '{$this->collection->getHTTPVerb()}: {$uri}'."
224
            );
225
        }
226

227
        // Checks auto routes
228
        $this->autoRoute($uri);
229

230
        return $this->controllerName();
231
    }
232

233
    /**
234
     * Returns the filter info for the matched route, if any.
235
     *
236
     * @return list<string>
237
     */
238
    public function getFilters(): array
239
    {
240
        return $this->filtersInfo;
241
    }
242

243
    /**
244
     * Returns the name of the matched controller or closure.
245
     *
246
     * @return (Closure(mixed...): (ResponseInterface|string|void))|string Controller classname or Closure
247
     */
248
    public function controllerName()
249
    {
250
        return $this->translateURIDashes
251
            ? str_replace('-', '_', $this->controller)
252
            : $this->controller;
253
    }
254

255
    /**
256
     * Returns the name of the method to run in the
257
     * chosen controller.
258
     */
259
    public function methodName(): string
260
    {
261
        return $this->translateURIDashes
262
            ? str_replace('-', '_', $this->method)
263
            : $this->method;
264
    }
265

266
    /**
267
     * Returns the 404 Override settings from the Collection.
268
     * If the override is a string, will split to controller/index array.
269
     */
270
    public function get404Override()
271
    {
272
        $route = $this->collection->get404Override();
273

274
        if (is_string($route)) {
275
            $routeArray = explode('::', $route);
276

277
            return [
278
                $routeArray[0], // Controller
279
                $routeArray[1] ?? 'index',   // Method
280
            ];
281
        }
282

283
        if (is_callable($route)) {
284
            return $route;
285
        }
286

287
        return null;
288
    }
289

290
    /**
291
     * Returns the binds that have been matched and collected
292
     * during the parsing process as an array, ready to send to
293
     * instance->method(...$params).
294
     */
295
    public function params(): array
296
    {
297
        return $this->params;
298
    }
299

300
    /**
301
     * Returns the name of the sub-directory the controller is in,
302
     * if any. Relative to APPPATH.'Controllers'.
303
     *
304
     * Only used when auto-routing is turned on.
305
     */
306
    public function directory(): string
307
    {
308
        if ($this->autoRouter instanceof AutoRouter) {
309
            return $this->autoRouter->directory();
310
        }
311

312
        return '';
313
    }
314

315
    /**
316
     * Returns the routing information that was matched for this
317
     * request, if a route was defined.
318
     *
319
     * @return array|null
320
     */
321
    public function getMatchedRoute()
322
    {
323
        return $this->matchedRoute;
324
    }
325

326
    /**
327
     * Returns all options set for the matched route
328
     *
329
     * @return array|null
330
     */
331
    public function getMatchedRouteOptions()
332
    {
333
        return $this->matchedRouteOptions;
334
    }
335

336
    /**
337
     * Sets the value that should be used to match the index.php file. Defaults
338
     * to index.php but this allows you to modify it in case you are using
339
     * something like mod_rewrite to remove the page. This allows you to set
340
     * it a blank.
341
     *
342
     * @param string $page
343
     */
344
    public function setIndexPage($page): self
345
    {
346
        $this->indexPage = $page;
347

348
        return $this;
349
    }
350

351
    /**
352
     * Tells the system whether we should translate URI dashes or not
353
     * in the URI from a dash to an underscore.
354
     *
355
     * @deprecated This method should be removed.
356
     */
357
    public function setTranslateURIDashes(bool $val = false): self
358
    {
359
        if ($this->autoRouter instanceof AutoRouter) {
360
            $this->autoRouter->setTranslateURIDashes($val);
361

362
            return $this;
363
        }
364

365
        return $this;
366
    }
367

368
    /**
369
     * Returns true/false based on whether the current route contained
370
     * a {locale} placeholder.
371
     *
372
     * @return bool
373
     */
374
    public function hasLocale()
375
    {
376
        return (bool) $this->detectedLocale;
377
    }
378

379
    /**
380
     * Returns the detected locale, if any, or null.
381
     *
382
     * @return string
383
     */
384
    public function getLocale()
385
    {
386
        return $this->detectedLocale;
387
    }
388

389
    /**
390
     * Checks Defined Routes.
391
     *
392
     * Compares the uri string against the routes that the
393
     * RouteCollection class defined for us, attempting to find a match.
394
     * This method will modify $this->controller, etal as needed.
395
     *
396
     * @param string $uri The URI path to compare against the routes
397
     *
398
     * @return bool Whether the route was matched or not.
399
     *
400
     * @throws RedirectException
401
     */
402
    protected function checkRoutes(string $uri): bool
403
    {
404
        // @phpstan-ignore-next-line
405
        $routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
406

407
        // Don't waste any time
408
        if ($routes === []) {
409
            return false;
410
        }
411

412
        $uri = $uri === '/'
413
            ? $uri
414
            : trim($uri, '/ ');
415

416
        // Loop through the route array looking for wildcards
417
        foreach ($routes as $routeKey => $handler) {
418
            $routeKey = $routeKey === '/'
419
                ? $routeKey
420
                // $routeKey may be int, because it is an array key,
421
                // and the URI `/1` is valid. The leading `/` is removed.
422
                : ltrim((string) $routeKey, '/ ');
423

424
            $matchedKey = $routeKey;
425

426
            // Are we dealing with a locale?
427
            if (str_contains($routeKey, '{locale}')) {
428
                $routeKey = str_replace('{locale}', '[^/]+', $routeKey);
429
            }
430

431
            // Does the RegEx match?
432
            if (preg_match('#^' . $routeKey . '$#u', $uri, $matches)) {
433
                // Is this route supposed to redirect to another?
434
                if ($this->collection->isRedirect($routeKey)) {
435
                    // replacing matched route groups with references: post/([0-9]+) -> post/$1
436
                    $redirectTo = preg_replace_callback('/(\([^\(]+\))/', static function () {
437
                        static $i = 1;
438

439
                        return '$' . $i++;
440
                    }, is_array($handler) ? key($handler) : $handler);
441

442
                    throw new RedirectException(
443
                        preg_replace('#\A' . $routeKey . '\z#u', $redirectTo, $uri),
444
                        $this->collection->getRedirectCode($routeKey)
445
                    );
446
                }
447
                // Store our locale so CodeIgniter object can
448
                // assign it to the Request.
449
                if (str_contains($matchedKey, '{locale}')) {
450
                    preg_match(
451
                        '#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
452
                        $uri,
453
                        $matched
454
                    );
455

456
                    if ($this->collection->shouldUseSupportedLocalesOnly()
457
                        && ! in_array($matched['locale'], config(App::class)->supportedLocales, true)) {
458
                        // Throw exception to prevent the autorouter, if enabled,
459
                        // from trying to find a route
460
                        throw PageNotFoundException::forLocaleNotSupported($matched['locale']);
461
                    }
462

463
                    $this->detectedLocale = $matched['locale'];
464
                    unset($matched);
465
                }
466

467
                // Are we using Closures? If so, then we need
468
                // to collect the params into an array
469
                // so it can be passed to the controller method later.
470
                if (! is_string($handler) && is_callable($handler)) {
471
                    $this->controller = $handler;
472

473
                    // Remove the original string from the matches array
474
                    array_shift($matches);
475

476
                    $this->params = $matches;
477

478
                    $this->setMatchedRoute($matchedKey, $handler);
479

480
                    return true;
481
                }
482

483
                if (str_contains($handler, '::')) {
484
                    [$controller, $methodAndParams] = explode('::', $handler);
485
                } else {
486
                    $controller      = $handler;
487
                    $methodAndParams = '';
488
                }
489

490
                // Checks `/` in controller name
491
                if (str_contains($controller, '/')) {
492
                    throw RouterException::forInvalidControllerName($handler);
493
                }
494

495
                if (str_contains($handler, '$') && str_contains($routeKey, '(')) {
496
                    // Checks dynamic controller
497
                    if (str_contains($controller, '$')) {
498
                        throw RouterException::forDynamicController($handler);
499
                    }
500

501
                    if (config(Routing::class)->multipleSegmentsOneParam === false) {
502
                        // Using back-references
503
                        $segments = explode('/', preg_replace('#\A' . $routeKey . '\z#u', $handler, $uri));
504
                    } else {
505
                        if (str_contains($methodAndParams, '/')) {
506
                            [$method, $handlerParams] = explode('/', $methodAndParams, 2);
507
                            $params                   = explode('/', $handlerParams);
508
                            $handlerSegments          = array_merge([$controller . '::' . $method], $params);
509
                        } else {
510
                            $handlerSegments = [$handler];
511
                        }
512

513
                        $segments = [];
514

515
                        foreach ($handlerSegments as $segment) {
516
                            $segments[] = $this->replaceBackReferences($segment, $matches);
517
                        }
518
                    }
519
                } else {
520
                    $segments = explode('/', $handler);
521
                }
522

523
                $this->setRequest($segments);
524

525
                $this->setMatchedRoute($matchedKey, $handler);
526

527
                return true;
528
            }
529
        }
530

531
        return false;
532
    }
533

534
    /**
535
     * Replace string `$n` with `$matches[n]` value.
536
     */
537
    private function replaceBackReferences(string $input, array $matches): string
538
    {
539
        $pattern = '/\$([1-' . count($matches) . '])/u';
540

541
        return preg_replace_callback(
542
            $pattern,
543
            static function ($match) use ($matches) {
544
                $index = (int) $match[1];
545

546
                return $matches[$index] ?? '';
547
            },
548
            $input
549
        );
550
    }
551

552
    /**
553
     * Checks Auto Routes.
554
     *
555
     * Attempts to match a URI path against Controllers and directories
556
     * found in APPPATH/Controllers, to find a matching route.
557
     *
558
     * @return void
559
     */
560
    public function autoRoute(string $uri)
561
    {
562
        [$this->directory, $this->controller, $this->method, $this->params]
563
            = $this->autoRouter->getRoute($uri, $this->collection->getHTTPVerb());
564
    }
565

566
    /**
567
     * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments
568
     *
569
     * @param array $segments URI segments
570
     *
571
     * @return array returns an array of remaining uri segments that don't map onto a directory
572
     *
573
     * @deprecated this function name does not properly describe its behavior so it has been deprecated
574
     *
575
     * @codeCoverageIgnore
576
     */
577
    protected function validateRequest(array $segments): array
578
    {
579
        return $this->scanControllers($segments);
580
    }
581

582
    /**
583
     * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments
584
     *
585
     * @param array $segments URI segments
586
     *
587
     * @return array returns an array of remaining uri segments that don't map onto a directory
588
     *
589
     * @deprecated Not used. Moved to AutoRouter class.
590
     */
591
    protected function scanControllers(array $segments): array
592
    {
593
        $segments = array_filter($segments, static fn ($segment) => $segment !== '');
594
        // numerically reindex the array, removing gaps
595
        $segments = array_values($segments);
596

597
        // if a prior directory value has been set, just return segments and get out of here
598
        if (isset($this->directory)) {
599
            return $segments;
600
        }
601

602
        // Loop through our segments and return as soon as a controller
603
        // is found or when such a directory doesn't exist
604
        $c = count($segments);
605

606
        while ($c-- > 0) {
607
            $segmentConvert = ucfirst($this->translateURIDashes === true ? str_replace('-', '_', $segments[0]) : $segments[0]);
608
            // as soon as we encounter any segment that is not PSR-4 compliant, stop searching
609
            if (! $this->isValidSegment($segmentConvert)) {
610
                return $segments;
611
            }
612

613
            $test = APPPATH . 'Controllers/' . $this->directory . $segmentConvert;
614

615
            // as long as each segment is *not* a controller file but does match a directory, add it to $this->directory
616
            if (! is_file($test . '.php') && is_dir($test)) {
617
                $this->setDirectory($segmentConvert, true, false);
618
                array_shift($segments);
619

620
                continue;
621
            }
622

623
            return $segments;
624
        }
625

626
        // This means that all segments were actually directories
627
        return $segments;
628
    }
629

630
    /**
631
     * Sets the sub-directory that the controller is in.
632
     *
633
     * @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments
634
     *
635
     * @return void
636
     *
637
     * @deprecated This method should be removed.
638
     */
639
    public function setDirectory(?string $dir = null, bool $append = false, bool $validate = true)
640
    {
641
        if ($dir === null || $dir === '') {
642
            $this->directory = null;
643
        }
644

645
        if ($this->autoRouter instanceof AutoRouter) {
646
            $this->autoRouter->setDirectory($dir, $append, $validate);
647
        }
648
    }
649

650
    /**
651
     * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment
652
     *
653
     * regex comes from https://www.php.net/manual/en/language.variables.basics.php
654
     *
655
     * @deprecated Moved to AutoRouter class.
656
     */
657
    private function isValidSegment(string $segment): bool
658
    {
659
        return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
660
    }
661

662
    /**
663
     * Set request route
664
     *
665
     * Takes an array of URI segments as input and sets the class/method
666
     * to be called.
667
     *
668
     * @param array $segments URI segments
669
     *
670
     * @return void
671
     */
672
    protected function setRequest(array $segments = [])
673
    {
674
        // If we don't have any segments - use the default controller;
675
        if ($segments === []) {
676
            return;
677
        }
678

679
        [$controller, $method] = array_pad(explode('::', $segments[0]), 2, null);
680

681
        $this->controller = $controller;
682

683
        // $this->method already contains the default method name,
684
        // so don't overwrite it with emptiness.
685
        if (! empty($method)) {
686
            $this->method = $method;
687
        }
688

689
        array_shift($segments);
690

691
        $this->params = $segments;
692
    }
693

694
    /**
695
     * Sets the default controller based on the info set in the RouteCollection.
696
     *
697
     * @deprecated This was an unnecessary method, so it is no longer used.
698
     *
699
     * @return void
700
     */
701
    protected function setDefaultController()
702
    {
703
        if (empty($this->controller)) {
704
            throw RouterException::forMissingDefaultRoute();
705
        }
706

707
        sscanf($this->controller, '%[^/]/%s', $class, $this->method);
708

709
        if (! is_file(APPPATH . 'Controllers/' . $this->directory . ucfirst($class) . '.php')) {
710
            return;
711
        }
712

713
        $this->controller = ucfirst($class);
714

715
        log_message('info', 'Used the default controller.');
716
    }
717

718
    /**
719
     * @param callable|string $handler
720
     */
721
    protected function setMatchedRoute(string $route, $handler): void
722
    {
723
        $this->matchedRoute = [$route, $handler];
724

725
        $this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
726
    }
727

728
    /**
729
     * Checks disallowed characters
730
     */
731
    private function checkDisallowedChars(string $uri): void
732
    {
733
        foreach (explode('/', $uri) as $segment) {
734
            if ($segment !== '' && $this->permittedURIChars !== ''
735
                && preg_match('/\A[' . $this->permittedURIChars . ']+\z/iu', $segment) !== 1
736
            ) {
737
                throw new BadRequestException(
738
                    'The URI you submitted has disallowed characters: "' . $segment . '"'
739
                );
740
            }
741
        }
742
    }
743
}
744

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.