<?php
namespace CodeIgniter;
use Closure;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Request;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Config\Cache;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Debug\Timer;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\Router\Exceptions\RedirectException;
use CodeIgniter\Router\RouteCollectionInterface;
use CodeIgniter\Exceptions\PageNotFoundException;
use Exception;
class CodeIgniter
{
const CI_VERSION = '4.0.0-rc.3';
protected $startTime;
protected $totalTime;
protected $config;
protected $benchmark;
protected $request;
protected $response;
protected $router;
protected $controller;
protected $method;
protected $output;
protected static $cacheTTL = 0;
protected $path;
protected $useSafeOutput = false;
public function __construct($config)
{
$this->startTime = microtime(true);
$this->config = $config;
}
public function initialize()
{
date_default_timezone_set($this->config->appTimezone ?? 'UTC');
Services::exceptions()
->initialize();
$this->detectEnvironment();
$this->bootstrapEnvironment();
if (CI_DEBUG)
{
require_once SYSTEMPATH . 'ThirdParty/Kint/kint.php';
}
}
public function run(RouteCollectionInterface $routes = null, bool $returnResponse = false)
{
$this->startBenchmark();
$this->getRequestObject();
$this->getResponseObject();
$this->forceSecureAccess();
$this->spoofRequestMethod();
Events::trigger('pre_system');
$cacheConfig = new Cache();
$response = $this->displayCache($cacheConfig);
if ($response instanceof ResponseInterface)
{
if ($returnResponse)
{
return $response;
}
$this->response->pretend($this->useSafeOutput)->send();
$this->callExit(EXIT_SUCCESS);
}
try
{
return $this->handleRequest($routes, $cacheConfig, $returnResponse);
}
catch (RedirectException $e)
{
$logger = Services::logger();
$logger->info('REDIRECTED ROUTE at ' . $e->getMessage());
$this->response->redirect($e->getMessage(), 'auto', $e->getCode());
$this->sendResponse();
$this->callExit(EXIT_SUCCESS);
}
catch (PageNotFoundException $e)
{
$this->display404errors($e);
}
}
public function useSafeOutput(bool $safe = true)
{
$this->useSafeOutput = $safe;
return $this;
}
protected function handleRequest(RouteCollectionInterface $routes = null, $cacheConfig, bool $returnResponse = false)
{
$routeFilter = $this->tryToRouteIt($routes);
$filters = Services::filters();
if (! is_null($routeFilter))
{
$filters->enableFilter($routeFilter, 'before');
$filters->enableFilter($routeFilter, 'after');
}
$uri = $this->request instanceof CLIRequest ? $this->request->getPath() : $this->request->uri->getPath();
if (! defined('SPARKED'))
{
$possibleRedirect = $filters->run($uri, 'before');
if ($possibleRedirect instanceof RedirectResponse)
{
return $possibleRedirect->send();
}
if ($possibleRedirect instanceof ResponseInterface)
{
return $possibleRedirect->send();
}
}
$returned = $this->startController();
if (! is_callable($this->controller))
{
$controller = $this->createController();
Events::trigger('post_controller_constructor');
$returned = $this->runController($controller);
}
else
{
$this->benchmark->stop('controller_constructor');
$this->benchmark->stop('controller');
}
$this->gatherOutput($cacheConfig, $returned);
if (! defined('SPARKED'))
{
$filters->setResponse($this->response);
$response = $filters->run($uri, 'after');
}
else
{
$response = $this->response;
if (is_numeric($returned) || $returned === false)
{
$response->setStatusCode(400);
}
}
if ($response instanceof Response)
{
$this->response = $response;
}
$this->storePreviousURL($this->request->uri ?? $uri);
unset($uri);
if (! $returnResponse)
{
$this->sendResponse();
}
Events::trigger('post_system');
return $this->response;
}
protected function detectEnvironment()
{
if (! defined('ENVIRONMENT'))
{
if (getenv('CI') !== false)
{
define('ENVIRONMENT', 'testing');
}
else
{
define('ENVIRONMENT', $_SERVER['CI_ENVIRONMENT'] ?? 'production');
}
}
}
protected function bootstrapEnvironment()
{
if (is_file(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php'))
{
require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
}
else
{
header('HTTP/1.1 503 Service Unavailable.', true, 503);
echo 'The application environment is not set correctly.';
exit(1); }
}
protected function startBenchmark()
{
$this->startTime = microtime(true);
$this->benchmark = Services::timer();
$this->benchmark->start('total_execution', $this->startTime);
$this->benchmark->start('bootstrap');
}
public function setRequest(Request $request)
{
$this->request = $request;
return $this;
}
protected function getRequestObject()
{
if ($this->request instanceof Request)
{
return;
}
if (is_cli() && ! (ENVIRONMENT === 'testing'))
{
$this->request = Services::clirequest($this->config);
}
else
{
$this->request = Services::request($this->config);
$this->request->setProtocolVersion($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1');
}
}
protected function getResponseObject()
{
$this->response = Services::response($this->config);
if (! is_cli() || ENVIRONMENT === 'testing')
{
$this->response->setProtocolVersion($this->request->getProtocolVersion());
}
$this->response->setStatusCode(200);
}
protected function forceSecureAccess($duration = 31536000)
{
if ($this->config->forceGlobalSecureRequests !== true)
{
return;
}
force_https($duration, $this->request, $this->response);
}
public function displayCache($config)
{
if ($cachedResponse = cache()->get($this->generateCacheName($config)))
{
$cachedResponse = unserialize($cachedResponse);
if (! is_array($cachedResponse) || ! isset($cachedResponse['output']) || ! isset($cachedResponse['headers']))
{
throw new Exception('Error unserializing page cache');
}
$headers = $cachedResponse['headers'];
$output = $cachedResponse['output'];
foreach ($this->response->getHeaders() as $key => $val)
{
$this->response->removeHeader($key);
}
foreach ($headers as $name => $value)
{
$this->response->setHeader($name, $value);
}
$output = $this->displayPerformanceMetrics($output);
$this->response->setBody($output);
return $this->response;
}
return false;
}
public static function cache(int $time)
{
static::$cacheTTL = $time;
}
public function cachePage(Cache $config)
{
$headers = [];
foreach ($this->response->getHeaders() as $header)
{
$headers[$header->getName()] = $header->getValueLine();
}
return cache()->save(
$this->generateCacheName($config), serialize(['headers' => $headers, 'output' => $this->output]), static::$cacheTTL
);
}
public function getPerformanceStats(): array
{
return [
'startTime' => $this->startTime,
'totalTime' => $this->totalTime,
];
}
protected function generateCacheName($config): string
{
if (get_class($this->request) === CLIRequest::class)
{
return md5($this->request->getPath());
}
$uri = $this->request->uri;
if ($config->cacheQueryString)
{
$name = URI::createURIString(
$uri->getScheme(), $uri->getAuthority(), $uri->getPath(), $uri->getQuery()
);
}
else
{
$name = URI::createURIString(
$uri->getScheme(), $uri->getAuthority(), $uri->getPath()
);
}
return md5($name);
}
public function displayPerformanceMetrics(string $output): string
{
$this->totalTime = $this->benchmark->getElapsedTime('total_execution');
$output = str_replace('{elapsed_time}', $this->totalTime, $output);
return $output;
}
protected function tryToRouteIt(RouteCollectionInterface $routes = null)
{
if (empty($routes) || ! $routes instanceof RouteCollectionInterface)
{
require APPPATH . '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 ($this->router->hasLocale())
{
$this->request->setLocale($this->router->getLocale());
}
$this->benchmark->stop('routing');
return $this->router->getFilter();
}
protected function determinePath()
{
if (! empty($this->path))
{
return $this->path;
}
return (is_cli() && ! (ENVIRONMENT === 'testing')) ? $this->request->getPath() : $this->request->uri->getPath();
}
public function setPath(string $path)
{
$this->path = $path;
return $this;
}
protected function startController()
{
$this->benchmark->start('controller');
$this->benchmark->start('controller_constructor');
if (is_object($this->controller) && (get_class($this->controller) === 'Closure'))
{
$controller = $this->controller;
return $controller(...$this->router->params());
}
if (empty($this->controller))
{
throw PageNotFoundException::forEmptyController();
}
if (! class_exists($this->controller, true) || $this->method[0] === '_')
{
throw PageNotFoundException::forControllerNotFound($this->controller, $this->method);
}
else if (! method_exists($this->controller, '_remap') &&
! is_callable([$this->controller, $this->method], false)
)
{
throw PageNotFoundException::forMethodNotFound($this->method);
}
}
protected function createController()
{
$class = new $this->controller();
$class->initController($this->request, $this->response, Services::logger());
$this->benchmark->stop('controller_constructor');
return $class;
}
protected function runController($class)
{
if (method_exists($class, '_remap'))
{
$output = $class->_remap($this->method, ...$this->router->params());
}
else
{
$output = $class->{$this->method}(...$this->router->params());
}
$this->benchmark->stop('controller');
return $output;
}
protected function display404errors(PageNotFoundException $e)
{
if ($override = $this->router->get404Override())
{
if ($override instanceof Closure)
{
echo $override($e->getMessage());
}
else if (is_array($override))
{
$this->benchmark->start('controller');
$this->benchmark->start('controller_constructor');
$this->controller = $override[0];
$this->method = $override[1];
unset($override);
$controller = $this->createController();
$this->runController($controller);
}
$this->gatherOutput();
$this->sendResponse();
return;
}
$this->response->setStatusCode($e->getCode());
if (ENVIRONMENT !== 'testing')
{
if (ob_get_level() > 0)
{
ob_end_flush();
}
}
else
{
if (ob_get_level() > 2)
{
ob_end_flush();
}
}
throw PageNotFoundException::forPageNotFound($e->getMessage());
}
protected function gatherOutput($cacheConfig = null, $returned = null)
{
$this->output = ob_get_contents();
if (ob_get_length())
{
ob_end_clean();
}
if ($returned instanceof DownloadResponse)
{
$this->response = $returned;
return;
}
if ($returned instanceof Response)
{
$this->response = $returned;
$returned = $returned->getBody();
}
if (is_string($returned))
{
$this->output .= $returned;
}
if (static::$cacheTTL > 0)
{
$this->cachePage($cacheConfig);
}
$this->output = $this->displayPerformanceMetrics($this->output);
$this->response->setBody($this->output);
}
public function storePreviousURL($uri)
{
if (is_cli())
{
return;
}
if (method_exists($this->request, 'isAJAX') && $this->request->isAJAX())
{
return;
}
if (is_string($uri))
{
$uri = new URI($uri);
}
if (isset($_SESSION))
{
$_SESSION['_ci_previous_url'] = (string) $uri;
}
}
public function spoofRequestMethod()
{
if ($this->request->getMethod() !== 'post')
{
return;
}
$method = $this->request->getPost('_method');
if (empty($method))
{
return;
}
$this->request = $this->request->setMethod($method);
}
protected function sendResponse()
{
$this->response->pretend($this->useSafeOutput)->send();
}
protected function callExit($code)
{
exit($code);
}
}