Grav.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. <?php
  2. /**
  3. * @package Grav.Common
  4. *
  5. * @copyright Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use Grav\Common\Config\Config;
  10. use Grav\Common\Page\Medium\ImageMedium;
  11. use Grav\Common\Page\Medium\Medium;
  12. use Grav\Common\Page\Page;
  13. use RocketTheme\Toolbox\DI\Container;
  14. use RocketTheme\Toolbox\Event\Event;
  15. use RocketTheme\Toolbox\Event\EventDispatcher;
  16. class Grav extends Container
  17. {
  18. /**
  19. * @var string Processed output for the page.
  20. */
  21. public $output;
  22. /**
  23. * @var static The singleton instance
  24. */
  25. protected static $instance;
  26. /**
  27. * @var array Contains all Services and ServicesProviders that are mapped
  28. * to the dependency injection container.
  29. */
  30. protected static $diMap = [
  31. 'Grav\Common\Service\LoggerServiceProvider',
  32. 'Grav\Common\Service\ErrorServiceProvider',
  33. 'uri' => 'Grav\Common\Uri',
  34. 'events' => 'RocketTheme\Toolbox\Event\EventDispatcher',
  35. 'cache' => 'Grav\Common\Cache',
  36. 'Grav\Common\Service\SessionServiceProvider',
  37. 'plugins' => 'Grav\Common\Plugins',
  38. 'themes' => 'Grav\Common\Themes',
  39. 'twig' => 'Grav\Common\Twig\Twig',
  40. 'taxonomy' => 'Grav\Common\Taxonomy',
  41. 'language' => 'Grav\Common\Language\Language',
  42. 'pages' => 'Grav\Common\Page\Pages',
  43. 'Grav\Common\Service\TaskServiceProvider',
  44. 'Grav\Common\Service\AssetsServiceProvider',
  45. 'Grav\Common\Service\PageServiceProvider',
  46. 'Grav\Common\Service\OutputServiceProvider',
  47. 'browser' => 'Grav\Common\Browser',
  48. 'exif' => 'Grav\Common\Helpers\Exif',
  49. 'Grav\Common\Service\StreamsServiceProvider',
  50. 'Grav\Common\Service\ConfigServiceProvider',
  51. 'inflector' => 'Grav\Common\Inflector',
  52. 'siteSetupProcessor' => 'Grav\Common\Processors\SiteSetupProcessor',
  53. 'configurationProcessor' => 'Grav\Common\Processors\ConfigurationProcessor',
  54. 'errorsProcessor' => 'Grav\Common\Processors\ErrorsProcessor',
  55. 'debuggerInitProcessor' => 'Grav\Common\Processors\DebuggerInitProcessor',
  56. 'initializeProcessor' => 'Grav\Common\Processors\InitializeProcessor',
  57. 'pluginsProcessor' => 'Grav\Common\Processors\PluginsProcessor',
  58. 'themesProcessor' => 'Grav\Common\Processors\ThemesProcessor',
  59. 'tasksProcessor' => 'Grav\Common\Processors\TasksProcessor',
  60. 'assetsProcessor' => 'Grav\Common\Processors\AssetsProcessor',
  61. 'twigProcessor' => 'Grav\Common\Processors\TwigProcessor',
  62. 'pagesProcessor' => 'Grav\Common\Processors\PagesProcessor',
  63. 'debuggerAssetsProcessor' => 'Grav\Common\Processors\DebuggerAssetsProcessor',
  64. 'renderProcessor' => 'Grav\Common\Processors\RenderProcessor',
  65. ];
  66. /**
  67. * @var array All processors that are processed in $this->process()
  68. */
  69. protected $processors = [
  70. 'siteSetupProcessor',
  71. 'configurationProcessor',
  72. 'errorsProcessor',
  73. 'debuggerInitProcessor',
  74. 'initializeProcessor',
  75. 'pluginsProcessor',
  76. 'themesProcessor',
  77. 'tasksProcessor',
  78. 'assetsProcessor',
  79. 'twigProcessor',
  80. 'pagesProcessor',
  81. 'debuggerAssetsProcessor',
  82. 'renderProcessor',
  83. ];
  84. /**
  85. * Reset the Grav instance.
  86. */
  87. public static function resetInstance()
  88. {
  89. if (self::$instance) {
  90. self::$instance = null;
  91. }
  92. }
  93. /**
  94. * Return the Grav instance. Create it if it's not already instanced
  95. *
  96. * @param array $values
  97. *
  98. * @return Grav
  99. */
  100. public static function instance(array $values = [])
  101. {
  102. if (!self::$instance) {
  103. self::$instance = static::load($values);
  104. } elseif ($values) {
  105. $instance = self::$instance;
  106. foreach ($values as $key => $value) {
  107. $instance->offsetSet($key, $value);
  108. }
  109. }
  110. return self::$instance;
  111. }
  112. /**
  113. * Process a request
  114. */
  115. public function process()
  116. {
  117. // process all processors (e.g. config, initialize, assets, ..., render)
  118. foreach ($this->processors as $processor) {
  119. $processor = $this[$processor];
  120. $this->measureTime($processor->id, $processor->title, function () use ($processor) {
  121. $processor->process();
  122. });
  123. }
  124. /** @var Debugger $debugger */
  125. $debugger = $this['debugger'];
  126. $debugger->render();
  127. register_shutdown_function([$this, 'shutdown']);
  128. }
  129. /**
  130. * Set the system locale based on the language and configuration
  131. */
  132. public function setLocale()
  133. {
  134. // Initialize Locale if set and configured.
  135. if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
  136. $language = $this['language']->getLanguage();
  137. setlocale(LC_ALL, strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
  138. } elseif ($this['config']->get('system.default_locale')) {
  139. setlocale(LC_ALL, $this['config']->get('system.default_locale'));
  140. }
  141. }
  142. /**
  143. * Redirect browser to another location.
  144. *
  145. * @param string $route Internal route.
  146. * @param int $code Redirection code (30x)
  147. */
  148. public function redirect($route, $code = null)
  149. {
  150. /** @var Uri $uri */
  151. $uri = $this['uri'];
  152. //Check for code in route
  153. $regex = '/.*(\[(30[1-7])\])$/';
  154. preg_match($regex, $route, $matches);
  155. if ($matches) {
  156. $route = str_replace($matches[1], '', $matches[0]);
  157. $code = $matches[2];
  158. }
  159. if ($code === null) {
  160. $code = $this['config']->get('system.pages.redirect_default_code', 302);
  161. }
  162. if (isset($this['session'])) {
  163. $this['session']->close();
  164. }
  165. if ($uri->isExternal($route)) {
  166. $url = $route;
  167. } else {
  168. $url = rtrim($uri->rootUrl(), '/') . '/';
  169. if ($this['config']->get('system.pages.redirect_trailing_slash', true)) {
  170. $url .= trim($route, '/'); // Remove trailing slash
  171. } else {
  172. $url .= ltrim($route, '/'); // Support trailing slash default routes
  173. }
  174. }
  175. header("Location: {$url}", true, $code);
  176. exit();
  177. }
  178. /**
  179. * Redirect browser to another location taking language into account (preferred)
  180. *
  181. * @param string $route Internal route.
  182. * @param int $code Redirection code (30x)
  183. */
  184. public function redirectLangSafe($route, $code = null)
  185. {
  186. if (!$this['uri']->isExternal($route)) {
  187. $this->redirect($this['pages']->route($route), $code);
  188. } else {
  189. $this->redirect($route, $code);
  190. }
  191. }
  192. /**
  193. * Set response header.
  194. */
  195. public function header()
  196. {
  197. /** @var Page $page */
  198. $page = $this['page'];
  199. $format = $page->templateFormat();
  200. header('Content-type: ' . Utils::getMimeByExtension($format, 'text/html'));
  201. $cache_control = $page->cacheControl();
  202. // Calculate Expires Headers if set to > 0
  203. $expires = $page->expires();
  204. if ($expires > 0) {
  205. $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
  206. if (!$cache_control) {
  207. header('Cache-Control: max-age=' . $expires);
  208. }
  209. header('Expires: ' . $expires_date);
  210. }
  211. // Set cache-control header
  212. if ($cache_control) {
  213. header('Cache-Control: ' . strtolower($cache_control));
  214. }
  215. // Set the last modified time
  216. if ($page->lastModified()) {
  217. $last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT';
  218. header('Last-Modified: ' . $last_modified_date);
  219. }
  220. // Calculate a Hash based on the raw file
  221. if ($page->eTag()) {
  222. header('ETag: "' . md5($page->raw() . $page->modified()).'"');
  223. }
  224. // Set HTTP response code
  225. if (isset($this['page']->header()->http_response_code)) {
  226. http_response_code($this['page']->header()->http_response_code);
  227. }
  228. // Vary: Accept-Encoding
  229. if ($this['config']->get('system.pages.vary_accept_encoding', false)) {
  230. header('Vary: Accept-Encoding');
  231. }
  232. }
  233. /**
  234. * Fires an event with optional parameters.
  235. *
  236. * @param string $eventName
  237. * @param Event $event
  238. *
  239. * @return Event
  240. */
  241. public function fireEvent($eventName, Event $event = null)
  242. {
  243. /** @var EventDispatcher $events */
  244. $events = $this['events'];
  245. return $events->dispatch($eventName, $event);
  246. }
  247. /**
  248. * Set the final content length for the page and flush the buffer
  249. *
  250. */
  251. public function shutdown()
  252. {
  253. // Prevent user abort allowing onShutdown event to run without interruptions.
  254. if (function_exists('ignore_user_abort')) {
  255. @ignore_user_abort(true);
  256. }
  257. // Close the session allowing new requests to be handled.
  258. if (isset($this['session'])) {
  259. $this['session']->close();
  260. }
  261. if ($this['config']->get('system.debugger.shutdown.close_connection', true)) {
  262. // Flush the response and close the connection to allow time consuming tasks to be performed without leaving
  263. // the connection to the client open. This will make page loads to feel much faster.
  264. // FastCGI allows us to flush all response data to the client and finish the request.
  265. $success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
  266. if (!$success) {
  267. // Unfortunately without FastCGI there is no way to force close the connection.
  268. // We need to ask browser to close the connection for us.
  269. if ($this['config']->get('system.cache.gzip')) {
  270. // Flush gzhandler buffer if gzip setting was enabled.
  271. ob_end_flush();
  272. } else {
  273. // Without gzip we have no other choice than to prevent server from compressing the output.
  274. // This action turns off mod_deflate which would prevent us from closing the connection.
  275. if ($this['config']->get('system.cache.allow_webserver_gzip')) {
  276. header('Content-Encoding: identity');
  277. } else {
  278. header('Content-Encoding: none');
  279. }
  280. }
  281. // Get length and close the connection.
  282. header('Content-Length: ' . ob_get_length());
  283. header("Connection: close");
  284. ob_end_flush();
  285. @ob_flush();
  286. flush();
  287. }
  288. }
  289. // Run any time consuming tasks.
  290. $this->fireEvent('onShutdown');
  291. }
  292. /**
  293. * Magic Catch All Function
  294. * Used to call closures like measureTime on the instance.
  295. * Source: http://stackoverflow.com/questions/419804/closures-as-class-members
  296. */
  297. public function __call($method, $args)
  298. {
  299. $closure = $this->$method;
  300. call_user_func_array($closure, $args);
  301. }
  302. /**
  303. * Initialize and return a Grav instance
  304. *
  305. * @param array $values
  306. *
  307. * @return static
  308. */
  309. protected static function load(array $values)
  310. {
  311. $container = new static($values);
  312. $container['grav'] = $container;
  313. $container['debugger'] = new Debugger();
  314. $debugger = $container['debugger'];
  315. // closure that measures time by wrapping a function into startTimer and stopTimer
  316. // The debugger can be passed to the closure. Should be more performant
  317. // then to get it from the container all time.
  318. $container->measureTime = function ($timerId, $timerTitle, $callback) use ($debugger) {
  319. $debugger->startTimer($timerId, $timerTitle);
  320. $callback();
  321. $debugger->stopTimer($timerId);
  322. };
  323. $container->measureTime('_services', 'Services', function () use ($container) {
  324. $container->registerServices($container);
  325. });
  326. return $container;
  327. }
  328. /**
  329. * Register all services
  330. * Services are defined in the diMap. They can either only the class
  331. * of a Service Provider or a pair of serviceKey => serviceClass that
  332. * gets directly mapped into the container.
  333. *
  334. * @return void
  335. */
  336. protected function registerServices()
  337. {
  338. foreach (self::$diMap as $serviceKey => $serviceClass) {
  339. if (is_int($serviceKey)) {
  340. $this->registerServiceProvider($serviceClass);
  341. } else {
  342. $this->registerService($serviceKey, $serviceClass);
  343. }
  344. }
  345. }
  346. /**
  347. * Register a service provider with the container.
  348. *
  349. * @param string $serviceClass
  350. *
  351. * @return void
  352. */
  353. protected function registerServiceProvider($serviceClass)
  354. {
  355. $this->register(new $serviceClass);
  356. }
  357. /**
  358. * Register a service with the container.
  359. *
  360. * @param string $serviceKey
  361. * @param string $serviceClass
  362. *
  363. * @return void
  364. */
  365. protected function registerService($serviceKey, $serviceClass)
  366. {
  367. $this[$serviceKey] = function ($c) use ($serviceClass) {
  368. return new $serviceClass($c);
  369. };
  370. }
  371. /**
  372. * This attempts to find media, other files, and download them
  373. *
  374. * @param $path
  375. */
  376. public function fallbackUrl($path)
  377. {
  378. $this->fireEvent('onPageFallBackUrl');
  379. /** @var Uri $uri */
  380. $uri = $this['uri'];
  381. /** @var Config $config */
  382. $config = $this['config'];
  383. $uri_extension = strtolower($uri->extension());
  384. $fallback_types = $config->get('system.media.allowed_fallback_types', null);
  385. $supported_types = $config->get('media.types');
  386. // Check whitelist first, then ensure extension is a valid media type
  387. if (!empty($fallback_types) && !\in_array($uri_extension, $fallback_types, true)) {
  388. return false;
  389. }
  390. if (!array_key_exists($uri_extension, $supported_types)) {
  391. return false;
  392. }
  393. $path_parts = pathinfo($path);
  394. /** @var Page $page */
  395. $page = $this['pages']->dispatch($path_parts['dirname'], true);
  396. if ($page) {
  397. $media = $page->media()->all();
  398. $parsed_url = parse_url(rawurldecode($uri->basename()));
  399. $media_file = $parsed_url['path'];
  400. // if this is a media object, try actions first
  401. if (isset($media[$media_file])) {
  402. /** @var Medium $medium */
  403. $medium = $media[$media_file];
  404. foreach ($uri->query(null, true) as $action => $params) {
  405. if (in_array($action, ImageMedium::$magic_actions)) {
  406. call_user_func_array([&$medium, $action], explode(',', $params));
  407. }
  408. }
  409. Utils::download($medium->path(), false);
  410. }
  411. // unsupported media type, try to download it...
  412. if ($uri_extension) {
  413. $extension = $uri_extension;
  414. } else {
  415. if (isset($path_parts['extension'])) {
  416. $extension = $path_parts['extension'];
  417. } else {
  418. $extension = null;
  419. }
  420. }
  421. if ($extension) {
  422. $download = true;
  423. if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []))) {
  424. $download = false;
  425. }
  426. Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
  427. }
  428. // Nothing found
  429. return false;
  430. }
  431. return $page;
  432. }
  433. }