Skip to content

Multiple filters for a route and classname filter #5128

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

Merged
merged 5 commits into from
Oct 5, 2021
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
27 changes: 27 additions & 0 deletions app/Config/Feature.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Config;

use CodeIgniter\Config\BaseConfig;

/**
* Enable/disable backward compatibility breaking features.
*/
class Feature extends BaseConfig
{
/**
* Enable multiple filters for a route or not
*
* If you enable this:
* - CodeIgniter\CodeIgniter::handleRequest() uses:
* - CodeIgniter\Filters\Filters::enableFilters(), instead of enableFilter()
* - CodeIgniter\CodeIgniter::tryToRouteIt() uses:
* - CodeIgniter\Router\Router::getFilters(), instead of getFilter()
* - CodeIgniter\Router\Router::handle() uses:
* - property $filtersInfo, instead of $filterInfo
* - CodeIgniter\Router\RouteCollection::getFiltersForRoute(), instead of getFilterForRoute()
*
* @var bool
*/
public $multipleFilters = false;
}
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ parameters:
- '#Access to an undefined property CodeIgniter\\Database\\BaseConnection::\$mysqli|\$schema#'
- '#Access to an undefined property CodeIgniter\\Database\\ConnectionInterface::(\$DBDriver|\$connID|\$likeEscapeStr|\$likeEscapeChar|\$escapeChar|\$protectIdentifiers|\$schema)#'
- '#Call to an undefined method CodeIgniter\\Database\\BaseConnection::_(disable|enable)ForeignKeyChecks\(\)#'
- '#Call to an undefined method CodeIgniter\\Router\\RouteCollectionInterface::(getDefaultNamespace|isFiltered|getFilterForRoute|getRoutesOptions)\(\)#'
- '#Call to an undefined method CodeIgniter\\Router\\RouteCollectionInterface::(getDefaultNamespace|isFiltered|getFilterForRoute|getFiltersForRoute|getRoutesOptions)\(\)#'
- '#Cannot access property [\$a-z_]+ on ((bool\|)?object\|resource)#'
- '#Cannot call method [a-zA-Z_]+\(\) on ((bool\|)?object\|resource)#'
- '#Method CodeIgniter\\Router\\RouteCollectionInterface::getRoutes\(\) invoked with 1 parameter, 0 required#'
Expand Down
21 changes: 17 additions & 4 deletions system/CodeIgniter.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,15 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
// If any filters were specified within the routes file,
// we need to ensure it's active for the current request
if ($routeFilter !== null) {
$filters->enableFilter($routeFilter, 'before');
$filters->enableFilter($routeFilter, 'after');
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
if ($multipleFiltersEnabled) {
$filters->enableFilters($routeFilter, 'before');
$filters->enableFilters($routeFilter, 'after');
} else {
// for backward compatibility
$filters->enableFilter($routeFilter, 'before');
$filters->enableFilter($routeFilter, 'after');
}
}

$uri = $this->determinePath();
Expand Down Expand Up @@ -690,7 +697,7 @@ public function displayPerformanceMetrics(string $output): string
*
* @throws RedirectException
*
* @return string|null
* @return string|string[]|null
*/
protected function tryToRouteIt(?RouteCollectionInterface $routes = null)
{
Expand Down Expand Up @@ -719,7 +726,13 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null)

$this->benchmark->stop('routing');

return $this->router->getFilter();
// for backward compatibility
$multipleFiltersEnabled = config('Feature')->multipleFilters ?? false;
if (! $multipleFiltersEnabled) {
return $this->router->getFilter();
}

return $this->router->getFilters();
}

/**
Expand Down
24 changes: 23 additions & 1 deletion system/Filters/Filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ public function addFilter(string $class, ?string $alias = null, string $when = '
* are passed to the filter when executed.
*
* @return Filters
*
* @deprecated Use enableFilters(). This method will be private.
*/
public function enableFilter(string $name, string $when = 'before')
{
Expand All @@ -334,7 +336,9 @@ public function enableFilter(string $name, string $when = 'before')
$this->arguments[$name] = $params;
}

if (! array_key_exists($name, $this->config->aliases)) {
if (class_exists($name)) {
$this->config->aliases[$name] = $name;
} elseif (! array_key_exists($name, $this->config->aliases)) {
throw FilterException::forNoAlias($name);
}

Expand All @@ -352,6 +356,24 @@ public function enableFilter(string $name, string $when = 'before')
return $this;
}

/**
* Ensures that specific filters is on and enabled for the current request.
*
* Filters can have "arguments". This is done by placing a colon immediately
* after the filter name, followed by a comma-separated list of arguments that
* are passed to the filter when executed.
*
* @return Filters
*/
public function enableFilters(array $names, string $when = 'before')
{
foreach ($names as $filter) {
$this->enableFilter($filter, $when);
}

return $this;
}

/**
* Returns the arguments for a specified key, or all.
*
Expand Down
45 changes: 34 additions & 11 deletions system/Router/RouteCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ public function map(array $routes = [], ?array $options = null): RouteCollection
* Example:
* $routes->add('news', 'Posts::index');
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function add(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand Down Expand Up @@ -821,7 +821,7 @@ public function presenter(string $name, ?array $options = null): RouteCollection
* Example:
* $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function match(array $verbs = [], string $from = '', $to = '', ?array $options = null): RouteCollectionInterface
{
Expand All @@ -841,7 +841,7 @@ public function match(array $verbs = [], string $from = '', $to = '', ?array $op
/**
* Specifies a route that is only available to GET requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function get(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand All @@ -853,7 +853,7 @@ public function get(string $from, $to, ?array $options = null): RouteCollectionI
/**
* Specifies a route that is only available to POST requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function post(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand All @@ -865,7 +865,7 @@ public function post(string $from, $to, ?array $options = null): RouteCollection
/**
* Specifies a route that is only available to PUT requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function put(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand All @@ -877,7 +877,7 @@ public function put(string $from, $to, ?array $options = null): RouteCollectionI
/**
* Specifies a route that is only available to DELETE requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function delete(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand All @@ -889,7 +889,7 @@ public function delete(string $from, $to, ?array $options = null): RouteCollecti
/**
* Specifies a route that is only available to HEAD requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function head(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand All @@ -901,7 +901,7 @@ public function head(string $from, $to, ?array $options = null): RouteCollection
/**
* Specifies a route that is only available to PATCH requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function patch(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand All @@ -913,7 +913,7 @@ public function patch(string $from, $to, ?array $options = null): RouteCollectio
/**
* Specifies a route that is only available to OPTIONS requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function options(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand All @@ -925,7 +925,7 @@ public function options(string $from, $to, ?array $options = null): RouteCollect
/**
* Specifies a route that is only available to command-line requests.
*
* @param array|string $to
* @param array|Closure|string $to
*/
public function cli(string $from, $to, ?array $options = null): RouteCollectionInterface
{
Expand Down Expand Up @@ -1040,6 +1040,8 @@ public function isFiltered(string $search, ?string $verb = null): bool
* 'role:admin,manager'
*
* has a filter of "role", with parameters of ['admin', 'manager'].
*
* @deprecated Use getFiltersForRoute()
*/
public function getFilterForRoute(string $search, ?string $verb = null): string
{
Expand All @@ -1048,6 +1050,27 @@ public function getFilterForRoute(string $search, ?string $verb = null): string
return $options[$search]['filter'] ?? '';
}

/**
* Returns the filters that should be applied for a single route, along
* with any parameters it might have. Parameters are found by splitting
* the parameter name on a colon to separate the filter name from the parameter list,
* and the splitting the result on commas. So:
*
* 'role:admin,manager'
*
* has a filter of "role", with parameters of ['admin', 'manager'].
*/
public function getFiltersForRoute(string $search, ?string $verb = null): array
{
$options = $this->loadRoutesOptions($verb);

if (is_string($options[$search]['filter'])) {
return [$options[$search]['filter']];
}

return $options[$search]['filter'] ?? [];
}

/**
* Given a
*
Expand Down Expand Up @@ -1083,7 +1106,7 @@ protected function fillRouteParams(string $from, ?array $params = null): string
* the request method(s) that this route will work for. They can be separated
* by a pipe character "|" if there is more than one.
*
* @param array|string $to
* @param array|Closure|string $to
*/
protected function create(string $verb, string $from, $to, ?array $options = null)
{
Expand Down
4 changes: 2 additions & 2 deletions system/Router/RouteCollectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ interface RouteCollectionInterface
/**
* Adds a single route to the collection.
*
* @param array|string $to
* @param array $options
* @param array|Closure|string $to
* @param array $options
*
* @return mixed
*/
Expand Down
30 changes: 29 additions & 1 deletion system/Router/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,19 @@ class Router implements RouterInterface
* 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 = [];

/**
* Stores a reference to the RouteCollection object.
*
Expand Down Expand Up @@ -144,7 +154,13 @@ public function handle(?string $uri = null)

if ($this->checkRoutes($uri)) {
if ($this->collection->isFiltered($this->matchedRoute[0])) {
$this->filterInfo = $this->collection->getFilterForRoute($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;
Expand All @@ -166,12 +182,24 @@ public function handle(?string $uri = null)
* Returns the filter info for the matched route, if any.
*
* @return string
*
* @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.
*
Expand Down
24 changes: 24 additions & 0 deletions tests/system/CodeIgniterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CodeIgniter\Test\Mock\MockCodeIgniter;
use Config\App;
use Config\Modules;
use Tests\Support\Filters\Customfilter;

/**
* @backupGlobals enabled
Expand Down Expand Up @@ -172,6 +173,29 @@ public function testControllersCanReturnResponseObject()
$this->assertStringContainsString("You want to see 'about' page.", $output);
}

public function testControllersRunFilterByClassName()
{
$_SERVER['argv'] = ['index.php', 'pages/about'];
$_SERVER['argc'] = 2;

$_SERVER['REQUEST_URI'] = '/pages/about';

// Inject mock router.
$routes = Services::routes();
$routes->add('pages/about', static function () {
return Services::request()->url;
}, ['filter' => Customfilter::class]);

$router = Services::router($routes, Services::request());
Services::injectMock('router', $router);

ob_start();
$this->codeigniter->useSafeOutput(true)->run();
$output = ob_get_clean();

$this->assertStringContainsString('http://hellowworld.com', $output);
}

public function testResponseConfigEmpty()
{
$_SERVER['argv'] = ['index.php', '/'];
Expand Down
Loading