Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new improved auto router spark routes command #5953

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/Config/Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
$routes->setDefaultMethod('index');
$routes->setTranslateURIDashes(false);
$routes->set404Override();
// The auto-routing is very dangerous. It is easy to create vulnerable apps
// The Auto Routing (Legacy) is very dangerous. It is easy to create vulnerable apps
// where controller filters or CSRF protection are bypassed.
// It is recommended that you do not set it to `true`.
// If you don't want to define all routes, please use the Auto Routing (Improved).
// Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true.
//$routes->setAutoRoute(false);

/*
Expand Down
42 changes: 29 additions & 13 deletions system/Commands/Utilities/Routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Commands\Utilities\Routes\AutoRouteCollector;
use CodeIgniter\Commands\Utilities\Routes\AutoRouterImproved\AutoRouteCollector as AutoRouteCollectorImproved;
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;
use CodeIgniter\Commands\Utilities\Routes\SampleURIGenerator;
use Config\Services;
Expand Down Expand Up @@ -112,19 +113,34 @@ public function run(array $params)
}

if ($collection->shouldAutoRoute()) {
$autoRouteCollector = new AutoRouteCollector(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod()
);
$autoRoutes = $autoRouteCollector->get();

foreach ($autoRoutes as &$routes) {
// There is no `auto` method, but it is intentional not to get route filters.
$filters = $filterCollector->get('auto', $uriGenerator->get($routes[1]));

$routes[] = implode(' ', array_map('class_basename', $filters['before']));
$routes[] = implode(' ', array_map('class_basename', $filters['after']));
$autoRoutesImproved = config('Feature')->autoRoutesImproved ?? false;

if ($autoRoutesImproved) {
$autoRouteCollector = new AutoRouteCollectorImproved(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod(),
$methods,
$collection->getRegisteredControllers('*')
);

$autoRoutes = $autoRouteCollector->get();
} else {
$autoRouteCollector = new AutoRouteCollector(
$collection->getDefaultNamespace(),
$collection->getDefaultController(),
$collection->getDefaultMethod()
);

$autoRoutes = $autoRouteCollector->get();

foreach ($autoRoutes as &$routes) {
// There is no `auto` method, but it is intentional not to get route filters.
$filters = $filterCollector->get('auto', $uriGenerator->get($routes[1]));

$routes[] = implode(' ', array_map('class_basename', $filters['before']));
$routes[] = implode(' ', array_map('class_basename', $filters['after']));
}
}

$tbody = [...$tbody, ...$autoRoutes];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?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\Commands\Utilities\Routes\AutoRouterImproved;

use CodeIgniter\Commands\Utilities\Routes\ControllerFinder;
use CodeIgniter\Commands\Utilities\Routes\FilterCollector;

/**
* Collects data for Auto Routing Improved.
*/
final class AutoRouteCollector
{
/**
* @var string namespace to search
*/
private string $namespace;

private string $defaultController;
private string $defaultMethod;
private array $httpMethods;

/**
* List of controllers in Defined Routes that should not be accessed via Auto-Routing.
*
* @var class-string[]
*/
private array $protectedControllers;

/**
* @param string $namespace namespace to search
*/
public function __construct(
string $namespace,
string $defaultController,
string $defaultMethod,
array $httpMethods,
array $protectedControllers
) {
$this->namespace = $namespace;
$this->defaultController = $defaultController;
$this->defaultMethod = $defaultMethod;
$this->httpMethods = $httpMethods;
$this->protectedControllers = $protectedControllers;
}

/**
* @return array<int, array<int, string>>
* @phpstan-return list<list<string>>
*/
public function get(): array
{
$finder = new ControllerFinder($this->namespace);
$reader = new ControllerMethodReader($this->namespace, $this->httpMethods);

$tbody = [];

foreach ($finder->find() as $class) {
// Exclude controllers in Defined Routes.
if (in_array($class, $this->protectedControllers, true)) {
continue;
}

$routes = $reader->read(
$class,
$this->defaultController,
$this->defaultMethod
);

if ($routes === []) {
continue;
}

$routes = $this->addFilters($routes);

foreach ($routes as $item) {
$tbody[] = [
strtoupper($item['method']) . '(auto)',
$item['route'] . $item['route_params'],
$item['handler'],
$item['before'],
$item['after'],
];
}
}

return $tbody;
}

private function addFilters($routes)
{
$filterCollector = new FilterCollector(true);

foreach ($routes as &$route) {
// Search filters for the URI with all params
$sampleUri = $this->generateSampleUri($route);
$filtersLongest = $filterCollector->get($route['method'], $route['route'] . $sampleUri);

// Search filters for the URI without optional params
$sampleUri = $this->generateSampleUri($route, false);
$filtersShortest = $filterCollector->get($route['method'], $route['route'] . $sampleUri);

// Get common array elements
$filters['before'] = array_intersect($filtersLongest['before'], $filtersShortest['before']);
$filters['after'] = array_intersect($filtersLongest['after'], $filtersShortest['after']);

$route['before'] = implode(' ', array_map('class_basename', $filters['before']));
$route['after'] = implode(' ', array_map('class_basename', $filters['after']));
}

return $routes;
}

private function generateSampleUri(array $route, bool $longest = true): string
{
$sampleUri = '';

if (isset($route['params'])) {
$i = 1;

foreach ($route['params'] as $required) {
if ($longest && ! $required) {
$sampleUri .= '/' . $i++;
}
}
}

return $sampleUri;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?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\Commands\Utilities\Routes\AutoRouterImproved;

use ReflectionClass;
use ReflectionMethod;

/**
* Reads a controller and returns a list of auto route listing.
*/
final class ControllerMethodReader
{
/**
* @var string the default namespace
*/
private string $namespace;

private array $httpMethods;

/**
* @param string $namespace the default namespace
*/
public function __construct(string $namespace, array $httpMethods)
{
$this->namespace = $namespace;
$this->httpMethods = $httpMethods;
}

/**
* Returns found route info in the controller.
*
* @phpstan-param class-string $class
*
* @return array<int, array<string, array|string>>
* @phpstan-return list<array<string, string|array>>
*/
public function read(string $class, string $defaultController = 'Home', string $defaultMethod = 'index'): array
{
$reflection = new ReflectionClass($class);

if ($reflection->isAbstract()) {
return [];
}

$classname = $reflection->getName();
$classShortname = $reflection->getShortName();

$output = [];
$classInUri = $this->getUriByClass($classname);

foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$methodName = $method->getName();

foreach ($this->httpMethods as $httpVerb) {
if (strpos($methodName, $httpVerb) === 0) {
// Remove HTTP verb prefix.
$methodInUri = lcfirst(substr($methodName, strlen($httpVerb)));

if ($methodInUri === $defaultMethod) {
$routeWithoutController = $this->getRouteWithoutController(
$classShortname,
$defaultController,
$classInUri,
$classname,
$methodName,
$httpVerb
);

if ($routeWithoutController !== []) {
$output = [...$output, ...$routeWithoutController];

continue;
}

// Route for the default method.
$output[] = [
'method' => $httpVerb,
'route' => $classInUri,
'route_params' => '',
'handler' => '\\' . $classname . '::' . $methodName,
'params' => [],
];

continue;
}

$route = $classInUri . '/' . $methodInUri;

$params = [];
$routeParams = '';
$refParams = $method->getParameters();

foreach ($refParams as $param) {
$required = true;
if ($param->isOptional()) {
$required = false;

$routeParams .= '[/..]';
} else {
$routeParams .= '/..';
}

// [variable_name => required?]
$params[$param->getName()] = $required;
}

$output[] = [
'method' => $httpVerb,
'route' => $route,
'route_params' => $routeParams,
'handler' => '\\' . $classname . '::' . $methodName,
'params' => $params,
];
}
}
}

return $output;
}

/**
* @phpstan-param class-string $classname
*
* @return string URI path part from the folder(s) and controller
*/
private function getUriByClass(string $classname): string
{
// remove the namespace
$pattern = '/' . preg_quote($this->namespace, '/') . '/';
$class = ltrim(preg_replace($pattern, '', $classname), '\\');

$classParts = explode('\\', $class);
$classPath = '';

foreach ($classParts as $part) {
// make the first letter lowercase, because auto routing makes
// the URI path's first letter uppercase and search the controller
$classPath .= lcfirst($part) . '/';
}

return rtrim($classPath, '/');
}

/**
* Gets a route without default controller.
*/
private function getRouteWithoutController(
string $classShortname,
string $defaultController,
string $uriByClass,
string $classname,
string $methodName,
string $httpVerb
): array {
$output = [];

if ($classShortname === $defaultController) {
$pattern = '#' . preg_quote(lcfirst($defaultController), '#') . '\z#';
$routeWithoutController = rtrim(preg_replace($pattern, '', $uriByClass), '/');
$routeWithoutController = $routeWithoutController ?: '/';

$output[] = [
'method' => $httpVerb,
'route' => $routeWithoutController,
'route_params' => '',
'handler' => '\\' . $classname . '::' . $methodName,
'params' => [],
];
}

return $output;
}
}
Loading