/vendor/DCBase/FrontController.php
PHP | 298 lines | 199 code | 50 blank | 49 comment | 70 complexity | 8082becccdf742512a19199d0337e60b MD5 | raw file
- <?php
- /**
- *
- * @package dcbase
- * @copyright (c) 2020 Direct Connect Network Foundation / www.dcbase.org
- * @license https://www.dcbase.org/DCBase/LICENSE GNU General Public License v2
- *
- */
- declare(strict_types = 1);
- namespace DCBase;
- /**
- * @ignore
- */
- if (!defined('IN_DCBASE')) exit;
- use Throwable;
- use Exception;
- use ErrorException;
- use SplFileInfo;
- use DCBase\WebRequest;
- use DCBase\WebResponse;
- use DCBase\Handler\AbstractHandler;
- use DCBase\Response\ResponseHelper;
- use DCBase\ErrorReporting;
- use DCBase\Common\StrUtil as str;
- use DCBase\Common\Util;
- /**
- * Slim front controller implementation
- */
- class FrontController
- {
- /**
- * Constructor
- */
- public static function create()
- {
- return new static();
- }
- protected function __construct()
- {
- // set error handlers
- set_error_handler(array($this, 'errorHandler'));
- set_exception_handler(array($this, 'exceptionHandler'));
- if (!is_writable(DCBASE_CACHE_PATH))
- throw new Exception('Cache directory not writable.');
- }
- /**
- * The main method, pull and server content based on PATH_INFO
- */
- public function run(WebRequest $request) : WebResponse
- {
- $path_info = trim(rawurldecode($request->getPathInfo()));
- if (empty($path_info))
- $path_info = '/';
- $matches = array();
- if ((str::strlen($path_info) > 255) || !preg_match('#^(?:(?!.*\/\.)[\pL\pN_\/\-\. \+]+?)(?P<extension>\.[a-z_0-9]+)?$#ui', $path_info, $matches))
- return WebResponse::error($request, 403);
- if (isset($matches['extension']) && $matches['extension'] === '.' . PHP_EXT)
- return WebResponse::error($request, 403);
- if (isset($matches['extension']) && ($matches['extension'] === '.html' || $matches['extension'] === '.htm'))
- {
- $matches[0] = str_replace(array('.html', '.htm'), '', $matches[0]);
- $matches[0] .= '.html.twig';
- }
- // Remove suprefluous slashes (also ensures inconsistent server config will not break things)
- $matches[0] = trim($matches[0], '/');
- // Disallow symlinks to simplify processing of paths significantly
- $file = new SplFileInfo($request->getContentRoot() . $matches[0]);
- if ($file->isLink())
- return WebResponse::error($request, 403);
- // Make sure we are browsing this resource with the best possible URI
- $response = $this->lookupCanonicalURL($request, $file);
- if ($response instanceof WebResponse)
- return $response;
- // Check if we have some external handler for this request
- $response = $this->processExtendedResource($request, $matches[0], $file);
- if ($response instanceof WebResponse)
- return $response;
- // These should be served as soon as possible
- $response = ResponseHelper::serveStaticFile($request, $file);
- if ($response instanceof WebResponse)
- return $response;
- // Here we try our best to find a page by searching for multiple file types (may update $file or return HTTP error response)
- $response = $this->lookupFile($request, $file);
- if ($response instanceof WebResponse)
- return $response;
- // Automatic directory indexes
- if ($file->isDir())
- {
- $response = WebResponse::directory($request, $request->getContentRoot(), $matches[0]);
- if ($response instanceof WebResponse)
- return $response;
- }
- // This is the most likely scenario, a twig template file of indeterminate type
- if ($file->getExtension() === 'twig')
- {
- $basename = $file->getBaseName('.twig');
- if (str::substr($basename, -3) === '.md' || str::substr($basename, -9) === '.markdown')
- {
- $response = ResponseHelper::serveMarkdownFile($request, $file);
- if ($response instanceof WebResponse)
- return $response;
- }
- $response = ResponseHelper::serveTwigFile($request, $file);
- if ($response instanceof WebResponse)
- return $response;
- }
- // Markdown is cool.... but its extensions aren't
- if ($file->getExtension() === 'md' || $file->getExtension() === 'markdown')
- {
- $response = ResponseHelper::serveMarkdownFile($request, $file);
- if ($response instanceof WebResponse)
- return $response;
- }
- // Still here, this should never happen...
- return WebResponse::error($request, 500);
- }
- protected function processExtendedResource(WebRequest $request, string $relative_path, ?SplFileInfo $file = null) : ?WebResponse
- {
- $handler_map = ConfigLoader::load('HandlerMap');
- if (empty($handler_map))
- return null;
- $handler = $request->parsePathInfo($handler_map, $relative_path);
- if ($handler instanceof AbstractHandler)
- return $handler->handleResource($request, $relative_path, $file);
- return null;
- }
- protected function lookupCanonicalURL(WebRequest $request, SplFileInfo $file) : ?WebResponse
- {
- $host_map = ConfigLoader::load('HostMap');
- if (empty($host_map) || empty($file->getRealPath()))
- return null;
- $scheme = ($request->secure() ? 'https' : 'http');
- if (!isset($host_map[$request->host()]))
- $host_map[$request->host()] = $request->getContentRoot();
- $roots = array_map(function($value) { return rtrim($value, '/'); }, array_values($host_map));
- $hosts = array_map(function($value) use($scheme) { return "{$scheme}://{$value}"; }, array_keys($host_map));
- // symlinks aren't followed so this is fine...
- $canonical_url = rtrim(str_replace($roots, $hosts, $file->getRealPath()), '/');
- $request_url = explode('?', $request->getRequestURL(), 2);
- if ($file->isFile() && $file->getExtension() === 'twig')
- {
- // drop the filename them decide whether to add it back or not
- $canonical_url = str::substr($canonical_url, 0, str::strrpos($canonical_url, '/'));
- $basename = $file->getBaseName('.twig');
- if ($basename !== 'index.html')
- $canonical_url .= "/{$basename}";
- if (str::strrpos($basename, '.') === false)
- $canonical_url .= '.twig';
- }
- if ($canonical_url !== $request_url[0])
- return WebResponse::redirect($request, $canonical_url . (isset($request_url[1]) && !empty($request_url[1]) ? "?{$request_url[1]}" : ''), 301);
- // we are already using the best URL we know, for now we don't redirect from FQFN to any fuzzy names, see lookupFile(...)
- return null;
- }
- /**
- * Do some limited one level deep fuzzy lookup for files
- */
- protected function lookupFile(WebRequest $request, SplFileInfo &$file, array $extensions = array()) : ?WebResponse
- {
- if ($file->isLink() || ($file->isFile() || $file->isDir()) && !$file->isReadable())
- return WebResponse::error($request, 403);
- if (!$file->isDir() && $file->isFile())
- return null;
- if (empty($extensions))
- $extensions = array('html.twig', 'md', 'markdown', 'md.twig', 'markdown.twig');
- $lookup_path = $file->getPathname();
- if (!$file->isDir() && !empty($file->getExtension()))
- {
- $patterns = array_map(function ($ext) { return '#\.' . str_replace('.', '\.', $ext) . '$#'; }, $extensions);
- $lookup_path = preg_replace($patterns, '', $lookup_path);
- }
- else if ($file->isDir())
- {
- $lookup_path = $file->getRealPath() . '/index';
- }
- $found_file = false;
- $last_error = $file->isDir() ? 403 : 404;
- foreach ($extensions as $ext)
- {
- $tmp = new SplFileInfo("$lookup_path.$ext");
- if ($tmp->isFile() && $tmp->isReadable() && !$tmp->isLink())
- {
- $found_file = true;
- $file = $tmp;
- break;
- }
- else if (($tmp->isFile() || $tmp->isDir()) && ($tmp->isLink() || !$tmp->isReadable()))
- {
- $last_error = 403;
- }
- }
- // this is a file or candidate for directory indexing
- if ($found_file || (!$found_file && $file->isDir()))
- return null;
- return WebResponse::error($request, $last_error);
- }
- /**
- * PHP Error handler
- */
- public function errorHandler(int $errno, string $message, ?string $file = null, ?int $line = null)
- {
- // Do not display notices if we suppress them via @
- if (error_reporting() == 0 && $errno != E_USER_ERROR && $errno != E_USER_WARNING && $errno != E_USER_NOTICE)
- return true;
- // Check the error reporting level and return if the error level does not match
- if (($errno & (Util::isDebug() ? (E_ALL | E_STRICT) : error_reporting())) == 0)
- return true;
- switch ($errno)
- {
- case E_NOTICE:
- case E_WARNING:
- case E_USER_NOTICE:
- case E_USER_WARNING:
- case E_DEPRECATED:
- case E_STRICT:
- print(ErrorReporting::formatError($errno, $message, $file, $line, true));
- return true;
- break;
- }
- // Guard against error loop
- if (defined('DCBASE_FATAL_ERROR'))
- return true;
- throw new ErrorException($message, 0, $errno, $file, $line);
- }
- /**
- * PHP Exception handler
- */
- public function exceptionHandler(Throwable $exception)
- {
- // Let's prevent error loops
- define('DCBASE_FATAL_ERROR', true);
- // Pretty errors, although we'd rather there be none...
- $error_format = ErrorReporting::formatException($exception, true);
- try
- {
- // Send as '503 - Service Unavailable'
- $response = WebResponse::error(WebRequest::create(), 503, $error_format);
- $response->send();
- }
- catch (Throwable $e)
- {
- $original_error = "Original Error: $error_format";
- $error_format = sprintf('%s thrown within the exception handler. Message: %s on line %d. <br />', get_class($e), $e->getMessage(), $e->getLine()) . PHP_EOL;
- $error_format .= "<br /> $original_error";
- exit($error_format);
- }
- }
- }