PageRenderTime 42ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/DCBase/FrontController.php

https://gitlab.com/dcnf/dcbase.org
PHP | 298 lines | 199 code | 50 blank | 49 comment | 70 complexity | 8082becccdf742512a19199d0337e60b MD5 | raw file
  1. <?php
  2. /**
  3. *
  4. * @package dcbase
  5. * @copyright (c) 2020 Direct Connect Network Foundation / www.dcbase.org
  6. * @license https://www.dcbase.org/DCBase/LICENSE GNU General Public License v2
  7. *
  8. */
  9. declare(strict_types = 1);
  10. namespace DCBase;
  11. /**
  12. * @ignore
  13. */
  14. if (!defined('IN_DCBASE')) exit;
  15. use Throwable;
  16. use Exception;
  17. use ErrorException;
  18. use SplFileInfo;
  19. use DCBase\WebRequest;
  20. use DCBase\WebResponse;
  21. use DCBase\Handler\AbstractHandler;
  22. use DCBase\Response\ResponseHelper;
  23. use DCBase\ErrorReporting;
  24. use DCBase\Common\StrUtil as str;
  25. use DCBase\Common\Util;
  26. /**
  27. * Slim front controller implementation
  28. */
  29. class FrontController
  30. {
  31. /**
  32. * Constructor
  33. */
  34. public static function create()
  35. {
  36. return new static();
  37. }
  38. protected function __construct()
  39. {
  40. // set error handlers
  41. set_error_handler(array($this, 'errorHandler'));
  42. set_exception_handler(array($this, 'exceptionHandler'));
  43. if (!is_writable(DCBASE_CACHE_PATH))
  44. throw new Exception('Cache directory not writable.');
  45. }
  46. /**
  47. * The main method, pull and server content based on PATH_INFO
  48. */
  49. public function run(WebRequest $request) : WebResponse
  50. {
  51. $path_info = trim(rawurldecode($request->getPathInfo()));
  52. if (empty($path_info))
  53. $path_info = '/';
  54. $matches = array();
  55. if ((str::strlen($path_info) > 255) || !preg_match('#^(?:(?!.*\/\.)[\pL\pN_\/\-\. \+]+?)(?P<extension>\.[a-z_0-9]+)?$#ui', $path_info, $matches))
  56. return WebResponse::error($request, 403);
  57. if (isset($matches['extension']) && $matches['extension'] === '.' . PHP_EXT)
  58. return WebResponse::error($request, 403);
  59. if (isset($matches['extension']) && ($matches['extension'] === '.html' || $matches['extension'] === '.htm'))
  60. {
  61. $matches[0] = str_replace(array('.html', '.htm'), '', $matches[0]);
  62. $matches[0] .= '.html.twig';
  63. }
  64. // Remove suprefluous slashes (also ensures inconsistent server config will not break things)
  65. $matches[0] = trim($matches[0], '/');
  66. // Disallow symlinks to simplify processing of paths significantly
  67. $file = new SplFileInfo($request->getContentRoot() . $matches[0]);
  68. if ($file->isLink())
  69. return WebResponse::error($request, 403);
  70. // Make sure we are browsing this resource with the best possible URI
  71. $response = $this->lookupCanonicalURL($request, $file);
  72. if ($response instanceof WebResponse)
  73. return $response;
  74. // Check if we have some external handler for this request
  75. $response = $this->processExtendedResource($request, $matches[0], $file);
  76. if ($response instanceof WebResponse)
  77. return $response;
  78. // These should be served as soon as possible
  79. $response = ResponseHelper::serveStaticFile($request, $file);
  80. if ($response instanceof WebResponse)
  81. return $response;
  82. // Here we try our best to find a page by searching for multiple file types (may update $file or return HTTP error response)
  83. $response = $this->lookupFile($request, $file);
  84. if ($response instanceof WebResponse)
  85. return $response;
  86. // Automatic directory indexes
  87. if ($file->isDir())
  88. {
  89. $response = WebResponse::directory($request, $request->getContentRoot(), $matches[0]);
  90. if ($response instanceof WebResponse)
  91. return $response;
  92. }
  93. // This is the most likely scenario, a twig template file of indeterminate type
  94. if ($file->getExtension() === 'twig')
  95. {
  96. $basename = $file->getBaseName('.twig');
  97. if (str::substr($basename, -3) === '.md' || str::substr($basename, -9) === '.markdown')
  98. {
  99. $response = ResponseHelper::serveMarkdownFile($request, $file);
  100. if ($response instanceof WebResponse)
  101. return $response;
  102. }
  103. $response = ResponseHelper::serveTwigFile($request, $file);
  104. if ($response instanceof WebResponse)
  105. return $response;
  106. }
  107. // Markdown is cool.... but its extensions aren't
  108. if ($file->getExtension() === 'md' || $file->getExtension() === 'markdown')
  109. {
  110. $response = ResponseHelper::serveMarkdownFile($request, $file);
  111. if ($response instanceof WebResponse)
  112. return $response;
  113. }
  114. // Still here, this should never happen...
  115. return WebResponse::error($request, 500);
  116. }
  117. protected function processExtendedResource(WebRequest $request, string $relative_path, ?SplFileInfo $file = null) : ?WebResponse
  118. {
  119. $handler_map = ConfigLoader::load('HandlerMap');
  120. if (empty($handler_map))
  121. return null;
  122. $handler = $request->parsePathInfo($handler_map, $relative_path);
  123. if ($handler instanceof AbstractHandler)
  124. return $handler->handleResource($request, $relative_path, $file);
  125. return null;
  126. }
  127. protected function lookupCanonicalURL(WebRequest $request, SplFileInfo $file) : ?WebResponse
  128. {
  129. $host_map = ConfigLoader::load('HostMap');
  130. if (empty($host_map) || empty($file->getRealPath()))
  131. return null;
  132. $scheme = ($request->secure() ? 'https' : 'http');
  133. if (!isset($host_map[$request->host()]))
  134. $host_map[$request->host()] = $request->getContentRoot();
  135. $roots = array_map(function($value) { return rtrim($value, '/'); }, array_values($host_map));
  136. $hosts = array_map(function($value) use($scheme) { return "{$scheme}://{$value}"; }, array_keys($host_map));
  137. // symlinks aren't followed so this is fine...
  138. $canonical_url = rtrim(str_replace($roots, $hosts, $file->getRealPath()), '/');
  139. $request_url = explode('?', $request->getRequestURL(), 2);
  140. if ($file->isFile() && $file->getExtension() === 'twig')
  141. {
  142. // drop the filename them decide whether to add it back or not
  143. $canonical_url = str::substr($canonical_url, 0, str::strrpos($canonical_url, '/'));
  144. $basename = $file->getBaseName('.twig');
  145. if ($basename !== 'index.html')
  146. $canonical_url .= "/{$basename}";
  147. if (str::strrpos($basename, '.') === false)
  148. $canonical_url .= '.twig';
  149. }
  150. if ($canonical_url !== $request_url[0])
  151. return WebResponse::redirect($request, $canonical_url . (isset($request_url[1]) && !empty($request_url[1]) ? "?{$request_url[1]}" : ''), 301);
  152. // we are already using the best URL we know, for now we don't redirect from FQFN to any fuzzy names, see lookupFile(...)
  153. return null;
  154. }
  155. /**
  156. * Do some limited one level deep fuzzy lookup for files
  157. */
  158. protected function lookupFile(WebRequest $request, SplFileInfo &$file, array $extensions = array()) : ?WebResponse
  159. {
  160. if ($file->isLink() || ($file->isFile() || $file->isDir()) && !$file->isReadable())
  161. return WebResponse::error($request, 403);
  162. if (!$file->isDir() && $file->isFile())
  163. return null;
  164. if (empty($extensions))
  165. $extensions = array('html.twig', 'md', 'markdown', 'md.twig', 'markdown.twig');
  166. $lookup_path = $file->getPathname();
  167. if (!$file->isDir() && !empty($file->getExtension()))
  168. {
  169. $patterns = array_map(function ($ext) { return '#\.' . str_replace('.', '\.', $ext) . '$#'; }, $extensions);
  170. $lookup_path = preg_replace($patterns, '', $lookup_path);
  171. }
  172. else if ($file->isDir())
  173. {
  174. $lookup_path = $file->getRealPath() . '/index';
  175. }
  176. $found_file = false;
  177. $last_error = $file->isDir() ? 403 : 404;
  178. foreach ($extensions as $ext)
  179. {
  180. $tmp = new SplFileInfo("$lookup_path.$ext");
  181. if ($tmp->isFile() && $tmp->isReadable() && !$tmp->isLink())
  182. {
  183. $found_file = true;
  184. $file = $tmp;
  185. break;
  186. }
  187. else if (($tmp->isFile() || $tmp->isDir()) && ($tmp->isLink() || !$tmp->isReadable()))
  188. {
  189. $last_error = 403;
  190. }
  191. }
  192. // this is a file or candidate for directory indexing
  193. if ($found_file || (!$found_file && $file->isDir()))
  194. return null;
  195. return WebResponse::error($request, $last_error);
  196. }
  197. /**
  198. * PHP Error handler
  199. */
  200. public function errorHandler(int $errno, string $message, ?string $file = null, ?int $line = null)
  201. {
  202. // Do not display notices if we suppress them via @
  203. if (error_reporting() == 0 && $errno != E_USER_ERROR && $errno != E_USER_WARNING && $errno != E_USER_NOTICE)
  204. return true;
  205. // Check the error reporting level and return if the error level does not match
  206. if (($errno & (Util::isDebug() ? (E_ALL | E_STRICT) : error_reporting())) == 0)
  207. return true;
  208. switch ($errno)
  209. {
  210. case E_NOTICE:
  211. case E_WARNING:
  212. case E_USER_NOTICE:
  213. case E_USER_WARNING:
  214. case E_DEPRECATED:
  215. case E_STRICT:
  216. print(ErrorReporting::formatError($errno, $message, $file, $line, true));
  217. return true;
  218. break;
  219. }
  220. // Guard against error loop
  221. if (defined('DCBASE_FATAL_ERROR'))
  222. return true;
  223. throw new ErrorException($message, 0, $errno, $file, $line);
  224. }
  225. /**
  226. * PHP Exception handler
  227. */
  228. public function exceptionHandler(Throwable $exception)
  229. {
  230. // Let's prevent error loops
  231. define('DCBASE_FATAL_ERROR', true);
  232. // Pretty errors, although we'd rather there be none...
  233. $error_format = ErrorReporting::formatException($exception, true);
  234. try
  235. {
  236. // Send as '503 - Service Unavailable'
  237. $response = WebResponse::error(WebRequest::create(), 503, $error_format);
  238. $response->send();
  239. }
  240. catch (Throwable $e)
  241. {
  242. $original_error = "Original Error: $error_format";
  243. $error_format = sprintf('%s thrown within the exception handler. Message: %s on line %d. <br />', get_class($e), $e->getMessage(), $e->getLine()) . PHP_EOL;
  244. $error_format .= "<br /> $original_error";
  245. exit($error_format);
  246. }
  247. }
  248. }