first commit

This commit is contained in:
DESKTOP-DH6BVPV\chiefsoft
2022-11-20 09:46:33 -05:00
commit 6ddfa92eed
618 changed files with 104537 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
<?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;
/**
* Router for Auto-Routing
*/
final class AutoRouter implements AutoRouterInterface
{
/**
* List of controllers registered for the CLI verb that should not be accessed in the web.
*
* @var class-string[]
*/
private array $protectedControllers;
/**
* Sub-directory that contains the requested controller class.
* Primarily used by 'autoRoute'.
*/
private ?string $directory = null;
/**
* The name of the controller class.
*/
private string $controller;
/**
* The name of the method to use.
*/
private string $method;
/**
* 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;
/**
* Default namespace for controllers.
*/
private string $defaultNamespace;
public function __construct(
array $protectedControllers,
string $defaultNamespace,
string $defaultController,
string $defaultMethod,
bool $translateURIDashes,
string $httpVerb
) {
$this->protectedControllers = $protectedControllers;
$this->defaultNamespace = $defaultNamespace;
$this->translateURIDashes = $translateURIDashes;
$this->httpVerb = $httpVerb;
$this->controller = $defaultController;
$this->method = $defaultMethod;
}
/**
* Attempts to match a URI path against Controllers and directories
* found in APPPATH/Controllers, to find a matching route.
*
* @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.
$segments = $this->scanControllers($segments);
// 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($segments)) {
$this->controller = ucfirst(array_shift($segments));
}
$controllerName = $this->controllerName();
if (! $this->isValidSegment($controllerName)) {
throw new PageNotFoundException($this->controller . ' is not a valid controller name');
}
// Use the method name if it exists.
// If it doesn't, no biggie - the default method name
// has already been set.
if (! empty($segments)) {
$this->method = array_shift($segments) ?: $this->method;
}
// Prevent access to initController method
if (strtolower($this->method) === 'initcontroller') {
throw PageNotFoundException::forPageNotFound();
}
/** @var array $params An array of params to the controller method. */
$params = [];
if (! empty($segments)) {
$params = $segments;
}
// Ensure routes registered via $routes->cli() are not accessible via web.
if ($this->httpVerb !== 'cli') {
$controller = '\\' . $this->defaultNamespace;
$controller .= $this->directory ? str_replace('/', '\\', $this->directory) : '';
$controller .= $controllerName;
$controller = strtolower($controller);
foreach ($this->protectedControllers as $controllerInRoute) {
if (! is_string($controllerInRoute)) {
continue;
}
if (strtolower($controllerInRoute) !== $controller) {
continue;
}
throw new PageNotFoundException(
'Cannot access the controller in a CLI Route. Controller: ' . $controllerInRoute
);
}
}
// Load the file so that it's available for CodeIgniter.
$file = APPPATH . 'Controllers/' . $this->directory . $controllerName . '.php';
if (is_file($file)) {
include_once $file;
}
// Ensure the controller stores the fully-qualified class name
// We have to check for a length over 1, since by default it will be '\'
if (strpos($this->controller, '\\') === false && strlen($this->defaultNamespace) > 1) {
$this->controller = '\\' . ltrim(
str_replace(
'/',
'\\',
$this->defaultNamespace . $this->directory . $controllerName
),
'\\'
);
}
return [$this->directory, $this->controllerName(), $this->methodName(), $params];
}
/**
* Tells the system whether we should translate URI dashes or not
* in the URI from a dash to an underscore.
*
* @deprecated This method should be removed.
*/
public function setTranslateURIDashes(bool $val = false): self
{
$this->translateURIDashes = $val;
return $this;
}
/**
* 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);
// if a prior directory value has been set, just return segments and get out of here
if (isset($this->directory)) {
return $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 ? 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 = APPPATH . 'Controllers/' . $this->directory . $segmentConvert;
// as long as each segment is *not* a controller file but does match a directory, add it to $this->directory
if (! is_file($test . '.php') && is_dir($test)) {
$this->setDirectory($segmentConvert, true, false);
array_shift($segments);
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-directory that the controller is in.
*
* @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments
*
* @deprecated This method should be removed.
*/
public function setDirectory(?string $dir = null, bool $append = false, bool $validate = true)
{
if (empty($dir)) {
$this->directory = null;
return;
}
if ($validate) {
$segments = explode('/', trim($dir, '/'));
foreach ($segments as $segment) {
if (! $this->isValidSegment($segment)) {
return;
}
}
}
if ($append !== true || empty($this->directory)) {
$this->directory = trim($dir, '/') . '/';
} else {
$this->directory .= trim($dir, '/') . '/';
}
}
/**
* Returns the name of the sub-directory the controller is in,
* if any. Relative to APPPATH.'Controllers'.
*
* @deprecated This method should be removed.
*/
public function directory(): string
{
return ! empty($this->directory) ? $this->directory : '';
}
private function controllerName(): string
{
return $this->translateURIDashes
? str_replace('-', '_', $this->controller)
: $this->controller;
}
private function methodName(): string
{
return $this->translateURIDashes
? str_replace('-', '_', $this->method)
: $this->method;
}
}
+320
View File
@@ -0,0 +1,320 @@
<?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 = $this->translateURIDashes(ucfirst($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;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?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;
/**
* Expected behavior of a AutoRouter.
*/
interface AutoRouterInterface
{
/**
* Returns controller, method and params from the URI.
*
* @return array [directory_name, controller_name, controller_method, params]
*/
public function getRoute(string $uri): array;
}
@@ -0,0 +1,27 @@
<?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\Exceptions;
use Exception;
/**
* RedirectException
*/
class RedirectException extends Exception
{
/**
* HTTP status code for redirects
*
* @var int
*/
protected $code = 302;
}
@@ -0,0 +1,81 @@
<?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\Exceptions;
use CodeIgniter\Exceptions\FrameworkException;
/**
* RouterException
*/
class RouterException extends FrameworkException
{
/**
* Thrown when the actual parameter type does not match
* the expected types.
*
* @return RouterException
*/
public static function forInvalidParameterType()
{
return new static(lang('Router.invalidParameterType'));
}
/**
* Thrown when a default route is not set.
*
* @return RouterException
*/
public static function forMissingDefaultRoute()
{
return new static(lang('Router.missingDefaultRoute'));
}
/**
* Throw when controller or its method is not found.
*
* @return RouterException
*/
public static function forControllerNotFound(string $controller, string $method)
{
return new static(lang('HTTP.controllerNotFound', [$controller, $method]));
}
/**
* Throw when route is not valid.
*
* @return RouterException
*/
public static function forInvalidRoute(string $route)
{
return new static(lang('HTTP.invalidRoute', [$route]));
}
/**
* Throw when dynamic controller.
*
* @return RouterException
*/
public static function forDynamicController(string $handler)
{
return new static(lang('Router.invalidDynamicController', [$handler]));
}
/**
* Throw when controller name has `/`.
*
* @return RouterException
*/
public static function forInvalidControllerName(string $handler)
{
return new static(lang('Router.invalidControllerName', [$handler]));
}
}
File diff suppressed because it is too large Load Diff
+188
View File
@@ -0,0 +1,188 @@
<?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 Closure;
/**
* Interface RouteCollectionInterface
*
* A Route Collection's sole job is to hold a series of routes. The required
* number of methods is kept very small on purpose, but implementors may
* add a number of additional methods to customize how the routes are defined.
*
* The RouteCollection provides the Router with the routes so that it can determine
* which controller should be ran.
*/
interface RouteCollectionInterface
{
/**
* Adds a single route to the collection.
*
* @param array|Closure|string $to
* @param array $options
*
* @return mixed
*/
public function add(string $from, $to, ?array $options = null);
/**
* Registers a new constraint with the system. Constraints are used
* by the routes as placeholders for regular expressions to make defining
* the routes more human-friendly.
*
* You can pass an associative array as $placeholder, and have
* multiple placeholders added at once.
*
* @param array|string $placeholder
* @param string $pattern
*
* @return mixed
*/
public function addPlaceholder($placeholder, ?string $pattern = null);
/**
* Sets the default namespace to use for Controllers when no other
* namespace has been specified.
*
* @return mixed
*/
public function setDefaultNamespace(string $value);
/**
* Sets the default controller to use when no other controller has been
* specified.
*
* @return mixed
*/
public function setDefaultController(string $value);
/**
* Sets the default method to call on the controller when no other
* method has been set in the route.
*
* @return mixed
*/
public function setDefaultMethod(string $value);
/**
* Tells the system whether to convert dashes in URI strings into
* underscores. In some search engines, including Google, dashes
* create more meaning and make it easier for the search engine to
* find words and meaning in the URI for better SEO. But it
* doesn't work well with PHP method names....
*
* @return mixed
*/
public function setTranslateURIDashes(bool $value);
/**
* If TRUE, the system will attempt to match the URI against
* Controllers by matching each segment against folders/files
* in APPPATH/Controllers, when a match wasn't found against
* defined routes.
*
* If FALSE, will stop searching and do NO automatic routing.
*/
public function setAutoRoute(bool $value): self;
/**
* Sets the class/method that should be called if routing doesn't
* find a match. It can be either a closure or the controller/method
* name exactly like a route is defined: Users::index
*
* This setting is passed to the Router class and handled there.
*
* @param callable|null $callable
*/
public function set404Override($callable = null): self;
/**
* Returns the 404 Override setting, which can be null, a closure
* or the controller/string.
*
* @return Closure|string|null
*/
public function get404Override();
/**
* Returns the name of the default controller. With Namespace.
*
* @return string
*/
public function getDefaultController();
/**
* Returns the name of the default method to use within the controller.
*
* @return string
*/
public function getDefaultMethod();
/**
* Returns the current value of the translateURIDashes setting.
*
* @return bool
*/
public function shouldTranslateURIDashes();
/**
* Returns the flag that tells whether to autoRoute URI against Controllers.
*
* @return bool
*/
public function shouldAutoRoute();
/**
* Returns the raw array of available routes.
*
* @return array
*/
public function getRoutes();
/**
* Returns the current HTTP Verb being used.
*
* @return string
*/
public function getHTTPVerb();
/**
* Attempts to look up a route based on it's destination.
*
* If a route exists:
*
* 'path/(:any)/(:any)' => 'Controller::method/$1/$2'
*
* This method allows you to know the Controller and method
* and get the route that leads to it.
*
* // Equals 'path/$param1/$param2'
* reverseRoute('Controller::method', $param1, $param2);
*
* @param string $search Named route or Controller::method
* @param int|string ...$params
*
* @return false|string
*/
public function reverseRoute(string $search, ...$params);
/**
* Determines if the route is a redirecting route.
*/
public function isRedirect(string $from): bool;
/**
* Grabs the HTTP status code from a redirecting Route.
*/
public function getRedirectCode(string $from): int;
}
+659
View File
@@ -0,0 +1,659 @@
<?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 Closure;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Request;
use CodeIgniter\Router\Exceptions\RedirectException;
use CodeIgniter\Router\Exceptions\RouterException;
/**
* Request router.
*/
class Router implements RouterInterface
{
/**
* A RouteCollection instance.
*
* @var RouteCollectionInterface
*/
protected $collection;
/**
* Sub-directory that contains the requested controller class.
* Primarily used by 'autoRoute'.
*
* @var string|null
*/
protected $directory;
/**
* The name of the controller class.
*
* @var Closure|string
*/
protected $controller;
/**
* The name of the method to use.
*
* @var string
*/
protected $method;
/**
* An array of binds that were collected
* so they can be sent to closure routes.
*
* @var array
*/
protected $params = [];
/**
* The name of the front controller.
*
* @var string
*/
protected $indexPage = 'index.php';
/**
* Whether dashes in URI's should be converted
* to underscores when determining method names.
*
* @var bool
*/
protected $translateURIDashes = false;
/**
* The route that was matched for this request.
*
* @var array|null
*/
protected $matchedRoute;
/**
* The options set for the matched route.
*
* @var array|null
*/
protected $matchedRouteOptions;
/**
* The locale that was detected in a route.
*
* @var string
*/
protected $detectedLocale;
/**
* The filter info from Route Collection
* if the matched route should be filtered.
*
* @var string|null
*
* @deprecated Use $filtersInfo
*/
protected $filterInfo;
/**
* The filter info from Route Collection
* if the matched route should be filtered.
*
* @var string[]
*/
protected $filtersInfo = [];
protected ?AutoRouterInterface $autoRouter = null;
/**
* Stores a reference to the RouteCollection object.
*/
public function __construct(RouteCollectionInterface $routes, ?Request $request = null)
{
$this->collection = $routes;
// These are only for auto-routing
$this->controller = $this->collection->getDefaultController();
$this->method = $this->collection->getDefaultMethod();
$this->collection->setHTTPVerb(strtolower($request->getMethod() ?? $_SERVER['REQUEST_METHOD']));
$this->translateURIDashes = $this->collection->shouldTranslateURIDashes();
if ($this->collection->shouldAutoRoute()) {
$autoRoutesImproved = config('Feature')->autoRoutesImproved ?? false;
if ($autoRoutesImproved) {
$this->autoRouter = new AutoRouterImproved(
$this->collection->getRegisteredControllers('*'),
$this->collection->getDefaultNamespace(),
$this->collection->getDefaultController(),
$this->collection->getDefaultMethod(),
$this->translateURIDashes,
$this->collection->getHTTPVerb()
);
} else {
$this->autoRouter = new AutoRouter(
$this->collection->getRegisteredControllers('cli'),
$this->collection->getDefaultNamespace(),
$this->collection->getDefaultController(),
$this->collection->getDefaultMethod(),
$this->translateURIDashes,
$this->collection->getHTTPVerb()
);
}
}
}
/**
* @return Closure|string Controller classname or Closure
*
* @throws PageNotFoundException
* @throws RedirectException
*/
public function handle(?string $uri = null)
{
// If we cannot find a URI to match against, then
// everything runs off of its default settings.
if ($uri === null || $uri === '') {
return strpos($this->controller, '\\') === false
? $this->collection->getDefaultNamespace() . $this->controller
: $this->controller;
}
// Decode URL-encoded string
$uri = urldecode($uri);
// Restart filterInfo
$this->filterInfo = null;
$this->filtersInfo = [];
// Checks defined routes
if ($this->checkRoutes($uri)) {
if ($this->collection->isFiltered($this->matchedRoute[0])) {
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
if ($multipleFiltersEnabled) {
$this->filtersInfo = $this->collection->getFiltersForRoute($this->matchedRoute[0]);
} else {
// for backward compatibility
$this->filterInfo = $this->collection->getFilterForRoute($this->matchedRoute[0]);
}
}
return $this->controller;
}
// Still here? Then we can try to match the URI against
// Controllers/directories, but the application may not
// want this, like in the case of API's.
if (! $this->collection->shouldAutoRoute()) {
throw new PageNotFoundException(
"Can't find a route for '{$this->collection->getHTTPVerb()}: {$uri}'."
);
}
// Checks auto routes
$this->autoRoute($uri);
return $this->controllerName();
}
/**
* Returns the filter info for the matched route, if any.
*
* @return string|null
*
* @deprecated Use getFilters()
*/
public function getFilter()
{
return $this->filterInfo;
}
/**
* Returns the filter info for the matched route, if any.
*
* @return string[]
*/
public function getFilters(): array
{
return $this->filtersInfo;
}
/**
* Returns the name of the matched controller.
*
* @return Closure|string Controller classname or Closure
*/
public function controllerName()
{
return $this->translateURIDashes
? str_replace('-', '_', $this->controller)
: $this->controller;
}
/**
* Returns the name of the method to run in the
* chosen container.
*/
public function methodName(): string
{
return $this->translateURIDashes
? str_replace('-', '_', $this->method)
: $this->method;
}
/**
* Returns the 404 Override settings from the Collection.
* If the override is a string, will split to controller/index array.
*/
public function get404Override()
{
$route = $this->collection->get404Override();
if (is_string($route)) {
$routeArray = explode('::', $route);
return [
$routeArray[0], // Controller
$routeArray[1] ?? 'index', // Method
];
}
if (is_callable($route)) {
return $route;
}
return null;
}
/**
* Returns the binds that have been matched and collected
* during the parsing process as an array, ready to send to
* instance->method(...$params).
*/
public function params(): array
{
return $this->params;
}
/**
* Returns the name of the sub-directory the controller is in,
* if any. Relative to APPPATH.'Controllers'.
*
* Only used when auto-routing is turned on.
*/
public function directory(): string
{
if ($this->autoRouter instanceof AutoRouter) {
return $this->autoRouter->directory();
}
return '';
}
/**
* Returns the routing information that was matched for this
* request, if a route was defined.
*
* @return array|null
*/
public function getMatchedRoute()
{
return $this->matchedRoute;
}
/**
* Returns all options set for the matched route
*
* @return array|null
*/
public function getMatchedRouteOptions()
{
return $this->matchedRouteOptions;
}
/**
* Sets the value that should be used to match the index.php file. Defaults
* to index.php but this allows you to modify it in case your are using
* something like mod_rewrite to remove the page. This allows you to set
* it a blank.
*
* @param string $page
*/
public function setIndexPage($page): self
{
$this->indexPage = $page;
return $this;
}
/**
* Tells the system whether we should translate URI dashes or not
* in the URI from a dash to an underscore.
*
* @deprecated This method should be removed.
*/
public function setTranslateURIDashes(bool $val = false): self
{
if ($this->autoRouter instanceof AutoRouter) {
$this->autoRouter->setTranslateURIDashes($val);
return $this;
}
return $this;
}
/**
* Returns true/false based on whether the current route contained
* a {locale} placeholder.
*
* @return bool
*/
public function hasLocale()
{
return (bool) $this->detectedLocale;
}
/**
* Returns the detected locale, if any, or null.
*
* @return string
*/
public function getLocale()
{
return $this->detectedLocale;
}
/**
* Checks Defined Routs.
*
* Compares the uri string against the routes that the
* RouteCollection class defined for us, attempting to find a match.
* This method will modify $this->controller, etal as needed.
*
* @param string $uri The URI path to compare against the routes
*
* @return bool Whether the route was matched or not.
*
* @throws RedirectException
*/
protected function checkRoutes(string $uri): bool
{
$routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
// Don't waste any time
if (empty($routes)) {
return false;
}
$uri = $uri === '/'
? $uri
: trim($uri, '/ ');
// Loop through the route array looking for wildcards
foreach ($routes as $routeKey => $handler) {
$routeKey = $routeKey === '/'
? $routeKey
: ltrim($routeKey, '/ ');
$matchedKey = $routeKey;
// Are we dealing with a locale?
if (strpos($routeKey, '{locale}') !== false) {
$routeKey = str_replace('{locale}', '[^/]+', $routeKey);
}
// Does the RegEx match?
if (preg_match('#^' . $routeKey . '$#u', $uri, $matches)) {
// Is this route supposed to redirect to another?
if ($this->collection->isRedirect($routeKey)) {
// replacing matched route groups with references: post/([0-9]+) -> post/$1
$redirectTo = preg_replace_callback('/(\([^\(]+\))/', static function () {
static $i = 1;
return '$' . $i++;
}, is_array($handler) ? key($handler) : $handler);
throw new RedirectException(
preg_replace('#^' . $routeKey . '$#u', $redirectTo, $uri),
$this->collection->getRedirectCode($routeKey)
);
}
// Store our locale so CodeIgniter object can
// assign it to the Request.
if (strpos($matchedKey, '{locale}') !== false) {
preg_match(
'#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
$uri,
$matched
);
$this->detectedLocale = $matched['locale'];
unset($matched);
}
// Are we using Closures? If so, then we need
// to collect the params into an array
// so it can be passed to the controller method later.
if (! is_string($handler) && is_callable($handler)) {
$this->controller = $handler;
// Remove the original string from the matches array
array_shift($matches);
$this->params = $matches;
$this->setMatchedRoute($matchedKey, $handler);
return true;
}
[$controller] = explode('::', $handler);
// Checks `/` in controller name
if (strpos($controller, '/') !== false) {
throw RouterException::forInvalidControllerName($handler);
}
if (strpos($handler, '$') !== false && strpos($routeKey, '(') !== false) {
// Checks dynamic controller
if (strpos($controller, '$') !== false) {
throw RouterException::forDynamicController($handler);
}
// Using back-references
$handler = preg_replace('#^' . $routeKey . '$#u', $handler, $uri);
}
$this->setRequest(explode('/', $handler));
$this->setMatchedRoute($matchedKey, $handler);
return true;
}
}
return false;
}
/**
* Checks Auto Routs.
*
* Attempts to match a URI path against Controllers and directories
* found in APPPATH/Controllers, to find a matching route.
*/
public function autoRoute(string $uri)
{
[$this->directory, $this->controller, $this->method, $this->params]
= $this->autoRouter->getRoute($uri);
}
/**
* 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
*
* @deprecated this function name does not properly describe its behavior so it has been deprecated
*
* @codeCoverageIgnore
*/
protected function validateRequest(array $segments): array
{
return $this->scanControllers($segments);
}
/**
* 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
*
* @deprecated Not used. Moved to AutoRouter class.
*/
protected function scanControllers(array $segments): array
{
$segments = array_filter($segments, static fn ($segment) => $segment !== '');
// numerically reindex the array, removing gaps
$segments = array_values($segments);
// if a prior directory value has been set, just return segments and get out of here
if (isset($this->directory)) {
return $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 = APPPATH . 'Controllers/' . $this->directory . $segmentConvert;
// as long as each segment is *not* a controller file but does match a directory, add it to $this->directory
if (! is_file($test . '.php') && is_dir($test)) {
$this->setDirectory($segmentConvert, true, false);
array_shift($segments);
continue;
}
return $segments;
}
// This means that all segments were actually directories
return $segments;
}
/**
* Sets the sub-directory that the controller is in.
*
* @param bool $validate if true, checks to make sure $dir consists of only PSR4 compliant segments
*
* @deprecated This method should be removed.
*/
public function setDirectory(?string $dir = null, bool $append = false, bool $validate = true)
{
if (empty($dir)) {
$this->directory = null;
return;
}
if ($this->autoRouter instanceof AutoRouter) {
$this->autoRouter->setDirectory($dir, $append, $validate);
}
}
/**
* 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
*
* @deprecated Moved to AutoRouter class.
*/
private function isValidSegment(string $segment): bool
{
return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment);
}
/**
* Set request route
*
* Takes an array of URI segments as input and sets the class/method
* to be called.
*
* @param array $segments URI segments
*/
protected function setRequest(array $segments = [])
{
// If we don't have any segments - use the default controller;
if (empty($segments)) {
return;
}
[$controller, $method] = array_pad(explode('::', $segments[0]), 2, null);
$this->controller = $controller;
// $this->method already contains the default method name,
// so don't overwrite it with emptiness.
if (! empty($method)) {
$this->method = $method;
}
array_shift($segments);
$this->params = $segments;
}
/**
* Sets the default controller based on the info set in the RouteCollection.
*
* @deprecated This was an unnecessary method, so it is no longer used.
*/
protected function setDefaultController()
{
if (empty($this->controller)) {
throw RouterException::forMissingDefaultRoute();
}
sscanf($this->controller, '%[^/]/%s', $class, $this->method);
if (! is_file(APPPATH . 'Controllers/' . $this->directory . ucfirst($class) . '.php')) {
return;
}
$this->controller = ucfirst($class);
log_message('info', 'Used the default controller.');
}
/**
* @param callable|string $handler
*/
protected function setMatchedRoute(string $route, $handler): void
{
$this->matchedRoute = [$route, $handler];
$this->matchedRouteOptions = $this->collection->getRoutesOptions($route);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?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 Closure;
use CodeIgniter\HTTP\Request;
/**
* Expected behavior of a Router.
*/
interface RouterInterface
{
/**
* Stores a reference to the RouteCollection object.
*/
public function __construct(RouteCollectionInterface $routes, ?Request $request = null);
/**
* Finds the controller method corresponding to the URI.
*
* @param string $uri
*
* @return Closure|string Controller classname or Closure
*/
public function handle(?string $uri = null);
/**
* Returns the name of the matched controller.
*
* @return Closure|string Controller classname or Closure
*/
public function controllerName();
/**
* Returns the name of the method in the controller to run.
*
* @return string
*/
public function methodName();
/**
* Returns the binds that have been matched and collected
* during the parsing process as an array, ready to send to
* instance->method(...$params).
*
* @return array
*/
public function params();
/**
* Sets the value that should be used to match the index.php file. Defaults
* to index.php but this allows you to modify it in case you are using
* something like mod_rewrite to remove the page. This allows you to set
* it a blank.
*
* @param string $page
*
* @return RouterInterface
*/
public function setIndexPage($page);
}