diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 8c02a4acd331..e1d5f4fc0df2 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -7,6 +7,8 @@ use CodeIgniter\Filters\DebugToolbar; use CodeIgniter\Filters\Honeypot; use CodeIgniter\Filters\InvalidChars; +use CodeIgniter\Filters\PageCache; +use CodeIgniter\Filters\PerformanceMetrics; use CodeIgniter\Filters\SecureHeaders; class Filters extends BaseConfig @@ -24,6 +26,27 @@ class Filters extends BaseConfig 'honeypot' => Honeypot::class, 'invalidchars' => InvalidChars::class, 'secureheaders' => SecureHeaders::class, + 'pagecache' => PageCache::class, + 'performance' => PerformanceMetrics::class, + ]; + + /** + * List of special required filters. + * + * The filters listed here is special. They are applied before and after + * other kinds of filters, and always applied even if a route does not exist. + * + * @var array> + */ + public array $required = [ + 'before' => [ + 'pagecache', + ], + 'after' => [ + 'pagecache', + 'performance', + 'toolbar', + ], ]; /** @@ -40,7 +63,6 @@ class Filters extends BaseConfig // 'invalidchars', ], 'after' => [ - 'toolbar', // 'honeypot', // 'secureheaders', ], diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index c585715572fb..4ce4b5f69433 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -17,6 +17,7 @@ use CodeIgniter\Events\Events; use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\Exceptions\PageNotFoundException; +use CodeIgniter\Filters\Filters; use CodeIgniter\HTTP\CLIRequest; use CodeIgniter\HTTP\DownloadResponse; use CodeIgniter\HTTP\Exceptions\RedirectException; @@ -338,30 +339,86 @@ public function run(?RouteCollectionInterface $routes = null, bool $returnRespon $this->getRequestObject(); $this->getResponseObject(); - try { - $this->forceSecureAccess(); + Events::trigger('pre_system'); - $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); - } catch (ResponsableInterface|DeprecatedRedirectException $e) { - $this->outputBufferingEnd(); - if ($e instanceof DeprecatedRedirectException) { - $e = new RedirectException($e->getMessage(), $e->getCode(), $e); - } + $this->benchmark->stop('bootstrap'); - $this->response = $e->getResponse(); - } catch (PageNotFoundException $e) { - $this->response = $this->display404errors($e); - } catch (Throwable $e) { - $this->outputBufferingEnd(); + $this->benchmark->start('required_before_filters'); + // Start up the filters + $filters = Services::filters(); + // Run required before filters + $possibleResponse = $this->runRequiredBeforeFilters($filters); - throw $e; + // If a ResponseInterface instance is returned then send it back to the client and stop + if ($possibleResponse instanceof ResponseInterface) { + $this->response = $possibleResponse; + } else { + try { + $this->forceSecureAccess(); + + $this->response = $this->handleRequest($routes, config(Cache::class), $returnResponse); + } catch (ResponsableInterface|DeprecatedRedirectException $e) { + $this->outputBufferingEnd(); + if ($e instanceof DeprecatedRedirectException) { + $e = new RedirectException($e->getMessage(), $e->getCode(), $e); + } + + $this->response = $e->getResponse(); + } catch (PageNotFoundException $e) { + $this->response = $this->display404errors($e); + } catch (Throwable $e) { + $this->outputBufferingEnd(); + + throw $e; + } } + $this->runRequiredAfterFilters($filters); + if ($returnResponse) { return $this->response; } $this->sendResponse(); + + // Is there a post-system event? + Events::trigger('post_system'); + } + + /** + * Run required before filters. + */ + private function runRequiredBeforeFilters(Filters $filters): ?ResponseInterface + { + $possibleResponse = $filters->runRequired('before'); + $this->benchmark->stop('required_before_filters'); + + // If a ResponseInterface instance is returned then send it back to the client and stop + if ($possibleResponse instanceof ResponseInterface) { + return $possibleResponse; + } + + return null; + } + + /** + * Run required after filters. + */ + private function runRequiredAfterFilters(Filters $filters): void + { + $filters->setResponse($this->response); + + // After filter debug toolbar requires 'total_execution'. + $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); + + // Run required after filters + $this->benchmark->start('required_after_filters'); + $response = $filters->runRequired('after'); + $this->benchmark->stop('required_after_filters'); + + if ($response instanceof ResponseInterface) { + $this->response = $response; + } } /** @@ -404,20 +461,11 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache return $this->response->setStatusCode(405)->setBody('Method Not Allowed'); } - Events::trigger('pre_system'); - - // Check for a cached page. Execution will stop - // if the page has been cached. - if (($response = $this->displayCache($cacheConfig)) instanceof ResponseInterface) { - return $response; - } - $routeFilters = $this->tryToRouteIt($routes); $uri = $this->request->getPath(); if ($this->enableFilters) { - // Start up the filters $filters = Services::filters(); // If any filters were specified within the routes file, @@ -477,9 +525,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache $filters = Services::filters(); $filters->setResponse($this->response); - // After filter debug toolbar requires 'total_execution'. - $this->totalTime = $this->benchmark->getElapsedTime('total_execution'); - // Run "after" filters $this->benchmark->start('after_filters'); $response = $filters->run($uri, 'after'); @@ -495,11 +540,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache ! $this->response instanceof DownloadResponse && ! $this->response instanceof RedirectResponse ) { - // Cache it without the performance metrics replaced - // so that we can have live speed updates along the way. - // Must be run after filters to preserve the Response headers. - $this->pageCache->make($this->request, $this->response); - // Save our current URI as the previous URI in the session // for safer, more accurate use with `previous_url()` helper function. $this->storePreviousURL(current_url(true)); @@ -507,9 +547,6 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache unset($uri); - // Is there a post-system event? - Events::trigger('post_system'); - return $this->response; } @@ -667,6 +704,7 @@ protected function forceSecureAccess($duration = 31_536_000) * * @throws Exception * + * @deprecated 4.5.0 PageCache required filter is used. No longer used. * @deprecated 4.4.2 The parameter $config is deprecated. No longer used. */ public function displayCache(Cache $config) @@ -749,6 +787,8 @@ protected function generateCacheName(Cache $config): string /** * Replaces the elapsed_time and memory_usage tag. + * + * @deprecated 4.5.0 PerformanceMetrics required filter is used. No longer used. */ public function displayPerformanceMetrics(string $output): string { @@ -773,6 +813,8 @@ public function displayPerformanceMetrics(string $output): string */ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) { + $this->benchmark->start('routing'); + if ($routes === null) { $routes = Services::routes()->loadRoutes(); } @@ -782,9 +824,6 @@ protected function tryToRouteIt(?RouteCollectionInterface $routes = null) $uri = $this->request->getPath(); - $this->benchmark->stop('bootstrap'); - $this->benchmark->start('routing'); - $this->outputBufferingStart(); $this->controller = $this->router->handle($uri); @@ -1051,13 +1090,6 @@ public function spoofRequestMethod() */ protected function sendResponse() { - // Update the performance metrics - $body = $this->response->getBody(); - if ($body !== null) { - $output = $this->displayPerformanceMetrics($body); - $this->response->setBody($output); - } - $this->response->send(); } diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php index 0420c19b92dd..46aa91644004 100644 --- a/system/Debug/Toolbar/Collectors/Routes.php +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -64,9 +64,17 @@ public function display(): array try { $method = new ReflectionMethod($router->controllerName(), $router->methodName()); } catch (ReflectionException $e) { - // If we're here, the method doesn't exist - // and is likely calculated in _remap. - $method = new ReflectionMethod($router->controllerName(), '_remap'); + try { + // If we're here, the method doesn't exist + // and is likely calculated in _remap. + $method = new ReflectionMethod($router->controllerName(), '_remap'); + } catch (ReflectionException) { + // If we're here, page cache is returned. The router is not executed. + return [ + 'matchedRoute' => [], + 'routes' => [], + ]; + } } } diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 95a38f313303..6e15d6434c7f 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -226,6 +226,94 @@ public function run(string $uri, string $position = 'before') return $position === 'before' ? $this->request : $this->response; } + /** + * Runs required filters for the specified position. + * + * @return RequestInterface|ResponseInterface|string|null + * + * @throws FilterException + */ + public function runRequired(string $position = 'before') + { + // Set the toolbar filter to the last position to be executed + if ( + in_array('toolbar', $this->config->required['after'], true) + && ($count = count($this->config->required['after'])) > 1 + && $this->config->required['after'][$count - 1] !== 'toolbar' + ) { + array_splice( + $this->config->required['after'], + array_search('toolbar', $this->config->required['after'], true), + 1 + ); + $this->config->required['after'][] = 'toolbar'; + } + + $filtersClass = []; + + foreach ($this->config->required[$position] as $alias) { + if (! array_key_exists($alias, $this->config->aliases)) { + throw FilterException::forNoAlias($alias); + } + + if (is_array($this->config->aliases[$alias])) { + $filtersClass[$position] = array_merge($filtersClass[$position], $this->config->aliases[$alias]); + } else { + $filtersClass[$position][] = $this->config->aliases[$alias]; + } + } + + foreach ($filtersClass[$position] as $className) { + $class = new $className(); + + if (! $class instanceof FilterInterface) { + throw FilterException::forIncorrectInterface(get_class($class)); + } + + if ($position === 'before') { + $result = $class->before( + $this->request, + $this->argumentsClass[$className] ?? null + ); + + if ($result instanceof RequestInterface) { + $this->request = $result; + + continue; + } + + // If the response object was sent back, + // then send it and quit. + if ($result instanceof ResponseInterface) { + // short circuit - bypass any other filters + return $result; + } + // Ignore an empty result + if (empty($result)) { + continue; + } + + return $result; + } + + if ($position === 'after') { + $result = $class->after( + $this->request, + $this->response, + $this->argumentsClass[$className] ?? null + ); + + if ($result instanceof ResponseInterface) { + $this->response = $result; + + continue; + } + } + } + + return $position === 'before' ? $this->request : $this->response; + } + /** * Runs through our list of filters provided by the configuration * object to get them ready for use, including getting uri masks @@ -650,7 +738,7 @@ protected function processAliasesToClass(string $position) private function pathApplies(string $uri, $paths) { // empty path matches all - if (empty($paths)) { + if ($paths === '' || $paths === []) { return true; }