Grav.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <?php
  2. namespace Grav\Common;
  3. use Grav\Common\Language\Language;
  4. use Grav\Common\Page\Medium\ImageMedium;
  5. use Grav\Common\Page\Pages;
  6. use Grav\Common\Service\ConfigServiceProvider;
  7. use Grav\Common\Service\ErrorServiceProvider;
  8. use Grav\Common\Service\LoggerServiceProvider;
  9. use Grav\Common\Service\StreamsServiceProvider;
  10. use Grav\Common\Twig\Twig;
  11. use RocketTheme\Toolbox\DI\Container;
  12. use RocketTheme\Toolbox\Event\Event;
  13. use RocketTheme\Toolbox\Event\EventDispatcher;
  14. /**
  15. * Grav
  16. *
  17. * @author Andy Miller
  18. * @link http://www.rockettheme.com
  19. * @license http://opensource.org/licenses/MIT
  20. *
  21. * Influenced by Pico, Stacey, Kirby, PieCrust and other great platforms...
  22. */
  23. class Grav extends Container
  24. {
  25. /**
  26. * @var string
  27. */
  28. public $output;
  29. /**
  30. * @var static
  31. */
  32. protected static $instance;
  33. public static function instance(array $values = array())
  34. {
  35. if (!self::$instance) {
  36. self::$instance = static::load($values);
  37. GravTrait::setGrav(self::$instance);
  38. } elseif ($values) {
  39. $instance = self::$instance;
  40. foreach ($values as $key => $value) {
  41. $instance->offsetSet($key, $value);
  42. }
  43. }
  44. return self::$instance;
  45. }
  46. protected static function load(array $values)
  47. {
  48. $container = new static($values);
  49. $container['grav'] = $container;
  50. $container['debugger'] = new Debugger();
  51. $container['debugger']->startTimer('_init', 'Initialize');
  52. $container->register(new LoggerServiceProvider);
  53. $container->register(new ErrorServiceProvider);
  54. $container['uri'] = function ($c) {
  55. return new Uri($c);
  56. };
  57. $container['task'] = function ($c) {
  58. return !empty($_POST['task']) ? $_POST['task'] : $c['uri']->param('task');
  59. };
  60. $container['events'] = function ($c) {
  61. return new EventDispatcher;
  62. };
  63. $container['cache'] = function ($c) {
  64. return new Cache($c);
  65. };
  66. $container['session'] = function ($c) {
  67. return new Session($c);
  68. };
  69. $container['plugins'] = function ($c) {
  70. return new Plugins();
  71. };
  72. $container['themes'] = function ($c) {
  73. return new Themes($c);
  74. };
  75. $container['twig'] = function ($c) {
  76. return new Twig($c);
  77. };
  78. $container['taxonomy'] = function ($c) {
  79. return new Taxonomy($c);
  80. };
  81. $container['language'] = function ($c) {
  82. return new Language($c);
  83. };
  84. $container['pages'] = function ($c) {
  85. return new Page\Pages($c);
  86. };
  87. $container['assets'] = new Assets();
  88. $container['page'] = function ($c) {
  89. /** @var Pages $pages */
  90. $pages = $c['pages'];
  91. /** @var Language $language */
  92. $language = $c['language'];
  93. /** @var Uri $uri */
  94. $uri = $c['uri'];
  95. $path = rtrim($uri->path(), '/');
  96. $path = $path ?: '/';
  97. $page = $pages->dispatch($path);
  98. // Redirection tests
  99. if ($page) {
  100. // Language-specific redirection scenarios
  101. if ($language->enabled()) {
  102. if ($language->isLanguageInUrl() && !$language->isIncludeDefaultLanguage()) {
  103. $c->redirect($page->route());
  104. }
  105. if (!$language->isLanguageInUrl() && $language->isIncludeDefaultLanguage()) {
  106. $c->redirectLangSafe($page->route());
  107. }
  108. }
  109. // Default route test and redirect
  110. if ($c['config']->get('system.pages.redirect_default_route') && $page->route() != $path) {
  111. $c->redirectLangSafe($page->route());
  112. }
  113. }
  114. // if page is not found, try some fallback stuff
  115. if (!$page || !$page->routable()) {
  116. // Try fallback URL stuff...
  117. $c->fallbackUrl($page, $path);
  118. // If no page found, fire event
  119. $event = $c->fireEvent('onPageNotFound');
  120. if (isset($event->page)) {
  121. $page = $event->page;
  122. } else {
  123. throw new \RuntimeException('Page Not Found', 404);
  124. }
  125. }
  126. return $page;
  127. };
  128. $container['output'] = function ($c) {
  129. return $c['twig']->processSite($c['uri']->extension());
  130. };
  131. $container['browser'] = function ($c) {
  132. return new Browser();
  133. };
  134. $container['base_url_absolute'] = function ($c) {
  135. return $c['config']->get('system.base_url_absolute') ?: $c['uri']->rootUrl(true);
  136. };
  137. $container['base_url_relative'] = function ($c) {
  138. return $c['config']->get('system.base_url_relative') ?: $c['uri']->rootUrl(false);
  139. };
  140. $container['base_url'] = function ($c) {
  141. return $c['config']->get('system.absolute_urls') ? $c['base_url_absolute'] : $c['base_url_relative'];
  142. };
  143. $container->register(new StreamsServiceProvider);
  144. $container->register(new ConfigServiceProvider);
  145. $container['inflector'] = new Inflector();
  146. $container['debugger']->stopTimer('_init');
  147. return $container;
  148. }
  149. public function process()
  150. {
  151. /** @var Debugger $debugger */
  152. $debugger = $this['debugger'];
  153. // Initialize configuration.
  154. $debugger->startTimer('_config', 'Configuration');
  155. $this['config']->init();
  156. $this['errors']->resetHandlers();
  157. $this['uri']->init();
  158. $this['session']->init();
  159. $debugger->init();
  160. $this['config']->debug();
  161. $debugger->stopTimer('_config');
  162. // Use output buffering to prevent headers from being sent too early.
  163. ob_start();
  164. if ($this['config']->get('system.cache.gzip')) {
  165. ob_start('ob_gzhandler');
  166. }
  167. // Initialize the timezone
  168. if ($this['config']->get('system.timezone')) {
  169. date_default_timezone_set($this['config']->get('system.timezone'));
  170. }
  171. // Initialize Locale if set and configured
  172. if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
  173. setlocale(LC_ALL, $this['language']->getLanguage());
  174. } elseif ($this['config']->get('system.default_locale')) {
  175. setlocale(LC_ALL, $this['config']->get('system.default_locale'));
  176. }
  177. $debugger->startTimer('streams', 'Streams');
  178. $this['streams'];
  179. $debugger->stopTimer('streams');
  180. $debugger->startTimer('plugins', 'Plugins');
  181. $this['plugins']->init();
  182. $this->fireEvent('onPluginsInitialized');
  183. $debugger->stopTimer('plugins');
  184. $debugger->startTimer('themes', 'Themes');
  185. $this['themes']->init();
  186. $this->fireEvent('onThemeInitialized');
  187. $debugger->stopTimer('themes');
  188. $task = $this['task'];
  189. if ($task) {
  190. $this->fireEvent('onTask.' . $task);
  191. }
  192. $this['assets']->init();
  193. $this->fireEvent('onAssetsInitialized');
  194. $debugger->startTimer('twig', 'Twig');
  195. $this['twig']->init();
  196. $debugger->stopTimer('twig');
  197. $debugger->startTimer('pages', 'Pages');
  198. $this['pages']->init();
  199. $this->fireEvent('onPagesInitialized');
  200. $debugger->stopTimer('pages');
  201. $this->fireEvent('onPageInitialized');
  202. $debugger->addAssets();
  203. // Process whole page as required
  204. $debugger->startTimer('render', 'Render');
  205. $this->output = $this['output'];
  206. $this->fireEvent('onOutputGenerated');
  207. $debugger->stopTimer('render');
  208. // Set the header type
  209. $this->header();
  210. echo $this->output;
  211. $debugger->render();
  212. $this->fireEvent('onOutputRendered');
  213. register_shutdown_function([$this, 'shutdown']);
  214. }
  215. /**
  216. * Redirect browser to another location.
  217. *
  218. * @param string $route Internal route.
  219. * @param int $code Redirection code (30x)
  220. */
  221. public function redirect($route, $code = null)
  222. {
  223. /** @var Uri $uri */
  224. $uri = $this['uri'];
  225. //Check for code in route
  226. $regex = '/.*(\[(30[1-7])\])$/';
  227. preg_match($regex, $route, $matches);
  228. if ($matches) {
  229. $route = str_replace($matches[1], '', $matches[0]);
  230. $code = $matches[2];
  231. }
  232. if ($code == null) {
  233. $code = $this['config']->get('system.pages.redirect_default_code', 301);
  234. }
  235. if (isset($this['session'])) {
  236. $this['session']->close();
  237. }
  238. if ($uri->isExternal($route)) {
  239. $url = $route;
  240. } else {
  241. $url = rtrim($uri->rootUrl(), '/') .'/'. trim($route, '/');
  242. }
  243. header("Location: {$url}", true, $code);
  244. exit();
  245. }
  246. /**
  247. * Redirect browser to another location taking language into account (preferred)
  248. *
  249. * @param string $route Internal route.
  250. * @param int $code Redirection code (30x)
  251. */
  252. public function redirectLangSafe($route, $code = null)
  253. {
  254. /** @var Language $language */
  255. $language = $this['language'];
  256. if (!$this['uri']->isExternal($route) && $language->enabled() && $language->isIncludeDefaultLanguage()) {
  257. return $this->redirect($language->getLanguage() . $route, $code);
  258. } else {
  259. return $this->redirect($route, $code);
  260. }
  261. }
  262. /**
  263. * Returns mime type for the file format.
  264. *
  265. * @param string $format
  266. * @return string
  267. */
  268. public function mime($format)
  269. {
  270. switch ($format) {
  271. case 'json':
  272. return 'application/json';
  273. case 'html':
  274. return 'text/html';
  275. case 'atom':
  276. return 'application/atom+xml';
  277. case 'rss':
  278. return 'application/rss+xml';
  279. case 'xml':
  280. return 'application/xml';
  281. }
  282. return 'text/html';
  283. }
  284. /**
  285. * Set response header.
  286. */
  287. public function header()
  288. {
  289. $extension = $this['uri']->extension();
  290. /** @var Page $page */
  291. $page = $this['page'];
  292. header('Content-type: ' . $this->mime($extension));
  293. // Calculate Expires Headers if set to > 0
  294. $expires = $page->expires();
  295. if ($expires > 0) {
  296. $expires_date = gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT';
  297. header('Cache-Control: max-age=' . $expires);
  298. header('Expires: '. $expires_date);
  299. }
  300. // Set the last modified time
  301. if ($page->lastModified()) {
  302. $last_modified_date = gmdate('D, d M Y H:i:s', $page->modified()) . ' GMT';
  303. header('Last-Modified: ' . $last_modified_date);
  304. }
  305. // Calculate a Hash based on the raw file
  306. if ($page->eTag()) {
  307. header('ETag: ' . md5($page->raw() . $page->modified()));
  308. }
  309. // Set debugger data in headers
  310. if (!($extension === null || $extension == 'html')) {
  311. $this['debugger']->enabled(false);
  312. }
  313. // Set HTTP response code
  314. if (isset($this['page']->header()->http_response_code)) {
  315. http_response_code($this['page']->header()->http_response_code);
  316. }
  317. // Vary: Accept-Encoding
  318. if ($this['config']->get('system.pages.vary_accept_encoding', false)) {
  319. header('Vary: Accept-Encoding');
  320. }
  321. }
  322. /**
  323. * Fires an event with optional parameters.
  324. *
  325. * @param string $eventName
  326. * @param Event $event
  327. * @return Event
  328. */
  329. public function fireEvent($eventName, Event $event = null)
  330. {
  331. /** @var EventDispatcher $events */
  332. $events = $this['events'];
  333. return $events->dispatch($eventName, $event);
  334. }
  335. /**
  336. * Set the final content length for the page and flush the buffer
  337. *
  338. */
  339. public function shutdown()
  340. {
  341. if ($this['config']->get('system.debugger.shutdown.close_connection')) {
  342. //stop user abort
  343. if (function_exists('ignore_user_abort')) {
  344. @ignore_user_abort(true);
  345. }
  346. // close the session
  347. if (isset($this['session'])) {
  348. $this['session']->close();
  349. }
  350. // flush buffer if gzip buffer was started
  351. if ($this['config']->get('system.cache.gzip')) {
  352. ob_end_flush(); // gzhandler buffer
  353. }
  354. // get lengh and close the connection
  355. header('Content-Length: ' . ob_get_length());
  356. header("Connection: close");
  357. // flush the regular buffer
  358. ob_end_flush();
  359. @ob_flush();
  360. flush();
  361. // fix for fastcgi close connection issue
  362. if (function_exists('fastcgi_finish_request')) {
  363. @fastcgi_finish_request();
  364. }
  365. }
  366. $this->fireEvent('onShutdown');
  367. }
  368. /**
  369. * This attempts to find media, other files, and download them
  370. * @param $page
  371. * @param $path
  372. */
  373. protected function fallbackUrl($page, $path)
  374. {
  375. /** @var Uri $uri */
  376. $uri = $this['uri'];
  377. /** @var Config $config */
  378. $config = $this['config'];
  379. $uri_extension = $uri->extension();
  380. // Only allow whitelisted types to fallback
  381. if (!in_array($uri_extension, $config->get('system.pages.fallback_types'))) {
  382. return;
  383. }
  384. $path_parts = pathinfo($path);
  385. $page = $this['pages']->dispatch($path_parts['dirname'], true);
  386. if ($page) {
  387. $media = $page->media()->all();
  388. $parsed_url = parse_url(urldecode($uri->basename()));
  389. $media_file = $parsed_url['path'];
  390. // if this is a media object, try actions first
  391. if (isset($media[$media_file])) {
  392. $medium = $media[$media_file];
  393. foreach ($uri->query(null, true) as $action => $params) {
  394. if (in_array($action, ImageMedium::$magic_actions)) {
  395. call_user_func_array(array(&$medium, $action), explode(',', $params));
  396. }
  397. }
  398. Utils::download($medium->path(), false);
  399. }
  400. // unsupported media type, try to download it...
  401. if ($uri_extension) {
  402. $extension = $uri_extension;
  403. } else {
  404. if (isset($path_parts['extension'])) {
  405. $extension = $path_parts['extension'];
  406. } else {
  407. $extension = null;
  408. }
  409. }
  410. if ($extension) {
  411. $download = true;
  412. if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []))) {
  413. $download = false;
  414. }
  415. Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
  416. }
  417. }
  418. }
  419. }