diff --git a/phpstan-baseline.neon.dist b/phpstan-baseline.neon.dist index 53d3710b7ea7..7887ee6a3b03 100644 --- a/phpstan-baseline.neon.dist +++ b/phpstan-baseline.neon.dist @@ -715,6 +715,11 @@ parameters: count: 1 path: system/Router/Router.php + - + message: "#^Call to an undefined method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRegisteredControllers\\(\\)\\.$#" + count: 1 + path: system/Router/Router.php + - message: "#^Expression on left side of \\?\\? is not nullable\\.$#" count: 1 @@ -722,7 +727,7 @@ parameters: - message: "#^Method CodeIgniter\\\\Router\\\\RouteCollectionInterface\\:\\:getRoutes\\(\\) invoked with 1 parameter, 0 required\\.$#" - count: 2 + count: 1 path: system/Router/Router.php - diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index 093687254c77..3bc71d632993 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -792,6 +792,8 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) /** * Determines the path to use for us to try to route to, based * on user input (setPath), or the CLI/IncomingRequest path. + * + * @return string */ protected function determinePath() { diff --git a/system/Router/AutoRouter.php b/system/Router/AutoRouter.php new file mode 100644 index 000000000000..bd55d7fae39c --- /dev/null +++ b/system/Router/AutoRouter.php @@ -0,0 +1,285 @@ + + * + * 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 + */ +class AutoRouter +{ + /** + * List of controllers registered for the CLI verb that should not be accessed in the web. + */ + protected array $protectedControllers; + + /** + * Sub-directory that contains the requested controller class. + * Primarily used by 'autoRoute'. + */ + protected ?string $directory = null; + + /** + * The name of the controller class. + */ + protected string $controller; + + /** + * The name of the method to use. + */ + protected string $method; + + /** + * Whether dashes in URI's should be converted + * to underscores when determining method names. + */ + protected bool $translateURIDashes; + + /** + * HTTP verb for the request. + */ + protected string $httpVerb; + + /** + * Default namespace for controllers. + */ + protected 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. + */ + 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 + */ + 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 ? 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 + */ + 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'. + */ + 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; + } +} diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index a1dd7b1a8fe7..4bb048991a99 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -105,6 +105,16 @@ class RouteCollection implements RouteCollectionInterface * An array of all routes and their mappings. * * @var array + * + * [ + * verb => [ + * routeName => [ + * 'route' => [ + * routeKey => handler, + * ] + * ] + * ], + * ] */ protected $routes = [ '*' => [], @@ -1403,4 +1413,45 @@ public function setPrioritize(bool $enabled = true) return $this; } + + /** + * Get all controllers in Route Handlers + * + * @param string|null $verb HTTP verb. `'*'` returns all controllers in any verb. + */ + public function getRegisteredControllers(?string $verb = '*'): array + { + $routes = []; + + if ($verb === '*') { + $rawRoutes = []; + + foreach ($this->defaultHTTPMethods as $tmpVerb) { + $rawRoutes = array_merge($rawRoutes, $this->routes[$tmpVerb]); + } + + foreach ($rawRoutes as $route) { + $key = key($route['route']); + $handler = $route['route'][$key]; + + $routes[$key] = $handler; + } + } else { + $routes = $this->getRoutes($verb); + } + + $controllers = []; + + foreach ($routes as $handler) { + if (! is_string($handler)) { + continue; + } + + [$controller] = explode('::', $handler, 2); + + $controllers[] = $controller; + } + + return array_unique($controllers); + } } diff --git a/system/Router/RouteCollectionInterface.php b/system/Router/RouteCollectionInterface.php index a55b5d80a0b9..e5e4350a5db6 100644 --- a/system/Router/RouteCollectionInterface.php +++ b/system/Router/RouteCollectionInterface.php @@ -131,7 +131,7 @@ public function getDefaultMethod(); /** * Returns the current value of the translateURIDashes setting. * - * @return mixed + * @return bool */ public function shouldTranslateURIDashes(); @@ -145,7 +145,7 @@ public function shouldAutoRoute(); /** * Returns the raw array of available routes. * - * @return mixed + * @return array */ public function getRoutes(); diff --git a/system/Router/Router.php b/system/Router/Router.php index 37c88114172e..1b9ec718ff11 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -113,33 +113,45 @@ class Router implements RouterInterface */ protected $filtersInfo = []; + protected ?AutoRouter $autoRouter = null; + /** * Stores a reference to the RouteCollection object. - * - * @param Request $request */ 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($request->getMethod() ?? strtolower($_SERVER['REQUEST_METHOD'])); + + $this->translateURIDashes = $this->collection->shouldTranslateURIDashes(); + + if ($this->collection->shouldAutoRoute()) { + $this->autoRouter = new AutoRouter( + $this->collection->getRegisteredControllers('cli'), + $this->collection->getDefaultNamespace(), + $this->collection->getDefaultController(), + $this->collection->getDefaultMethod(), + $this->translateURIDashes, + $this->collection->getHTTPVerb() + ); + } } /** * @throws PageNotFoundException * @throws RedirectException * - * @return mixed|string + * @return Closure|string Controller classname or Closure */ public function handle(?string $uri = null) { - $this->translateURIDashes = $this->collection->shouldTranslateURIDashes(); - // If we cannot find a URI to match against, then - // everything runs off of it's default settings. + // everything runs off of its default settings. if ($uri === null || $uri === '') { return strpos($this->controller, '\\') === false ? $this->collection->getDefaultNamespace() . $this->controller @@ -153,6 +165,7 @@ public function handle(?string $uri = null) $this->filterInfo = null; $this->filtersInfo = []; + // Checks defined routes if ($this->checkRoutes($uri)) { if ($this->collection->isFiltered($this->matchedRoute[0])) { $multipleFiltersEnabled = config('Feature')->multipleFilters ?? false; @@ -174,6 +187,7 @@ public function handle(?string $uri = null) throw new PageNotFoundException("Can't find a route for '{$uri}'."); } + // Checks auto routes $this->autoRoute($uri); return $this->controllerName(); @@ -204,7 +218,7 @@ public function getFilters(): array /** * Returns the name of the matched controller. * - * @return Closure|string + * @return Closure|string Controller classname or Closure */ public function controllerName() { @@ -266,7 +280,11 @@ public function params(): array */ public function directory(): string { - return ! empty($this->directory) ? $this->directory : ''; + if ($this->autoRouter === null) { + return ''; + } + + return $this->autoRouter->directory(); } /** @@ -308,10 +326,16 @@ public function setIndexPage($page): self /** * Tells the system whether we should translate URI dashes or not * in the URI from a dash to an underscore. + * + * @deprecated Moved to AutoRouter class. */ public function setTranslateURIDashes(bool $val = false): self { - $this->translateURIDashes = $val; + if ($this->autoRouter === null) { + return $this; + } + + $this->autoRouter->setTranslateURIDashes($val); return $this; } @@ -338,6 +362,8 @@ public function getLocale() } /** + * 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. @@ -464,78 +490,15 @@ protected function checkRoutes(string $uri): bool } /** + * 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) { - $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(); - } - - if (! empty($segments)) { - $this->params = $segments; - } - - $defaultNamespace = $this->collection->getDefaultNamespace(); - if ($this->collection->getHTTPVerb() !== 'cli') { - $controller = '\\' . $defaultNamespace; - - $controller .= $this->directory ? str_replace('/', '\\', $this->directory) : ''; - $controller .= $controllerName; - - $controller = strtolower($controller); - $methodName = strtolower($this->methodName()); - - foreach ($this->collection->getRoutes('cli') as $route) { - if (is_string($route)) { - $route = strtolower($route); - if (strpos($route, $controller . '::' . $methodName) === 0) { - throw new PageNotFoundException(); - } - - if ($route === $controller) { - throw new PageNotFoundException(); - } - } - } - } - - // 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($defaultNamespace) > 1) { - $this->controller = '\\' . ltrim(str_replace('/', '\\', $defaultNamespace . $this->directory . $controllerName), '\\'); - } + [$this->directory, $this->controller, $this->method, $this->params] + = $this->autoRouter->getRoute($uri); } /** @@ -560,6 +523,8 @@ protected function validateRequest(array $segments): array * @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 { @@ -604,6 +569,8 @@ protected function scanControllers(array $segments): array * 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 Moved to AutoRouter class. */ public function setDirectory(?string $dir = null, bool $append = false, bool $validate = true) { @@ -613,27 +580,19 @@ public function setDirectory(?string $dir = null, bool $append = false, bool $va return; } - if ($validate) { - $segments = explode('/', trim($dir, '/')); - - foreach ($segments as $segment) { - if (! $this->isValidSegment($segment)) { - return; - } - } + if ($this->autoRouter === null) { + return; } - if ($append !== true || empty($this->directory)) { - $this->directory = trim($dir, '/') . '/'; - } else { - $this->directory .= trim($dir, '/') . '/'; - } + $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 { diff --git a/system/Router/RouterInterface.php b/system/Router/RouterInterface.php index e252b56decc8..1efeb2e2c6e2 100644 --- a/system/Router/RouterInterface.php +++ b/system/Router/RouterInterface.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Router; +use Closure; use CodeIgniter\HTTP\Request; /** @@ -20,33 +21,29 @@ interface RouterInterface { /** * Stores a reference to the RouteCollection object. - * - * @param Request $request */ public function __construct(RouteCollectionInterface $routes, ?Request $request = null); /** - * Scans the URI and attempts to match the current URI to the - * one of the defined routes in the RouteCollection. + * Finds the controller method corresponding to the URI. * * @param string $uri * - * @return mixed + * @return Closure|string Controller classname or Closure */ public function handle(?string $uri = null); /** * Returns the name of the matched controller. * - * @return mixed + * @return Closure|string Controller classname or Closure */ public function controllerName(); /** - * Returns the name of the method to run in the - * chosen container. + * Returns the name of the method in the controller to run. * - * @return mixed + * @return string */ public function methodName(); @@ -55,19 +52,19 @@ public function methodName(); * during the parsing process as an array, ready to send to * instance->method(...$params). * - * @return mixed + * @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 your are using + * 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 mixed + * @return RouterInterface */ public function setIndexPage($page); } diff --git a/tests/system/Database/Migrations/MigrationRunnerTest.php b/tests/system/Database/Migrations/MigrationRunnerTest.php index e97e4e38e56c..774137bc0f76 100644 --- a/tests/system/Database/Migrations/MigrationRunnerTest.php +++ b/tests/system/Database/Migrations/MigrationRunnerTest.php @@ -33,6 +33,12 @@ final class MigrationRunnerTest extends CIUnitTestCase use DatabaseTestTrait; protected $refresh = true; + + // Do not migrate automatically, because we test migrations. + protected $migrate = false; + + // Use specific migration files for this test case. + protected $namespace = 'Tests\Support\MigrationTestMigrations'; protected $root; protected $start; protected $config; @@ -62,6 +68,8 @@ protected function tearDown(): void { parent::tearDown(); + // To delete data with `$this->regressDatabase()`, set it true. + $this->migrate = true; $this->regressDatabase(); } @@ -342,10 +350,11 @@ public function testLatestTriggersEvent() { $runner = new MigrationRunner($this->config); $runner->setSilent(false) - ->setNamespace('Tests\Support\MigrationTestMigrations') - ->clearHistory(); + ->setNamespace('Tests\Support\MigrationTestMigrations'); $result = null; + + Events::removeAllListeners(); Events::on('migrate', static function ($arg) use (&$result) { $result = $arg; }); @@ -360,10 +369,10 @@ public function testRegressTriggersEvent() { $runner = new MigrationRunner($this->config); $runner->setSilent(false) - ->setNamespace('Tests\Support\MigrationTestMigrations') - ->clearHistory(); + ->setNamespace('Tests\Support\MigrationTestMigrations'); $result = null; + Events::removeAllListeners(); Events::on('migrate', static function ($arg) use (&$result) { $result = $arg; }); diff --git a/tests/system/Router/RouteCollectionTest.php b/tests/system/Router/RouteCollectionTest.php index 513658b57642..7eb3f99f3128 100644 --- a/tests/system/Router/RouteCollectionTest.php +++ b/tests/system/Router/RouteCollectionTest.php @@ -1674,4 +1674,97 @@ public function testRoutePriorityValue() $collection->add('string-negative-integer', 'Controller::method', ['priority' => '-1']); $this->assertSame(1, $collection->getRoutesOptions('string-negative-integer')['priority']); } + + public function testGetRegisteredControllersReturnsControllerForHTTPverb() + { + $collection = $this->getCollector(); + $collection->get('test', '\App\Controllers\Hello::get'); + $collection->post('test', '\App\Controllers\Hello::post'); + + $routes = $collection->getRegisteredControllers('get'); + + $expects = [ + '\App\Controllers\Hello', + ]; + $this->assertSame($expects, $routes); + + $routes = $collection->getRegisteredControllers('post'); + + $expects = [ + '\App\Controllers\Hello', + ]; + $this->assertSame($expects, $routes); + } + + public function testGetRegisteredControllersReturnsTwoControllers() + { + $collection = $this->getCollector(); + $collection->post('test', '\App\Controllers\Test::post'); + $collection->post('hello', '\App\Controllers\Hello::post'); + + $routes = $collection->getRegisteredControllers('post'); + + $expects = [ + '\App\Controllers\Test', + '\App\Controllers\Hello', + ]; + $this->assertSame($expects, $routes); + } + + public function testGetRegisteredControllersReturnsOneControllerWhenTwoRoutsWithDiffernetMethods() + { + $collection = $this->getCollector(); + $collection->post('test', '\App\Controllers\Test::test'); + $collection->post('hello', '\App\Controllers\Test::hello'); + + $routes = $collection->getRegisteredControllers('post'); + + $expects = [ + '\App\Controllers\Test', + ]; + $this->assertSame($expects, $routes); + } + + public function testGetRegisteredControllersReturnsAllControllers() + { + $collection = $this->getCollector(); + $collection->get('test', '\App\Controllers\Hello::get'); + $collection->post('test', '\App\Controllers\Hello::post'); + $collection->post('hello', '\App\Controllers\Test::hello'); + + $routes = $collection->getRegisteredControllers('*'); + + $expects = [ + '\App\Controllers\Hello', + '\App\Controllers\Test', + ]; + $this->assertSame($expects, $routes); + } + + public function testGetRegisteredControllersReturnsControllerByAddMethod() + { + $collection = $this->getCollector(); + $collection->get('test', '\App\Controllers\Hello::get'); + $collection->add('hello', '\App\Controllers\Test::hello'); + + $routes = $collection->getRegisteredControllers('get'); + + $expects = [ + '\App\Controllers\Hello', + '\App\Controllers\Test', + ]; + $this->assertSame($expects, $routes); + } + + public function testGetRegisteredControllersDoesNotReturnClosures() + { + $collection = $this->getCollector(); + $collection->get('feed', static function () { + }); + + $routes = $collection->getRegisteredControllers('*'); + + $expects = []; + $this->assertSame($expects, $routes); + } } diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 38eb9b7ba48c..be65a9d41460 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -200,6 +200,7 @@ public function testClosures() public function testAutoRouteFindsDefaultControllerAndMethod() { + $this->collection->setAutoRoute(true); $this->collection->setDefaultController('Test'); $this->collection->setDefaultMethod('test'); $router = new Router($this->collection, $this->request); @@ -212,6 +213,7 @@ public function testAutoRouteFindsDefaultControllerAndMethod() public function testAutoRouteFindsControllerWithFileAndMethod() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->autoRoute('myController/someMethod'); @@ -222,6 +224,7 @@ public function testAutoRouteFindsControllerWithFileAndMethod() public function testAutoRouteFindsControllerWithFile() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->autoRoute('myController'); @@ -232,6 +235,7 @@ public function testAutoRouteFindsControllerWithFile() public function testAutoRouteFindsControllerWithSubfolder() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); mkdir(APPPATH . 'Controllers/Subfolder'); @@ -246,6 +250,7 @@ public function testAutoRouteFindsControllerWithSubfolder() public function testAutoRouteFindsDashedSubfolder() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -262,6 +267,7 @@ public function testAutoRouteFindsDashedSubfolder() public function testAutoRouteFindsDashedController() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -280,6 +286,7 @@ public function testAutoRouteFindsDashedController() public function testAutoRouteFindsDashedMethod() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -298,6 +305,7 @@ public function testAutoRouteFindsDashedMethod() public function testAutoRouteFindsDefaultDashFolder() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -314,6 +322,7 @@ public function testAutoRouteFindsDefaultDashFolder() public function testAutoRouteFindsMByteDir() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -330,6 +339,7 @@ public function testAutoRouteFindsMByteDir() public function testAutoRouteFindsMByteController() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -345,6 +355,7 @@ public function testAutoRouteFindsMByteController() public function testAutoRouteRejectsSingleDot() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -355,6 +366,7 @@ public function testAutoRouteRejectsSingleDot() public function testAutoRouteRejectsDoubleDot() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -365,6 +377,7 @@ public function testAutoRouteRejectsDoubleDot() public function testAutoRouteRejectsMidDot() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -375,6 +388,7 @@ public function testAutoRouteRejectsMidDot() public function testAutoRouteRejectsInitController() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -706,6 +720,7 @@ public function testTranslateURIDashesForParams() */ public function testTranslateURIDashesForAutoRoute() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setTranslateURIDashes(true); @@ -720,6 +735,7 @@ public function testTranslateURIDashesForAutoRoute() */ public function testAutoRouteMatchesZeroParams() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->autoRoute('myController/someMethod/0/abc'); @@ -739,6 +755,7 @@ public function testAutoRouteMatchesZeroParams() */ public function testAutoRouteMethodEmpty() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $this->collection->setAutoRoute(true); @@ -789,8 +806,8 @@ public function testRegularExpressionPlaceholderWithUnicode() public function testRouterPriorDirectory() { - $router = new Router($this->collection, $this->request); $this->collection->setAutoRoute(true); + $router = new Router($this->collection, $this->request); $router->setDirectory('foo/bar/baz', false, true); $router->handle('Some_controller/some_method/param1/param2/param3'); @@ -802,6 +819,7 @@ public function testRouterPriorDirectory() public function testSetDirectoryValid() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setDirectory('foo/bar/baz', false, true); @@ -810,6 +828,7 @@ public function testSetDirectoryValid() public function testSetDirectoryInvalid() { + $this->collection->setAutoRoute(true); $router = new Router($this->collection, $this->request); $router->setDirectory('foo/bad-segment/bar', false, true); diff --git a/tests/system/Test/FeatureTestTraitTest.php b/tests/system/Test/FeatureTestTraitTest.php index 0a424aff4ad2..4636d58d3927 100644 --- a/tests/system/Test/FeatureTestTraitTest.php +++ b/tests/system/Test/FeatureTestTraitTest.php @@ -11,13 +11,13 @@ namespace CodeIgniter\Test; +use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\HTTP\Response; +use Config\Services; /** - * @group DatabaseLive - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled + * @group DatabaseLive * * @internal */ @@ -32,6 +32,15 @@ protected function setUp(): void $this->skipEvents(); } + protected function tearDown(): void + { + parent::tearDown(); + + Events::simulate(false); + + $this->resetServices(); + } + public function testCallGet() { $this->withRoutes([ @@ -253,11 +262,16 @@ public function provideRoutesData() 'Hello::index', 'Hello', ], - 'parameterized cli' => [ + 'parameterized param cli' => [ 'hello/(:any)', 'Hello::index/$1', 'Hello/index/samsonasik', ], + 'parameterized method cli' => [ + 'hello/(:segment)', + 'Hello::$1', + 'Hello/index', + ], 'default method index' => [ 'hello', 'Hello', @@ -281,8 +295,11 @@ public function provideRoutesData() public function testOpenCliRoutesFromHttpGot404($from, $to, $httpGet) { $this->expectException(PageNotFoundException::class); + $this->expectExceptionMessage('Cannot access the controller in a CLI Route.'); - require_once SUPPORTPATH . 'Controllers/Hello.php'; + $collection = Services::routes(); + $collection->setAutoRoute(true); + $collection->setDefaultNamespace('Tests\Support\Controllers'); $this->withRoutes([ [ diff --git a/user_guide_src/source/changelogs/v4.2.0.rst b/user_guide_src/source/changelogs/v4.2.0.rst index 449ad50f970d..67477d37cdf8 100644 --- a/user_guide_src/source/changelogs/v4.2.0.rst +++ b/user_guide_src/source/changelogs/v4.2.0.rst @@ -19,6 +19,7 @@ BREAKING - The method signature of ``CodeIgniter\CLI\CommandRunner::_remap()`` has been changed to fix a bug. - The ``CodeIgniter\Autoloader\Autoloader::initialize()`` has changed the behavior to fix a bug. It used to use Composer classmap only when ``$modules->discoverInComposer`` is true. Now it always uses the Composer classmap if Composer is available. - The color code output by :ref:`CLI::color() ` has been changed to fix a bug. +- To prevent unexpected access from the web browser, if a controller is added to a cli route (``$routes->cli()``), all methods of that controller are no longer accessible via auto routing. Enhancements ************