diff --git a/system/Router/Router.php b/system/Router/Router.php index 1c61d121940d..e1560b0d4bd6 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -498,7 +498,7 @@ public function autoRoute(string $uri) { $segments = explode('/', $uri); - $segments = $this->validateRequest($segments); + $segments = $this->scanControllers($segments); // If we don't have any segments left - try the default controller; // WARNING: Directories get shifted out of the segments array. @@ -512,6 +512,12 @@ public function autoRoute(string $uri) $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. @@ -526,7 +532,6 @@ public function autoRoute(string $uri) } $defaultNamespace = $this->collection->getDefaultNamespace(); - $controllerName = $this->controllerName(); if ($this->collection->getHTTPVerb() !== 'cli') { $controller = '\\' . $defaultNamespace; @@ -573,32 +578,61 @@ public function autoRoute(string $uri) //-------------------------------------------------------------------- /** - * Attempts to validate the URI request and determine the controller path. + * Scans the controller directory, attempting to locate a controller matching the supplied uri $segments * * @param array $segments URI segments * - * @return array 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 */ 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 + */ + protected function scanControllers(array $segments): array { $segments = array_filter($segments, function ($segment) { - // @phpstan-ignore-next-line - return ! empty($segment) || ($segment !== '0' || $segment !== 0); + return $segment !== ''; }); + // numerically reindex the array, removing gaps $segments = array_values($segments); - $c = count($segments); - $directoryOverride = isset($this->directory); + // 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) { - $test = $this->directory . ucfirst($this->translateURIDashes === true ? str_replace('-', '_', $segments[0]) : $segments[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; - if (! is_file(APPPATH . 'Controllers/' . $test . '.php') && $directoryOverride === false && is_dir(APPPATH . 'Controllers/' . $this->directory . ucfirst($segments[0]))) + // 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(array_shift($segments), true); + $this->setDirectory($segmentConvert, true, false); + array_shift($segments); continue; } @@ -614,10 +648,11 @@ protected function validateRequest(array $segments): array /** * Sets the sub-directory that the controller is in. * - * @param string|null $dir - * @param boolean|false $append + * @param string|null $dir + * @param boolean $append + * @param boolean $validate if true, checks to make sure $dir consists of only PSR4 compliant segments */ - public function setDirectory(string $dir = null, bool $append = false) + public function setDirectory(string $dir = null, bool $append = false, bool $validate = true) { if (empty($dir)) { @@ -625,18 +660,41 @@ public function setDirectory(string $dir = null, bool $append = false) return; } - $dir = ucfirst($dir); + if ($validate) + { + $segments = explode('/', trim($dir, '/')); + foreach ($segments as $segment) + { + if (! $this->isValidSegment($segment)) + { + return; + } + } + } if ($append !== true || empty($this->directory)) { - $this->directory = str_replace('.', '', trim($dir, '/')) . '/'; + $this->directory = trim($dir, '/') . '/'; } else { - $this->directory .= str_replace('.', '', trim($dir, '/')) . '/'; + $this->directory .= trim($dir, '/') . '/'; } } + /** + * 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 + * + * @param string $segment + * @return boolean + */ + private function isValidSegment(string $segment): bool + { + return (bool) preg_match('/^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$/', $segment); + } + //-------------------------------------------------------------------- /** diff --git a/tests/system/Router/RouterTest.php b/tests/system/Router/RouterTest.php index 221bca8488e4..e8b73e3adcf1 100644 --- a/tests/system/Router/RouterTest.php +++ b/tests/system/Router/RouterTest.php @@ -5,6 +5,7 @@ use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\Test\CIUnitTestCase; use Config\Modules; +use CodeIgniter\Exceptions\PageNotFoundException; class RouterTest extends CIUnitTestCase { @@ -81,9 +82,9 @@ public function testZeroAsURIPath() { $router = new Router($this->collection, $this->request); - $router->handle('0'); + $this->expectException(PageNotFoundException::class); - $this->assertEquals('0', $router->controllerName()); + $router->handle('0'); } //-------------------------------------------------------------------- @@ -231,6 +232,153 @@ public function testAutoRouteFindsControllerWithSubfolder() //-------------------------------------------------------------------- + public function testAutoRouteFindsDashedSubfolder() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + mkdir(APPPATH . 'Controllers/Dash_folder'); + + $router->autoRoute('dash-folder/mycontroller/somemethod'); + + rmdir(APPPATH . 'Controllers/Dash_folder'); + + $this->assertEquals('Dash_folder/', $router->directory()); + $this->assertEquals('Mycontroller', $router->controllerName()); + $this->assertEquals('somemethod', $router->methodName()); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteFindsDashedController() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + mkdir(APPPATH . 'Controllers/Dash_folder'); + file_put_contents(APPPATH . 'Controllers/Dash_folder/Dash_controller.php', ''); + + $router->autoRoute('dash-folder/dash-controller/somemethod'); + + unlink(APPPATH . 'Controllers/Dash_folder/Dash_controller.php'); + rmdir(APPPATH . 'Controllers/Dash_folder'); + + $this->assertEquals('Dash_folder/', $router->directory()); + $this->assertEquals('Dash_controller', $router->controllerName()); + $this->assertEquals('somemethod', $router->methodName()); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteFindsDashedMethod() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + mkdir(APPPATH . 'Controllers/Dash_folder'); + file_put_contents(APPPATH . 'Controllers/Dash_folder/Dash_controller.php', ''); + + $router->autoRoute('dash-folder/dash-controller/dash-method'); + + unlink(APPPATH . 'Controllers/Dash_folder/Dash_controller.php'); + rmdir(APPPATH . 'Controllers/Dash_folder'); + + $this->assertEquals('Dash_folder/', $router->directory()); + $this->assertEquals('Dash_controller', $router->controllerName()); + $this->assertEquals('dash_method', $router->methodName()); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteFindsDefaultDashFolder() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + mkdir(APPPATH . 'Controllers/Dash_folder'); + + $router->autoRoute('dash-folder'); + + rmdir(APPPATH . 'Controllers/Dash_folder'); + + $this->assertEquals('Dash_folder/', $router->directory()); + $this->assertEquals('Home', $router->controllerName()); + $this->assertEquals('index', $router->methodName()); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteFindsMByteDir() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + mkdir(APPPATH . 'Controllers/Φ'); + + $router->autoRoute('Φ'); + + rmdir(APPPATH . 'Controllers/Φ'); + + $this->assertEquals('Φ/', $router->directory()); + $this->assertEquals('Home', $router->controllerName()); + $this->assertEquals('index', $router->methodName()); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteFindsMByteController() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + file_put_contents(APPPATH . 'Controllers/Φ', ''); + + $router->autoRoute('Φ'); + + unlink(APPPATH . 'Controllers/Φ'); + + $this->assertEquals('Φ', $router->controllerName()); + $this->assertEquals('index', $router->methodName()); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteRejectsSingleDot() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + $this->expectException(PageNotFoundException::class); + + $router->autoRoute('.'); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteRejectsDoubleDot() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + $this->expectException(PageNotFoundException::class); + + $router->autoRoute('..'); + } + + //-------------------------------------------------------------------- + + public function testAutoRouteRejectsMidDot() + { + $router = new Router($this->collection, $this->request); + $router->setTranslateURIDashes(true); + + $this->expectException(PageNotFoundException::class); + + $router->autoRoute('Foo.bar'); + } + + //-------------------------------------------------------------------- + public function testDetectsLocales() { $router = new Router($this->collection, $this->request); @@ -575,4 +723,35 @@ public function testRegularExpressionPlaceholderWithUnicode() ]; $this->assertEquals($expected, $router->params()); } + + public function testRouterPriorDirectory() + { + $router = new Router($this->collection, $this->request); + + $router->setDirectory('foo/bar/baz', false, true); + $router->handle('Some_controller/some_method/param1/param2/param3'); + + $this->assertEquals('foo/bar/baz/', $router->directory()); + $this->assertEquals('Some_controller', $router->controllerName()); + $this->assertEquals('some_method', $router->methodName()); + } + + public function testSetDirectoryValid() + { + $router = new Router($this->collection, $this->request); + $router->setDirectory('foo/bar/baz', false, true); + + $this->assertEquals('foo/bar/baz/', $router->directory()); + } + + public function testSetDirectoryInvalid() + { + $router = new Router($this->collection, $this->request); + $router->setDirectory('foo/bad-segment/bar', false, true); + + $internal = $this->getPrivateProperty($router, 'directory'); + + $this->assertNull($internal); + $this->assertEquals('', $router->directory()); + } }