Viewing file: AutoRouterImproved.php (10.05 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
/** * This file is part of CodeIgniter 4 framework. * * (c) CodeIgniter Foundation <admin@codeigniter.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */
namespace CodeIgniter\Router;
use CodeIgniter\Exceptions\PageNotFoundException; use ReflectionClass; use ReflectionException;
/** * New Secure Router for Auto-Routing */ final class AutoRouterImproved implements AutoRouterInterface { /** * List of controllers in Defined Routes that should not be accessed via this Auto-Routing. * * @var class-string[] */ private array $protectedControllers;
/** * Sub-directory that contains the requested controller class. */ private ?string $directory = null;
/** * Sub-namespace that contains the requested controller class. */ private ?string $subNamespace = null;
/** * The name of the controller class. */ private string $controller;
/** * The name of the method to use. */ private string $method;
/** * An array of params to the controller method. */ private array $params = [];
/** * Whether dashes in URI's should be converted * to underscores when determining method names. */ private bool $translateURIDashes;
/** * HTTP verb for the request. */ private string $httpVerb;
/** * The namespace for controllers. */ private string $namespace;
/** * The name of the default controller class. */ private string $defaultController;
/** * The name of the default method */ private string $defaultMethod;
/** * @param class-string[] $protectedControllers * @param string $defaultController Short classname */ public function __construct( array $protectedControllers, string $namespace, string $defaultController, string $defaultMethod, bool $translateURIDashes, string $httpVerb ) { $this->protectedControllers = $protectedControllers; $this->namespace = rtrim($namespace, '\\') . '\\'; $this->translateURIDashes = $translateURIDashes; $this->httpVerb = $httpVerb; $this->defaultController = $defaultController; $this->defaultMethod = $httpVerb . ucfirst($defaultMethod);
// Set the default values $this->controller = $this->defaultController; $this->method = $this->defaultMethod; }
/** * Finds controller, method and params from the URI. * * @return array [directory_name, controller_name, controller_method, params] */ public function getRoute(string $uri): array { $segments = explode('/', $uri);
// WARNING: Directories get shifted out of the segments array. $nonDirSegments = $this->scanControllers($segments);
$controllerSegment = ''; $baseControllerName = $this->defaultController;
// If we don't have any segments left - use the default controller; // If not empty, then the first segment should be the controller if (! empty($nonDirSegments)) { $controllerSegment = array_shift($nonDirSegments);
$baseControllerName = $this->translateURIDashes(ucfirst($controllerSegment)); }
if (! $this->isValidSegment($baseControllerName)) { throw new PageNotFoundException($baseControllerName . ' is not a valid controller name'); }
// Prevent access to default controller path if ( strtolower($baseControllerName) === strtolower($this->defaultController) && strtolower($controllerSegment) === strtolower($this->defaultController) ) { throw new PageNotFoundException( 'Cannot access the default controller "' . $baseControllerName . '" with the controller name URI path.' ); }
// Use the method name if it exists. if (! empty($nonDirSegments)) { $methodSegment = $this->translateURIDashes(array_shift($nonDirSegments));
// Prefix HTTP verb $this->method = $this->httpVerb . ucfirst($methodSegment);
// Prevent access to default method path if (strtolower($this->method) === strtolower($this->defaultMethod)) { throw new PageNotFoundException( 'Cannot access the default method "' . $this->method . '" with the method name URI path.' ); } }
if (! empty($nonDirSegments)) { $this->params = $nonDirSegments; }
// Ensure the controller stores the fully-qualified class name $this->controller = '\\' . ltrim( str_replace( '/', '\\', $this->namespace . $this->subNamespace . $baseControllerName ), '\\' );
// Ensure routes registered via $routes->cli() are not accessible via web. $this->protectDefinedRoutes();
// Check _remap() $this->checkRemap();
// Check parameters try { $this->checkParameters($uri); } catch (ReflectionException $e) { throw PageNotFoundException::forControllerNotFound($this->controller, $this->method); }
return [$this->directory, $this->controller, $this->method, $this->params]; }
private function protectDefinedRoutes(): void { $controller = strtolower($this->controller);
foreach ($this->protectedControllers as $controllerInRoutes) { $routeLowerCase = strtolower($controllerInRoutes);
if ($routeLowerCase === $controller) { throw new PageNotFoundException( 'Cannot access the controller in Defined Routes. Controller: ' . $controllerInRoutes ); } } }
private function checkParameters(string $uri): void { $refClass = new ReflectionClass($this->controller); $refMethod = $refClass->getMethod($this->method); $refParams = $refMethod->getParameters();
if (! $refMethod->isPublic()) { throw PageNotFoundException::forMethodNotFound($this->method); }
if (count($refParams) < count($this->params)) { throw new PageNotFoundException( 'The param count in the URI are greater than the controller method params.' . ' Handler:' . $this->controller . '::' . $this->method . ', URI:' . $uri ); } }
private function checkRemap(): void { try { $refClass = new ReflectionClass($this->controller); $refClass->getMethod('_remap');
throw new PageNotFoundException( 'AutoRouterImproved does not support `_remap()` method.' . ' Controller:' . $this->controller ); } catch (ReflectionException $e) { // Do nothing. } }
/** * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments * * @param array $segments URI segments * * @return array returns an array of remaining uri segments that don't map onto a directory */ private function scanControllers(array $segments): array { $segments = array_filter($segments, static fn ($segment) => $segment !== ''); // numerically reindex the array, removing gaps $segments = array_values($segments);
// Loop through our segments and return as soon as a controller // is found or when such a directory doesn't exist $c = count($segments);
while ($c-- > 0) { $segmentConvert = ucfirst( $this->translateURIDashes === true ? str_replace('-', '_', $segments[0]) : $segments[0] );
// as soon as we encounter any segment that is not PSR-4 compliant, stop searching if (! $this->isValidSegment($segmentConvert)) { return $segments; }
$test = $this->namespace . $this->subNamespace . $segmentConvert;
// as long as each segment is *not* a controller file, add it to $this->subNamespace if (! class_exists($test)) { $this->setSubNamespace($segmentConvert, true, false); array_shift($segments);
$this->directory .= $this->directory . $segmentConvert . '/';
continue; }
return $segments; }
// This means that all segments were actually directories return $segments; }
/** * Returns true if the supplied $segment string represents a valid PSR-4 compliant namespace/directory segment * * regex comes from https://www.php.net/manual/en/language.variables.basics.php */ private function isValidSegment(string $segment): bool { return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment); }
/** * Sets the sub-namespace that the controller is in. * * @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments */ private function setSubNamespace(?string $namespace = null, bool $append = false, bool $validate = true): void { if ($validate) { $segments = explode('/', trim($namespace, '/'));
foreach ($segments as $segment) { if (! $this->isValidSegment($segment)) { return; } } }
if ($append !== true || empty($this->subNamespace)) { $this->subNamespace = trim($namespace, '/') . '\\'; } else { $this->subNamespace .= trim($namespace, '/') . '\\'; } }
private function translateURIDashes(string $classname): string { return $this->translateURIDashes ? str_replace('-', '_', $classname) : $classname; } }
|