Passed
Push — main ( c6deb1...79ccdf )
by Dimitri
12:23
created

AutoRouter::checkParameters()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 6
nop 1
dl 0
loc 24
rs 9.4222
c 0
b 0
f 0
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\Router;
13
14
use BlitzPHP\Contracts\Router\AutoRouterInterface;
15
use BlitzPHP\Exceptions\MethodNotFoundException;
16
use BlitzPHP\Exceptions\PageNotFoundException;
17
use BlitzPHP\Utilities\Helpers;
18
use BlitzPHP\Utilities\String\Text;
19
use ReflectionClass;
20
use ReflectionException;
21
22
/**
23
 * Routeur sécurisé pour le routage automatique
24
 *
25
 * @credit <a href="http://www.codeigniter.com">CodeIgniter 4.4 - CodeIgniter\Router\AutoRouterImproved</a>
26
 */
27
final class AutoRouter implements AutoRouterInterface
28
{
29
    /**
30
     * Sous-répertoire contenant la classe contrôleur demandée.
31
     * Principalement utilisé par 'autoRoute'.
32
     */
33
    private ?string $directory = null;
34
35
    /**
36
     * Le nom de la classe du contrôleur.
37
     */
38
    private string $controller;
39
40
    /**
41
     * Nom de la méthode à utiliser.
42
     */
43
    private string $method;
44
45
    /**
46
     * Tableau de paramètres de la méthode du contrôleur.
47
     *
48
     * @var string[]
49
     */
50
    private array $params = [];
51
52
    /**
53
     * Namespace des controleurs
54
     */
55
    private string $namespace;
56
57
    /**
58
     * Segments de l'URI
59
     *
60
     * @var string[]
61
     */
62
    private array $segments = [];
63
64
    /**
65
     * Position du contrôleur dans les segments URI. 
66
	 * Null pour le contrôleur par défaut. 
67
     */
68
    private ?int $controllerPos = null;
69
70
    /**
71
     * Position de la méthode dans les segments URI. 
72
	 * Null pour la méthode par défaut. 
73
     */
74
    private ?int $methodPos = null;
75
76
    /**
77
     * Position du premier Paramètre dans les segments URI. 
78
	 * Null pour les paramètres non definis. 
79
     */
80
    private ?int $paramPos = null;
81
82
    /**
83
     * Constructeur
84
     *
85
     * @param class-string[] $protectedControllers Liste des contrôleurs enregistrés pour le verbe CLI qui ne doivent pas être accessibles sur le Web.
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string[] at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string[].
Loading history...
86
     * @param string         $defaultNamespace     Espace de noms par défaut pour les contrôleurs.
87
     * @param string         $defaultController     Nom du controleur par defaut.
88
     * @param string         $defaultMethod     Nom de la methode par defaut.
89
     * @param bool           $translateURIDashes   Indique si les tirets dans les URI doivent être convertis en traits de soulignement lors de la détermination des noms de méthode.
90
     */
91
    public function __construct(
92
        private array $protectedControllers,
93
        string $namespace,
94
        private string $defaultController,
95
        private string $defaultMethod,
96
        private bool $translateURIDashes
97
    ) {
98
		$this->namespace = rtrim($namespace, '\\');
99
        
100
		// Definir les valeurs par defaut
101
        $this->controller = $this->defaultController;
102
    }
103
104
    private function createSegments(string $uri): array
105
    {
106
        $segments = explode('/', $uri);
107
        $segments = array_filter($segments, static fn ($segment) => $segment !== '');
108
109
        // réindexer numériquement le tableau, en supprimant les lacunes
110
        return array_values($segments);
111
    }
112
113
    /**
114
     * Recherchez le premier contrôleur correspondant au segment URI.
115
     *
116
     * S'il y a un contrôleur correspondant au premier segment, la recherche s'arrête là. 
117
	 * Les segments restants sont des paramètres du contrôleur. 
118
	 * 
119
     * @return bool true si une classe de contrôleur est trouvée.
120
     */
121
    private function searchFirstController(): bool
122
    {
123
        $segments = $this->segments;
124
125
        $controller = '\\' . $this->namespace;
126
127
        $controllerPos = -1;
128
129
        while ($segments !== []) {
130
            $segment = array_shift($segments);
131
            $controllerPos++;
132
133
            $class = $this->translateURIDashes(ucfirst($segment));
134
135
            // dès que nous rencontrons un segment qui n'est pas compatible PSR-4, arrêter la recherche
136
            if (! $this->isValidSegment($class)) {
137
                return false;
138
            }
139
140
            $controller .= '\\' . $class;
141
142
            if (class_exists($controller)) {
143
                $this->controller    = $controller;
144
                $this->controllerPos = $controllerPos;
145
146
                // Le premier élément peut être un nom de méthode.
147
                $this->params = $segments;
148
                if ($segments !== []) {
149
                    $this->paramPos = $this->controllerPos + 1;
150
                }
151
152
                return true;
153
            }
154
        }
155
156
        return false;
157
    }
158
159
	/**
160
     * Recherchez le dernier contrôleur par défaut correspondant aux segments URI.
161
     *
162
     * @return bool true si une classe de contrôleur est trouvée.
163
     */
164
    private function searchLastDefaultController(): bool
165
    {
166
        $segments = $this->segments;
167
168
        $segmentCount = count($this->segments);
169
        $paramPos     = null;
170
        $params       = [];
171
172
        while ($segments !== []) {
173
            if ($segmentCount > count($segments)) {
174
                $paramPos = count($segments);
175
            }
176
177
            $namespaces = array_map(
178
                fn ($segment) => $this->translateURIDashes(ucfirst($segment)),
179
                $segments
180
            );
181
182
            $controller = '\\' . $this->namespace
183
                . '\\' . implode('\\', $namespaces)
184
                . '\\' . $this->defaultController;
185
186
            if (class_exists($controller)) {
187
                $this->controller = $controller;
188
                $this->params     = $params;
189
190
                if ($params !== []) {
191
                    $this->paramPos = $paramPos;
192
                }
193
194
                return true;
195
            }
196
197
            // ajoutons le dernier élément dans $segments au début de $params.
198
            array_unshift($params, array_pop($segments));
199
        }
200
201
        // Vérifiez le contrôleur par défaut dans le répertoire des contrôleurs.
202
        $controller = '\\' . $this->namespace
203
            . '\\' . $this->defaultController;
204
205
        if (class_exists($controller)) {
206
            $this->controller = $controller;
207
            $this->params     = $params;
208
209
            if ($params !== []) {
210
                $this->paramPos = 0;
211
            }
212
213
            return true;
214
        }
215
216
        return false;
217
    }
218
219
	/**
220
     * Recherche contrôleur, méthode et params dans l'URI.
221
     *
222
     * @return array [directory_name, controller_name, controller_method, params]
223
     */
224
    public function getRoute(string $uri, string $httpVerb): array
225
    {
226
		$httpVerb = strtolower($httpVerb);
227
228
		// Reinitialise les parametres de la methode du controleur.
229
        $this->params = [];
230
231
        $defaultMethod = $httpVerb . ucfirst($this->defaultMethod);
232
        $this->method  = $defaultMethod;
233
234
        $this->segments = $this->createSegments($uri);
235
236
		//Verifier les routes de modules
237
        if (
238
            $this->segments !== []
239
            && ($routingConfig = (object) config('routing'))
240
            && array_key_exists($this->segments[0], $routingConfig->module_routes)
0 ignored issues
show
Bug introduced by
The property module_routes does not seem to exist on BlitzPHP\Config\Config.
Loading history...
241
        ) {
242
            $uriSegment      = array_shift($this->segments);
243
            $this->namespace = rtrim($routingConfig->module_routes[$uriSegment], '\\');
244
        }
245
246
		if ($this->searchFirstController()) {
247
            // Le contrôleur a ete trouvé.
248
            $baseControllerName = Helpers::classBasename($this->controller);
249
250
            // Empêcher l'accès au chemin de contrôleur par défaut
251
            if (strtolower($baseControllerName) === strtolower($this->defaultController)) {
252
                throw new PageNotFoundException(
253
                    'Impossible d\'accéder au contrôleur par défaut "' . $this->controller . '" avec le nom du contrôleur comme chemin de l\'URI.'
254
                );
255
            }
256
        } elseif ($this->searchLastDefaultController()) {
257
            // Le controleur par defaut a ete trouve.
258
            $baseControllerName = Helpers::classBasename($this->controller);
259
        } else {
260
            // Aucun controleur trouvé
261
            throw new PageNotFoundException('Aucun contrôleur trouvé pour: ' . $uri);
262
        }
263
264
		// Le premier élément peut être un nom de méthode.
265
        /** @var string[] $params */
266
        $params = $this->params;
267
268
        $methodParam = array_shift($params);
269
270
        $method = '';
271
        if ($methodParam !== null) {
272
            $method = $httpVerb . ucfirst($this->translateURIDashes($methodParam));
273
        }
274
275
		if ($methodParam !== null && method_exists($this->controller, $method)) {
276
            // Methode trouvee.
277
            $this->method = $method;
278
            $this->params = $params;
279
280
            // Mise a jour des positions.
281
            $this->methodPos = $this->paramPos;
282
            if ($params === []) {
283
                $this->paramPos = null;
284
            }
285
            if ($this->paramPos !== null) {
286
                $this->paramPos++;
287
            }
288
289
            // Empêcher l'accès à la méthode du contrôleur par défaut
290
            if (strtolower($baseControllerName) === strtolower($this->defaultController)) {
291
                throw new PageNotFoundException(
292
                    'Impossible d\'accéder au contrôleur par défaut "' . $this->controller . '::' . $this->method . '"'
293
                );
294
            }
295
296
            // Empêcher l'accès au chemin de méthode par défaut
297
            if (strtolower($this->method) === strtolower($defaultMethod)) {
298
                throw new PageNotFoundException(
299
                    'Impossible d\'accéder à la méthode par défaut "' . $this->method . '" avec le nom de méthode comme chemin d\'URI.'
300
                );
301
            }
302
        } elseif (method_exists($this->controller, $defaultMethod)) {
303
            // La methode par defaut a ete trouvée
304
            $this->method = $defaultMethod;
305
        } else {
306
            // Aucune methode trouvee
307
            throw PageNotFoundException::controllerNotFound($this->controller, $method);
308
        }
309
310
		// Vérifiez le contrôleur n'est pas défini dans les routes.
311
        $this->protectDefinedRoutes();
312
313
        // Assurez-vous que le contrôleur n'a pas la méthode _remap().
314
        $this->checkRemap();
315
316
        // Assurez-vous que les segments URI pour le contrôleur et la méthode 
317
		// ne contiennent pas de soulignement lorsque $translateURIDashes est true.
318
        $this->checkUnderscore($uri);
319
320
        // Verifier le nombre de parametres
321
        try {
322
            $this->checkParameters($uri);
323
        } catch (MethodNotFoundException $e) {
324
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
325
        }
326
327
        $this->setDirectory();
328
        
329
		return [$this->directory, $this->controllerName(), $this->methodName(), $this->params];
330
    }
331
332
    private function checkParameters(string $uri): void
333
    {
334
        try {
335
            $refClass = new ReflectionClass($this->controller);
336
        } catch (ReflectionException $e) {
337
            throw PageNotFoundException::controllerNotFound($this->controller, $this->method);
338
        }
339
340
        try {
341
            $refMethod = $refClass->getMethod($this->method);
342
            $refParams = $refMethod->getParameters();
343
        } catch (ReflectionException $e) {
344
            throw new MethodNotFoundException();
345
        }
346
347
        if (! $refMethod->isPublic()) {
348
            throw new MethodNotFoundException();
349
        }
350
351
        if (count($refParams) < count($this->params)) {
352
            throw new PageNotFoundException(
353
                'Le nombre de param dans l\'URI est supérieur aux paramètres de la méthode du contrôleur.'
354
                . ' Handler:' . $this->controller . '::' . $this->method
355
                . ', URI:' . $uri
356
            );
357
        }
358
    }
359
360
    private function checkRemap(): void
361
    {
362
        try {
363
            $refClass = new ReflectionClass($this->controller);
364
            $refClass->getMethod('_remap');
365
366
            throw new PageNotFoundException(
367
                'AutoRouterImproved ne prend pas en charge la methode `_remap()`.'
368
                . ' Contrôleur:' . $this->controller
369
            );
370
        } catch (ReflectionException $e) {
371
            // Ne rien faire
372
        }
373
    }
374
375
    private function checkUnderscore(string $uri): void
376
    {
377
        if ($this->translateURIDashes === false) {
378
            return;
379
        }
380
381
        $paramPos = $this->paramPos ?? count($this->segments);
382
383
        for ($i = 0; $i < $paramPos; $i++) {
384
            if (strpos($this->segments[$i], '_') !== false) {
385
                throw new PageNotFoundException(
386
                    'AutoRouterImproved interdit l\'accès à l\'URI'
387
                    . ' contenant les undescore ("' . $this->segments[$i] . '")'
388
                    . ' quand $translate_uri_dashes est activé.'
389
                    . ' Veuillez utiliser les tiret.'
390
                    . ' Handler:' . $this->controller . '::' . $this->method
391
                    . ', URI:' . $uri
392
                );
393
            }
394
        }
395
    }
396
397
    /**
398
     * Renvoie true si la chaîne $segment fournie représente un segment d'espace de noms/répertoire valide conforme à PSR-4
399
     *
400
     * La regex vient de https://www.php.net/manual/en/language.variables.basics.php
401
     */
402
    private function isValidSegment(string $segment): bool
403
    {
404
        return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
405
    }
406
407
    private function translateURIDashes(string $segment): string
408
    {
409
        return $this->translateURIDashes
410
            ? str_replace('-', '_', $segment)
411
            : $segment;
412
    }
413
414
    /**
415
     * Obtenez le chemin du dossier du contrôleur et définissez-le sur la propriété.
416
     */
417
    private function setDirectory(): void
418
    {
419
        $segments = explode('\\', trim($this->controller, '\\'));
420
421
        // Supprimer le court nom de classe.
422
        array_pop($segments);
423
424
        $namespaces = implode('\\', $segments);
425
426
        $dir = str_replace(
427
            '\\',
428
            '/',
429
            ltrim(substr($namespaces, strlen($this->namespace)), '\\')
430
        );
431
432
        if ($dir !== '') {
433
            $this->directory = $dir . '/';
434
        }
435
    }
436
437
	private function protectDefinedRoutes(): void
438
    {
439
        $controller = strtolower($this->controller);
440
441
        foreach ($this->protectedControllers as $controllerInRoutes) {
442
            $routeLowerCase = strtolower($controllerInRoutes);
443
444
            if ($routeLowerCase === $controller) {
445
                throw new PageNotFoundException(
446
                    'Impossible d\'accéder à un contrôleur définie dans les routes. Contrôleur : ' . $controllerInRoutes
447
                );
448
            }
449
        }
450
    }
451
452
    /**
453
     * Renvoie le nom du sous-répertoire dans lequel se trouve le contrôleur.
454
     * Relatif à CONTROLLER_PATH
455
	 * 
456
	 * @deprecated 1.0
457
     */
458
    public function directory(): string
459
    {
460
        return ! empty($this->directory) ? $this->directory : '';
461
    }
462
463
    /**
464
     * Renvoie le nom du contrôleur matché
465
     *
466
     * @return closure|string
0 ignored issues
show
Bug introduced by
The type BlitzPHP\Router\closure was not found. Did you mean closure? If so, make sure to prefix the type with \.
Loading history...
467
     */
468
    private function controllerName()
469
    {
470
        if (! is_string($this->controller)) {
0 ignored issues
show
introduced by
The condition is_string($this->controller) is always true.
Loading history...
471
            return $this->controller;
472
        }
473
474
        return $this->translateURIDashes
475
            ? str_replace('-', '_', trim($this->controller, '/\\'))
476
            : Text::toPascalCase($this->controller);
477
    }
478
479
    /**
480
     * Retourne le nom de la méthode à exécuter
481
     */
482
    private function methodName(): string
483
    {
484
        return $this->translateURIDashes
485
            ? str_replace('-', '_', $this->method)
486
            : Text::toCamelCase($this->method);
487
    }
488
489
    /**
490
     * Construit un nom de contrôleur valide
491
	 * 
492
	 * @deprecated 1.0
493
     */
494
    public function makeController(string $name): string
495
    {
496
        return preg_replace(
497
            ['#(\_)?Controller$#i', '#' . config('app.url_suffix') . '$#i'],
0 ignored issues
show
Bug introduced by
Are you sure config('app.url_suffix') of type BlitzPHP\Config\Config|null can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

497
            ['#(\_)?Controller$#i', '#' . /** @scrutinizer ignore-type */ config('app.url_suffix') . '$#i'],
Loading history...
498
            '',
499
            ucfirst($name)
500
        ) . 'Controller';
501
    }
502
}
503