2019-03-27 08:06:12 -07:00
< ? php
2020-08-03 11:32:10 +08:00
2017-01-20 00:57:53 -08:00
/**
2021-07-19 21:32:33 +08:00
* This file is part of CodeIgniter 4 framework .
2017-01-20 00:57:53 -08:00
*
2020-10-24 16:38:41 +08:00
* ( c ) CodeIgniter Foundation < admin @ codeigniter . com >
2017-01-20 00:57:53 -08:00
*
2021-07-19 21:32:33 +08:00
* For the full copyright and license information , please view
* the LICENSE file that was distributed with this source code .
2017-01-20 00:57:53 -08:00
*/
2018-11-10 02:57:39 -02:00
2019-03-27 08:06:12 -07:00
namespace CodeIgniter ;
2019-04-19 17:56:57 +05:30
use Closure ;
2020-04-23 01:27:54 +07:00
use CodeIgniter\Debug\Timer ;
use CodeIgniter\Events\Events ;
2020-08-03 11:32:10 +08:00
use CodeIgniter\Exceptions\FrameworkException ;
2020-04-23 01:27:54 +07:00
use CodeIgniter\Exceptions\PageNotFoundException ;
use CodeIgniter\HTTP\CLIRequest ;
2018-09-25 00:16:49 +09:00
use CodeIgniter\HTTP\DownloadResponse ;
2021-01-05 22:20:17 +07:00
use CodeIgniter\HTTP\IncomingRequest ;
2021-09-14 23:05:23 +00:00
use CodeIgniter\HTTP\RedirectResponse ;
2018-05-08 00:02:34 -05:00
use CodeIgniter\HTTP\Request ;
2018-07-05 23:11:26 -05:00
use CodeIgniter\HTTP\ResponseInterface ;
2017-01-15 23:35:14 -06:00
use CodeIgniter\HTTP\URI ;
2019-10-16 11:48:28 -04:00
use CodeIgniter\Router\Exceptions\RedirectException ;
2016-04-11 23:40:33 -05:00
use CodeIgniter\Router\RouteCollectionInterface ;
2021-01-05 22:20:17 +07:00
use CodeIgniter\Router\Router ;
2020-10-04 00:27:56 +07:00
use Config\App ;
2020-04-23 01:27:54 +07:00
use Config\Cache ;
2022-05-30 22:48:39 +07:00
use Config\Kint as KintConfig ;
2020-04-23 01:27:54 +07:00
use Config\Services ;
2019-04-19 17:56:57 +05:30
use Exception ;
2020-10-04 00:27:56 +07:00
use Kint ;
use Kint\Renderer\CliRenderer ;
2022-02-05 10:01:59 +09:00
use Kint\Renderer\RichRenderer ;
2022-07-13 17:16:56 +09:00
use Locale ;
2022-05-30 10:42:10 +09:00
use LogicException ;
2016-02-07 23:26:11 -06:00
2017-01-15 23:35:14 -06:00
/**
* This class is the core of the framework , and will analyse the
* request , route it to a controller , and send back the response .
* Of course , there are variations to that flow , but this is the brains .
*/
2016-03-27 11:28:13 +09:00
class CodeIgniter
2016-03-18 23:19:33 -05:00
{
2021-06-04 22:51:52 +08:00
/**
* The current version of CodeIgniter Framework
*/
2022-06-16 13:12:19 +00:00
public const CI_VERSION = '4.2.1' ;
2021-06-04 22:51:52 +08:00
2021-12-14 11:49:33 +07:00
private const MIN_PHP_VERSION = '7.4' ;
2021-06-04 22:51:52 +08:00
/**
* App startup time .
*
* @ var mixed
*/
protected $startTime ;
/**
* Total app execution time
*
* @ var float
*/
protected $totalTime ;
/**
* Main application configuration
*
* @ var App
*/
protected $config ;
/**
* Timer instance .
*
* @ var Timer
*/
protected $benchmark ;
/**
* Current request .
*
2022-04-23 09:30:46 +07:00
* @ var CLIRequest | IncomingRequest | Request | null
2021-06-04 22:51:52 +08:00
*/
protected $request ;
/**
* Current response .
*
* @ var ResponseInterface
*/
protected $response ;
/**
* Router to use .
*
* @ var Router
*/
protected $router ;
/**
* Controller to use .
*
2021-06-11 23:46:56 +08:00
* @ var Closure | string
2021-06-04 22:51:52 +08:00
*/
protected $controller ;
/**
* Controller method to invoke .
*
* @ var string
*/
protected $method ;
/**
* Output handler to use .
*
* @ var string
*/
protected $output ;
/**
* Cache expiration time
*
2021-06-08 11:46:56 +08:00
* @ var int
2021-06-04 22:51:52 +08:00
*/
protected static $cacheTTL = 0 ;
/**
* Request path to use .
*
* @ var string
*/
protected $path ;
/**
* Should the Response instance " pretend "
* to keep from setting headers / cookies / etc
*
2021-06-08 11:46:56 +08:00
* @ var bool
2021-06-04 22:51:52 +08:00
*/
protected $useSafeOutput = false ;
2022-02-03 17:02:19 +09:00
/**
2022-02-06 11:09:43 +09:00
* Context
* web : Invoked by HTTP request
* php - cli : Invoked by CLI via `php public/index.php`
* spark : Invoked by CLI via the `spark` command
*
* @ phpstan - var 'php-cli' | 'spark' | 'web'
2022-02-03 17:02:19 +09:00
*/
2022-02-19 06:49:57 +07:00
protected ? string $context = null ;
2022-02-03 17:02:19 +09:00
2021-06-04 22:51:52 +08:00
/**
* Constructor .
*/
public function __construct ( App $config )
{
2021-06-07 19:06:26 +08:00
if ( version_compare ( PHP_VERSION , self :: MIN_PHP_VERSION , '<' )) {
2021-06-04 22:51:52 +08:00
// @codeCoverageIgnoreStart
$message = extension_loaded ( 'intl' )
? lang ( 'Core.invalidPhpVersion' , [ self :: MIN_PHP_VERSION , PHP_VERSION ])
: sprintf ( 'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s' , self :: MIN_PHP_VERSION , PHP_VERSION );
exit ( $message );
// @codeCoverageIgnoreEnd
}
$this -> startTime = microtime ( true );
$this -> config = $config ;
}
/**
* Handles some basic app and environment setup .
*/
public function initialize ()
{
// Define environment variables
$this -> detectEnvironment ();
$this -> bootstrapEnvironment ();
// Setup Exception Handling
Services :: exceptions () -> initialize ();
// Run this check for manual installations
2021-06-07 19:06:26 +08:00
if ( ! is_file ( COMPOSER_PATH )) {
2021-06-04 22:51:52 +08:00
$this -> resolvePlatformExtensions (); // @codeCoverageIgnore
}
// Set default locale on the server
2022-07-13 17:16:56 +09:00
Locale :: setDefault ( $this -> config -> defaultLocale ? ? 'en' );
2021-06-04 22:51:52 +08:00
// Set default timezone on the server
date_default_timezone_set ( $this -> config -> appTimezone ? ? 'UTC' );
$this -> initializeKint ();
2021-06-07 19:06:26 +08:00
if ( ! CI_DEBUG ) {
2021-06-04 22:51:52 +08:00
Kint :: $enabled_mode = false ; // @codeCoverageIgnore
}
}
/**
* Checks system for missing required PHP extensions .
*
* @ throws FrameworkException
*
* @ codeCoverageIgnore
*/
protected function resolvePlatformExtensions ()
{
$requiredExtensions = [
'curl' ,
'intl' ,
'json' ,
'mbstring' ,
'xml' ,
];
$missingExtensions = [];
2021-06-07 19:06:26 +08:00
foreach ( $requiredExtensions as $extension ) {
if ( ! extension_loaded ( $extension )) {
2021-06-04 22:51:52 +08:00
$missingExtensions [] = $extension ;
}
}
2021-06-07 19:06:26 +08:00
if ( $missingExtensions !== []) {
2021-06-04 22:51:52 +08:00
throw FrameworkException :: forMissingExtension ( implode ( ', ' , $missingExtensions ));
}
}
/**
* Initializes Kint
*/
protected function initializeKint ()
{
// If we have KINT_DIR it means it's already loaded via composer
2021-06-07 19:06:26 +08:00
if ( ! defined ( 'KINT_DIR' )) {
2021-06-04 22:51:52 +08:00
spl_autoload_register ( function ( $class ) {
$class = explode ( '\\' , $class );
2021-06-07 19:06:26 +08:00
if ( array_shift ( $class ) !== 'Kint' ) {
2021-06-04 22:51:52 +08:00
return ;
}
$file = SYSTEMPATH . 'ThirdParty/Kint/' . implode ( '/' , $class ) . '.php' ;
2022-05-24 16:53:04 +00:00
if ( is_file ( $file )) {
2021-07-11 07:31:21 +07:00
require_once $file ;
}
2021-06-04 22:51:52 +08:00
});
require_once SYSTEMPATH . 'ThirdParty/Kint/init.php' ;
}
2022-06-16 08:55:34 +09:00
/** @var \Config\Kint $config */
2022-05-30 22:48:39 +07:00
$config = config ( KintConfig :: class );
2021-06-04 22:51:52 +08:00
2021-12-19 16:28:11 +09:00
Kint :: $depth_limit = $config -> maxDepth ;
2021-06-04 22:51:52 +08:00
Kint :: $display_called_from = $config -> displayCalledFrom ;
Kint :: $expanded = $config -> expanded ;
2021-06-07 19:06:26 +08:00
if ( ! empty ( $config -> plugins ) && is_array ( $config -> plugins )) {
2021-06-04 22:51:52 +08:00
Kint :: $plugins = $config -> plugins ;
}
2022-02-05 10:01:59 +09:00
$csp = Services :: csp ();
if ( $csp -> enabled ()) {
RichRenderer :: $js_nonce = $csp -> getScriptNonce ();
RichRenderer :: $css_nonce = $csp -> getStyleNonce ();
}
2021-12-25 11:57:59 +09:00
2021-06-04 22:51:52 +08:00
RichRenderer :: $theme = $config -> richTheme ;
RichRenderer :: $folder = $config -> richFolder ;
RichRenderer :: $sort = $config -> richSort ;
2021-06-07 19:06:26 +08:00
if ( ! empty ( $config -> richObjectPlugins ) && is_array ( $config -> richObjectPlugins )) {
2021-12-19 16:28:11 +09:00
RichRenderer :: $value_plugins = $config -> richObjectPlugins ;
2021-06-04 22:51:52 +08:00
}
2021-06-07 19:06:26 +08:00
if ( ! empty ( $config -> richTabPlugins ) && is_array ( $config -> richTabPlugins )) {
2021-06-04 22:51:52 +08:00
RichRenderer :: $tab_plugins = $config -> richTabPlugins ;
}
CliRenderer :: $cli_colors = $config -> cliColors ;
CliRenderer :: $force_utf8 = $config -> cliForceUTF8 ;
CliRenderer :: $detect_width = $config -> cliDetectWidth ;
CliRenderer :: $min_terminal_width = $config -> cliMinWidth ;
}
/**
* Launch the application !
*
* This is " the loop " if you will . The main entry point into the script
* that gets the required class instances , fires off the filters ,
* tries to route the response , loads the controller and generally
* makes all of the pieces work together .
*
2021-06-15 22:53:20 +08:00
* @ throws RedirectException
*
2022-07-20 11:30:16 +09:00
* @ return ResponseInterface | void
2021-06-04 22:51:52 +08:00
*/
2021-07-09 23:13:08 +08:00
public function run ( ? RouteCollectionInterface $routes = null , bool $returnResponse = false )
2021-06-04 22:51:52 +08:00
{
2022-05-30 10:42:10 +09:00
if ( $this -> context === null ) {
throw new LogicException ( 'Context must be set before run() is called. If you are upgrading from 4.1.x, you need to merge `public/index.php` and `spark` file from `vendor/codeigniter4/framework`.' );
}
2022-02-03 17:02:19 +09:00
2021-06-04 22:51:52 +08:00
$this -> startBenchmark ();
$this -> getRequestObject ();
$this -> getResponseObject ();
$this -> forceSecureAccess ();
$this -> spoofRequestMethod ();
2022-05-05 10:17:42 +09:00
if ( $this -> request instanceof IncomingRequest && strtolower ( $this -> request -> getMethod ()) === 'cli' ) {
2022-02-02 16:18:14 +09:00
$this -> response -> setStatusCode ( 405 ) -> setBody ( 'Method Not Allowed' );
return $this -> sendResponse ();
}
2021-06-04 22:51:52 +08:00
Events :: trigger ( 'pre_system' );
// Check for a cached page. Execution will stop
// if the page has been cached.
$cacheConfig = new Cache ();
$response = $this -> displayCache ( $cacheConfig );
2021-06-07 19:06:26 +08:00
if ( $response instanceof ResponseInterface ) {
if ( $returnResponse ) {
2021-06-04 22:51:52 +08:00
return $response ;
}
$this -> response -> pretend ( $this -> useSafeOutput ) -> send ();
$this -> callExit ( EXIT_SUCCESS );
2021-06-08 01:26:32 +08:00
2021-06-04 22:51:52 +08:00
return ;
}
2022-02-01 17:24:07 +09:00
// spark command has nothing to do with HTTP redirect and 404
2022-02-03 17:02:19 +09:00
if ( $this -> isSparked ()) {
2022-02-01 17:24:07 +09:00
return $this -> handleRequest ( $routes , $cacheConfig , $returnResponse );
}
2021-06-07 19:06:26 +08:00
try {
2021-06-04 22:51:52 +08:00
return $this -> handleRequest ( $routes , $cacheConfig , $returnResponse );
2021-06-07 19:06:26 +08:00
} catch ( RedirectException $e ) {
2021-06-04 22:51:52 +08:00
$logger = Services :: logger ();
$logger -> info ( 'REDIRECTED ROUTE at ' . $e -> getMessage ());
// If the route is a 'redirect' route, it throws
// the exception with the $to as the message
$this -> response -> redirect ( base_url ( $e -> getMessage ()), 'auto' , $e -> getCode ());
$this -> sendResponse ();
$this -> callExit ( EXIT_SUCCESS );
2021-06-08 01:26:32 +08:00
2021-06-04 22:51:52 +08:00
return ;
2021-06-07 19:06:26 +08:00
} catch ( PageNotFoundException $e ) {
2021-06-04 22:51:52 +08:00
$this -> display404errors ( $e );
}
}
/**
* Set our Response instance to " pretend " mode so that things like
* cookies and headers are not actually sent , allowing PHP 7.2 + to
* not complain when ini_set () function is used .
*
* @ return $this
*/
public function useSafeOutput ( bool $safe = true )
{
$this -> useSafeOutput = $safe ;
return $this ;
}
2022-02-03 17:02:19 +09:00
/**
* Invoked via spark command ?
*/
private function isSparked () : bool
{
return $this -> context === 'spark' ;
}
/**
* Invoked via php - cli command ?
*/
private function isPhpCli () : bool
{
return $this -> context === 'php-cli' ;
}
/**
* Web access ?
*/
private function isWeb () : bool
{
return $this -> context === 'web' ;
}
2021-06-04 22:51:52 +08:00
/**
* Handles the main request logic and fires the controller .
*
2022-02-02 16:18:14 +09:00
* @ throws PageNotFoundException
2021-06-04 22:51:52 +08:00
* @ throws RedirectException
2021-06-15 22:53:20 +08:00
*
2022-07-20 11:30:16 +09:00
* @ return ResponseInterface
2021-06-04 22:51:52 +08:00
*/
protected function handleRequest ( ? RouteCollectionInterface $routes , Cache $cacheConfig , bool $returnResponse = false )
{
$routeFilter = $this -> tryToRouteIt ( $routes );
2021-11-03 10:43:57 +09:00
$uri = $this -> determinePath ();
2021-07-04 18:22:15 +02:00
// Start up the filters
2021-06-04 22:51:52 +08:00
$filters = Services :: filters ();
// If any filters were specified within the routes file,
// we need to ensure it's active for the current request
2021-07-07 21:55:16 +08:00
if ( $routeFilter !== null ) {
2021-09-24 17:31:19 +09:00
$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' );
}
2021-06-04 22:51:52 +08:00
}
// Never run filters when running through Spark cli
2022-02-03 17:02:19 +09:00
if ( ! $this -> isSparked ()) {
2021-07-04 18:22:15 +02:00
// Run "before" filters
$this -> benchmark -> start ( 'before_filters' );
2021-06-04 22:51:52 +08:00
$possibleResponse = $filters -> run ( $uri , 'before' );
2021-07-04 18:22:15 +02:00
$this -> benchmark -> stop ( 'before_filters' );
2021-06-04 22:51:52 +08:00
// If a ResponseInterface instance is returned then send it back to the client and stop
2021-06-07 19:06:26 +08:00
if ( $possibleResponse instanceof ResponseInterface ) {
2021-06-04 22:51:52 +08:00
return $returnResponse ? $possibleResponse : $possibleResponse -> pretend ( $this -> useSafeOutput ) -> send ();
}
2021-06-07 19:06:26 +08:00
if ( $possibleResponse instanceof Request ) {
2021-06-04 22:51:52 +08:00
$this -> request = $possibleResponse ;
}
}
$returned = $this -> startController ();
// Closure controller has run in startController().
2021-06-07 19:06:26 +08:00
if ( ! is_callable ( $this -> controller )) {
2021-06-04 22:51:52 +08:00
$controller = $this -> createController ();
2021-06-07 19:06:26 +08:00
if ( ! method_exists ( $controller , '_remap' ) && ! is_callable ([ $controller , $this -> method ], false )) {
2021-06-04 22:51:52 +08:00
throw PageNotFoundException :: forMethodNotFound ( $this -> method );
}
// Is there a "post_controller_constructor" event?
Events :: trigger ( 'post_controller_constructor' );
$returned = $this -> runController ( $controller );
2021-06-07 19:06:26 +08:00
} else {
2021-06-04 22:51:52 +08:00
$this -> benchmark -> stop ( 'controller_constructor' );
$this -> benchmark -> stop ( 'controller' );
}
// If $returned is a string, then the controller output something,
// probably a view, instead of echoing it directly. Send it along
// so it can be used with the output.
$this -> gatherOutput ( $cacheConfig , $returned );
// Never run filters when running through Spark cli
2022-02-03 17:02:19 +09:00
if ( ! $this -> isSparked ()) {
2021-06-04 22:51:52 +08:00
$filters -> setResponse ( $this -> response );
2021-07-04 18:22:15 +02:00
2021-06-04 22:51:52 +08:00
// Run "after" filters
2021-07-04 18:22:15 +02:00
$this -> benchmark -> start ( 'after_filters' );
2021-06-04 22:51:52 +08:00
$response = $filters -> run ( $uri , 'after' );
2021-07-04 18:22:15 +02:00
$this -> benchmark -> stop ( 'after_filters' );
2021-06-07 19:06:26 +08:00
} else {
2021-06-04 22:51:52 +08:00
$response = $this -> response ;
// Set response code for CLI command failures
2021-06-07 19:06:26 +08:00
if ( is_numeric ( $returned ) || $returned === false ) {
2021-06-04 22:51:52 +08:00
$response -> setStatusCode ( 400 );
}
}
2021-06-07 19:06:26 +08:00
if ( $response instanceof ResponseInterface ) {
2021-06-04 22:51:52 +08:00
$this -> response = $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 ));
unset ( $uri );
2021-06-07 19:06:26 +08:00
if ( ! $returnResponse ) {
2021-06-04 22:51:52 +08:00
$this -> sendResponse ();
}
// Is there a post-system event?
Events :: trigger ( 'post_system' );
return $this -> response ;
}
/**
* You can load different configurations depending on your
* current environment . Setting the environment also influences
* things like logging and error reporting .
*
* This can be set to anything , but default usage is :
*
* development
* testing
* production
2021-07-11 07:38:24 +07:00
*
* @ codeCoverageIgnore
2021-06-04 22:51:52 +08:00
*/
protected function detectEnvironment ()
{
// Make sure ENVIRONMENT isn't already set by other means.
2021-07-11 07:39:57 +07:00
if ( ! defined ( 'ENVIRONMENT' )) {
2021-07-11 07:31:21 +07:00
define ( 'ENVIRONMENT' , $_SERVER [ 'CI_ENVIRONMENT' ] ? ? 'production' );
2021-07-11 07:38:24 +07:00
}
2021-06-04 22:51:52 +08:00
}
/**
* Load any custom boot files based upon the current environment .
*
* If no boot file exists , we shouldn ' t continue because something
* is wrong . At the very least , they should have error reporting setup .
*/
protected function bootstrapEnvironment ()
{
2021-06-07 19:06:26 +08:00
if ( is_file ( APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php' )) {
2021-06-04 22:51:52 +08:00
require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php' ;
2021-06-07 19:06:26 +08:00
} else {
2021-06-04 22:51:52 +08:00
// @codeCoverageIgnoreStart
header ( 'HTTP/1.1 503 Service Unavailable.' , true , 503 );
echo 'The application environment is not set correctly.' ;
2021-06-08 01:26:32 +08:00
2021-06-04 22:51:52 +08:00
exit ( EXIT_ERROR ); // EXIT_ERROR
// @codeCoverageIgnoreEnd
}
}
/**
* Start the Benchmark
*
* The timer is used to display total script execution both in the
* debug toolbar , and potentially on the displayed page .
*/
protected function startBenchmark ()
{
2021-09-07 23:30:27 +08:00
if ( $this -> startTime === null ) {
2021-07-06 12:34:23 +02:00
$this -> startTime = microtime ( true );
}
2021-06-04 22:51:52 +08:00
$this -> benchmark = Services :: timer ();
$this -> benchmark -> start ( 'total_execution' , $this -> startTime );
$this -> benchmark -> start ( 'bootstrap' );
}
/**
* Sets a Request object to be used for this request .
* Used when running certain tests .
*
* @ return $this
*/
public function setRequest ( Request $request )
{
$this -> request = $request ;
return $this ;
}
/**
2022-06-23 11:17:15 +09:00
* Get our Request object , ( either IncomingRequest or CLIRequest ) .
2021-06-04 22:51:52 +08:00
*/
protected function getRequestObject ()
{
2021-06-07 19:06:26 +08:00
if ( $this -> request instanceof Request ) {
2021-06-04 22:51:52 +08:00
return ;
}
2022-02-03 17:02:19 +09:00
if ( $this -> isSparked () || $this -> isPhpCli ()) {
2022-06-23 11:17:15 +09:00
Services :: createRequest ( $this -> config , true );
2021-06-07 19:06:26 +08:00
} else {
2022-06-23 11:17:15 +09:00
Services :: createRequest ( $this -> config );
2021-06-04 22:51:52 +08:00
}
2022-06-23 11:17:15 +09:00
$this -> request = Services :: request ();
2021-06-04 22:51:52 +08:00
}
/**
* Get our Response object , and set some default values , including
* the HTTP protocol version and a default successful response .
*/
protected function getResponseObject ()
{
$this -> response = Services :: response ( $this -> config );
2022-02-03 17:02:19 +09:00
if ( $this -> isWeb ()) {
2021-06-04 22:51:52 +08:00
$this -> response -> setProtocolVersion ( $this -> request -> getProtocolVersion ());
}
// Assume success until proven otherwise.
$this -> response -> setStatusCode ( 200 );
}
/**
* Force Secure Site Access ? If the config value 'forceGlobalSecureRequests'
* is true , will enforce that all requests to this site are made through
* HTTPS . Will redirect the user to the current page with HTTPS , as well
* as set the HTTP Strict Transport Security header for those browsers
* that support it .
*
2021-06-08 11:46:56 +08:00
* @ param int $duration How long the Strict Transport Security
2021-06-08 12:11:23 +08:00
* should be enforced for this URL .
2021-06-04 22:51:52 +08:00
*/
2021-12-13 22:07:52 +07:00
protected function forceSecureAccess ( $duration = 31_536_000 )
2021-06-04 22:51:52 +08:00
{
2021-06-07 19:06:26 +08:00
if ( $this -> config -> forceGlobalSecureRequests !== true ) {
2021-06-04 22:51:52 +08:00
return ;
}
force_https ( $duration , $this -> request , $this -> response );
}
/**
* Determines if a response has been cached for the given URI .
*
* @ throws Exception
*
2022-07-20 11:30:16 +09:00
* @ return false | ResponseInterface
2021-06-04 22:51:52 +08:00
*/
public function displayCache ( Cache $config )
{
2021-06-07 19:06:26 +08:00
if ( $cachedResponse = cache () -> get ( $this -> generateCacheName ( $config ))) {
2021-06-04 22:51:52 +08:00
$cachedResponse = unserialize ( $cachedResponse );
2021-06-07 19:06:26 +08:00
if ( ! is_array ( $cachedResponse ) || ! isset ( $cachedResponse [ 'output' ]) || ! isset ( $cachedResponse [ 'headers' ])) {
2021-06-04 22:51:52 +08:00
throw new Exception ( 'Error unserializing page cache' );
}
$headers = $cachedResponse [ 'headers' ];
$output = $cachedResponse [ 'output' ];
// Clear all default headers
2021-06-07 19:06:26 +08:00
foreach ( array_keys ( $this -> response -> headers ()) as $key ) {
2021-06-04 22:51:52 +08:00
$this -> response -> removeHeader ( $key );
}
// Set cached headers
2021-06-07 19:06:26 +08:00
foreach ( $headers as $name => $value ) {
2021-06-04 22:51:52 +08:00
$this -> response -> setHeader ( $name , $value );
}
$output = $this -> displayPerformanceMetrics ( $output );
$this -> response -> setBody ( $output );
return $this -> response ;
}
return false ;
}
/**
* Tells the app that the final output should be cached .
*/
public static function cache ( int $time )
{
static :: $cacheTTL = $time ;
}
/**
* Caches the full response from the current request . Used for
* full - page caching for very high performance .
*
* @ return mixed
*/
public function cachePage ( Cache $config )
{
$headers = [];
2021-06-08 01:26:32 +08:00
2021-06-07 19:06:26 +08:00
foreach ( $this -> response -> headers () as $header ) {
2021-06-04 22:51:52 +08:00
$headers [ $header -> getName ()] = $header -> getValueLine ();
}
return cache () -> save ( $this -> generateCacheName ( $config ), serialize ([ 'headers' => $headers , 'output' => $this -> output ]), static :: $cacheTTL );
}
/**
* Returns an array with our basic performance stats collected .
*/
public function getPerformanceStats () : array
{
return [
'startTime' => $this -> startTime ,
'totalTime' => $this -> totalTime ,
];
}
/**
* Generates the cache name to use for our full - page caching .
*/
protected function generateCacheName ( Cache $config ) : string
{
2021-06-07 19:06:26 +08:00
if ( $this -> request instanceof CLIRequest ) {
2021-06-04 22:51:52 +08:00
return md5 ( $this -> request -> getPath ());
}
$uri = $this -> request -> getUri ();
2021-06-07 19:06:26 +08:00
if ( $config -> cacheQueryString ) {
2021-06-04 22:51:52 +08:00
$name = URI :: createURIString ( $uri -> getScheme (), $uri -> getAuthority (), $uri -> getPath (), $uri -> getQuery ());
2021-06-07 19:06:26 +08:00
} else {
2021-06-04 22:51:52 +08:00
$name = URI :: createURIString ( $uri -> getScheme (), $uri -> getAuthority (), $uri -> getPath ());
}
return md5 ( $name );
}
/**
2021-07-17 15:50:50 +02:00
* Replaces the elapsed_time tag .
2021-06-04 22:51:52 +08:00
*/
public function displayPerformanceMetrics ( string $output ) : string
{
$this -> totalTime = $this -> benchmark -> getElapsedTime ( 'total_execution' );
return str_replace ( '{elapsed_time}' , ( string ) $this -> totalTime , $output );
}
/**
* Try to Route It - As it sounds like , works with the router to
* match a route against the current URI . If the route is a
* " redirect route " , will also handle the redirect .
*
* @ param RouteCollectionInterface | null $routes An collection interface to use in place
2021-06-08 12:11:23 +08:00
* of the config file .
2021-06-04 22:51:52 +08:00
*
* @ throws RedirectException
2021-06-15 22:53:20 +08:00
*
2022-01-28 10:36:08 +09:00
* @ return string | string [] | null Route filters , that is , the filters specified in the routes file
2021-06-04 22:51:52 +08:00
*/
2021-07-09 23:13:08 +08:00
protected function tryToRouteIt ( ? RouteCollectionInterface $routes = null )
2021-06-04 22:51:52 +08:00
{
2021-06-07 19:06:26 +08:00
if ( $routes === null ) {
2021-06-04 22:51:52 +08:00
require APPPATH . 'Config/Routes.php' ;
}
// $routes is defined in Config/Routes.php
$this -> router = Services :: router ( $routes , $this -> request );
$path = $this -> determinePath ();
$this -> benchmark -> stop ( 'bootstrap' );
$this -> benchmark -> start ( 'routing' );
ob_start ();
$this -> controller = $this -> router -> handle ( $path );
$this -> method = $this -> router -> methodName ();
// If a {locale} segment was matched in the final route,
// then we need to set the correct locale on our Request.
2021-06-07 19:06:26 +08:00
if ( $this -> router -> hasLocale ()) {
2021-12-13 15:05:13 +08:00
$this -> request -> setLocale ( $this -> router -> getLocale ());
2021-06-04 22:51:52 +08:00
}
$this -> benchmark -> stop ( 'routing' );
2021-09-24 17:31:19 +09:00
// for backward compatibility
$multipleFiltersEnabled = config ( 'Feature' ) -> multipleFilters ? ? false ;
if ( ! $multipleFiltersEnabled ) {
return $this -> router -> getFilter ();
}
return $this -> router -> getFilters ();
2021-06-04 22:51:52 +08:00
}
/**
* Determines the path to use for us to try to route to , based
* on user input ( setPath ), or the CLI / IncomingRequest path .
2022-04-08 10:10:11 +09:00
*
* @ return string
2021-06-04 22:51:52 +08:00
*/
protected function determinePath ()
{
2021-06-07 19:06:26 +08:00
if ( ! empty ( $this -> path )) {
2021-06-04 22:51:52 +08:00
return $this -> path ;
}
return method_exists ( $this -> request , 'getPath' ) ? $this -> request -> getPath () : $this -> request -> getUri () -> getPath ();
}
/**
* Allows the request path to be set from outside the class ,
* instead of relying on CLIRequest or IncomingRequest for the path .
*
* This is primarily used by the Console .
*
* @ return $this
*/
public function setPath ( string $path )
{
$this -> path = $path ;
return $this ;
}
/**
* Now that everything has been setup , this method attempts to run the
* controller method and make the script go . If it ' s not able to , will
* show the appropriate Page Not Found error .
2022-02-17 09:17:38 +09:00
*
* @ return ResponseInterface | string | void
2021-06-04 22:51:52 +08:00
*/
protected function startController ()
{
$this -> benchmark -> start ( 'controller' );
$this -> benchmark -> start ( 'controller_constructor' );
// Is it routed to a Closure?
2021-06-07 19:06:26 +08:00
if ( is_object ( $this -> controller ) && ( get_class ( $this -> controller ) === 'Closure' )) {
2021-06-04 22:51:52 +08:00
$controller = $this -> controller ;
2021-06-08 01:26:32 +08:00
2021-06-04 22:51:52 +08:00
return $controller ( ... $this -> router -> params ());
}
// No controller specified - we don't know what to do now.
2021-06-07 19:06:26 +08:00
if ( empty ( $this -> controller )) {
2021-06-04 22:51:52 +08:00
throw PageNotFoundException :: forEmptyController ();
}
// Try to autoload the class
2021-06-07 19:06:26 +08:00
if ( ! class_exists ( $this -> controller , true ) || $this -> method [ 0 ] === '_' ) {
2021-06-04 22:51:52 +08:00
throw PageNotFoundException :: forControllerNotFound ( $this -> controller , $this -> method );
}
}
/**
* Instantiates the controller class .
*
2022-02-17 09:17:38 +09:00
* @ return Controller
2021-06-04 22:51:52 +08:00
*/
protected function createController ()
{
$class = new $this -> controller ();
$class -> initController ( $this -> request , $this -> response , Services :: logger ());
$this -> benchmark -> stop ( 'controller_constructor' );
return $class ;
}
/**
* Runs the controller , allowing for _remap methods to function .
*
2021-10-16 14:36:37 +09:00
* CI4 supports three types of requests :
* 1. Web : URI segments become parameters , sent to Controllers via Routes ,
* output controlled by Headers to browser
* 2. Spark : accessed by CLI via the spark command , arguments are Command arguments ,
* sent to Commands by CommandRunner , output controlled by CLI class
* 3. PHP CLI : accessed by CLI via php public / index . php , arguments become URI segments ,
* sent to Controllers via Routes , output varies
*
2021-06-04 22:51:52 +08:00
* @ param mixed $class
*
2022-02-17 09:17:38 +09:00
* @ return false | ResponseInterface | string | void
2021-06-04 22:51:52 +08:00
*/
protected function runController ( $class )
{
2021-10-16 14:36:37 +09:00
if ( $this -> isSparked ()) {
// This is a Spark request
/** @var CLIRequest $request */
$request = $this -> request ;
$params = $request -> getArgs ();
2022-03-28 11:40:45 +09:00
$output = $class -> _remap ( $this -> method , $params );
2021-06-07 19:06:26 +08:00
} else {
2021-10-16 14:36:37 +09:00
// This is a Web request or PHP CLI request
$params = $this -> router -> params ();
2021-06-04 22:51:52 +08:00
2022-03-28 11:40:45 +09:00
$output = method_exists ( $class , '_remap' )
? $class -> _remap ( $this -> method , ... $params )
: $class -> { $this -> method }( ... $params );
}
2021-10-16 14:36:37 +09:00
2021-06-04 22:51:52 +08:00
$this -> benchmark -> stop ( 'controller' );
return $output ;
}
/**
* Displays a 404 Page Not Found error . If set , will try to
* call the 404 Override controller / method that was set in routing config .
*/
protected function display404errors ( PageNotFoundException $e )
{
// Is there a 404 Override available?
2021-06-07 19:06:26 +08:00
if ( $override = $this -> router -> get404Override ()) {
2022-02-17 09:11:04 +09:00
$returned = null ;
2021-06-07 19:06:26 +08:00
if ( $override instanceof Closure ) {
2021-06-04 22:51:52 +08:00
echo $override ( $e -> getMessage ());
2021-06-07 19:06:26 +08:00
} elseif ( is_array ( $override )) {
2021-06-04 22:51:52 +08:00
$this -> benchmark -> start ( 'controller' );
$this -> benchmark -> start ( 'controller_constructor' );
$this -> controller = $override [ 0 ];
$this -> method = $override [ 1 ];
$controller = $this -> createController ();
2022-02-17 09:11:04 +09:00
$returned = $this -> runController ( $controller );
2021-06-04 22:51:52 +08:00
}
unset ( $override );
$cacheConfig = new Cache ();
2022-02-17 09:11:04 +09:00
$this -> gatherOutput ( $cacheConfig , $returned );
2021-06-04 22:51:52 +08:00
$this -> sendResponse ();
return ;
}
// Display 404 Errors
$this -> response -> setStatusCode ( $e -> getCode ());
2021-06-07 19:06:26 +08:00
if ( ENVIRONMENT !== 'testing' ) {
2021-06-04 22:51:52 +08:00
// @codeCoverageIgnoreStart
2021-06-07 19:06:26 +08:00
if ( ob_get_level () > 0 ) {
2021-06-04 22:51:52 +08:00
ob_end_flush ();
}
// @codeCoverageIgnoreEnd
}
// When testing, one is for phpunit, another is for test case.
2021-06-07 19:06:26 +08:00
elseif ( ob_get_level () > 2 ) {
2021-06-04 22:51:52 +08:00
ob_end_flush (); // @codeCoverageIgnore
}
2022-02-03 17:02:19 +09:00
throw PageNotFoundException :: forPageNotFound (
2022-06-29 17:52:08 +09:00
( ENVIRONMENT !== 'production' || ! $this -> isWeb ()) ? $e -> getMessage () : null
2022-02-03 17:02:19 +09:00
);
2021-06-04 22:51:52 +08:00
}
/**
* Gathers the script output from the buffer , replaces some execution
* time tag in the output and displays the debug toolbar , if required .
*
2022-02-17 09:17:38 +09:00
* @ param ResponseInterface | string | null $returned
2021-06-04 22:51:52 +08:00
*/
2021-07-09 23:13:08 +08:00
protected function gatherOutput ( ? Cache $cacheConfig = null , $returned = null )
2021-06-04 22:51:52 +08:00
{
$this -> output = ob_get_contents ();
// If buffering is not null.
// Clean (erase) the output buffer and turn off output buffering
2021-06-07 19:06:26 +08:00
if ( ob_get_length ()) {
2021-06-04 22:51:52 +08:00
ob_end_clean ();
}
2021-06-07 19:06:26 +08:00
if ( $returned instanceof DownloadResponse ) {
2022-01-27 09:28:31 +09:00
// Turn off output buffering completely, even if php.ini output_buffering is not off
while ( ob_get_level () > 0 ) {
ob_end_clean ();
}
2021-06-04 22:51:52 +08:00
$this -> response = $returned ;
2021-06-08 01:26:32 +08:00
2021-06-04 22:51:52 +08:00
return ;
}
// If the controller returned a response object,
// we need to grab the body from it so it can
// be added to anything else that might have been
// echoed already.
// We also need to save the instance locally
// so that any status code changes, etc, take place.
2021-06-07 19:06:26 +08:00
if ( $returned instanceof ResponseInterface ) {
2021-06-04 22:51:52 +08:00
$this -> response = $returned ;
$returned = $returned -> getBody ();
}
2021-06-07 19:06:26 +08:00
if ( is_string ( $returned )) {
2021-06-04 22:51:52 +08:00
$this -> output .= $returned ;
}
// Cache it without the performance metrics replaced
// so that we can have live speed updates along the way.
2021-06-07 19:06:26 +08:00
if ( static :: $cacheTTL > 0 ) {
2021-06-04 22:51:52 +08:00
$this -> cachePage ( $cacheConfig );
}
$this -> output = $this -> displayPerformanceMetrics ( $this -> output );
$this -> response -> setBody ( $this -> output );
}
/**
* If we have a session object to use , store the current URI
* as the previous URI . This is called just prior to sending the
* response to the client , and will make it available next request .
*
* This helps provider safer , more reliable previous_url () detection .
*
2021-06-11 23:46:56 +08:00
* @ param string | URI $uri
2021-06-04 22:51:52 +08:00
*/
public function storePreviousURL ( $uri )
{
// Ignore CLI requests
2022-02-03 17:02:19 +09:00
if ( ! $this -> isWeb ()) {
return ;
2021-06-04 22:51:52 +08:00
}
// Ignore AJAX requests
2021-06-07 19:06:26 +08:00
if ( method_exists ( $this -> request , 'isAJAX' ) && $this -> request -> isAJAX ()) {
2021-06-04 22:51:52 +08:00
return ;
}
2021-09-14 23:05:23 +00:00
// Ignore unroutable responses
if ( $this -> response instanceof DownloadResponse || $this -> response instanceof RedirectResponse ) {
return ;
}
2022-05-21 18:46:13 +09:00
// Ignore non-HTML responses
if ( strpos ( $this -> response -> getHeaderLine ( 'Content-Type' ), 'text/html' ) === false ) {
return ;
}
2021-06-04 22:51:52 +08:00
// This is mainly needed during testing...
2021-06-07 19:06:26 +08:00
if ( is_string ( $uri )) {
2021-06-04 22:51:52 +08:00
$uri = new URI ( $uri );
}
2021-06-07 19:06:26 +08:00
if ( isset ( $_SESSION )) {
2021-06-04 22:51:52 +08:00
$_SESSION [ '_ci_previous_url' ] = URI :: createURIString ( $uri -> getScheme (), $uri -> getAuthority (), $uri -> getPath (), $uri -> getQuery (), $uri -> getFragment ());
}
}
/**
* Modifies the Request Object to use a different method if a POST
* variable called _method is found .
*/
public function spoofRequestMethod ()
{
// Only works with POSTED forms
2022-05-05 10:17:42 +09:00
if ( strtolower ( $this -> request -> getMethod ()) !== 'post' ) {
2021-06-04 22:51:52 +08:00
return ;
}
2021-12-13 15:05:13 +08:00
$method = $this -> request -> getPost ( '_method' );
2021-06-04 22:51:52 +08:00
2021-06-07 19:06:26 +08:00
if ( empty ( $method )) {
2021-06-04 22:51:52 +08:00
return ;
}
2022-02-09 11:40:48 +09:00
// Only allows PUT, PATCH, DELETE
if ( in_array ( strtoupper ( $method ), [ 'PUT' , 'PATCH' , 'DELETE' ], true )) {
$this -> request = $this -> request -> setMethod ( $method );
}
2021-06-04 22:51:52 +08:00
}
/**
* Sends the output of this request back to the client .
* This is what they ' ve been waiting for !
2022-07-20 11:30:16 +09:00
*
* @ return void
2021-06-04 22:51:52 +08:00
*/
protected function sendResponse ()
{
$this -> response -> pretend ( $this -> useSafeOutput ) -> send ();
}
/**
* Exits the application , setting the exit code for CLI - based applications
* that might be watching .
*
* Made into a separate method so that it can be mocked during testing
* without actually stopping script execution .
*
2021-06-08 11:46:56 +08:00
* @ param int $code
2021-06-04 22:51:52 +08:00
*/
protected function callExit ( $code )
{
2021-07-19 21:08:51 +08:00
exit ( $code ); // @codeCoverageIgnore
2021-06-04 22:51:52 +08:00
}
2022-02-03 17:02:19 +09:00
/**
* Sets the app context .
*
2022-02-06 11:09:43 +09:00
* @ phpstan - param 'php-cli' | 'spark' | 'web' $context
*
2022-02-03 17:02:19 +09:00
* @ return $this
*/
public function setContext ( string $context )
{
$this -> context = $context ;
return $this ;
}
2016-02-02 00:24:31 -06:00
}