Grav.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. <?php
  2. /**
  3. * @package Grav\Common
  4. *
  5. * @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common;
  9. use Composer\Autoload\ClassLoader;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Config\Setup;
  12. use Grav\Common\Helpers\Exif;
  13. use Grav\Common\Page\Interfaces\PageInterface;
  14. use Grav\Common\Page\Medium\ImageMedium;
  15. use Grav\Common\Page\Medium\Medium;
  16. use Grav\Common\Page\Pages;
  17. use Grav\Common\Processors\AssetsProcessor;
  18. use Grav\Common\Processors\BackupsProcessor;
  19. use Grav\Common\Processors\DebuggerAssetsProcessor;
  20. use Grav\Common\Processors\InitializeProcessor;
  21. use Grav\Common\Processors\PagesProcessor;
  22. use Grav\Common\Processors\PluginsProcessor;
  23. use Grav\Common\Processors\RenderProcessor;
  24. use Grav\Common\Processors\RequestProcessor;
  25. use Grav\Common\Processors\SchedulerProcessor;
  26. use Grav\Common\Processors\TasksProcessor;
  27. use Grav\Common\Processors\ThemesProcessor;
  28. use Grav\Common\Processors\TwigProcessor;
  29. use Grav\Common\Scheduler\Scheduler;
  30. use Grav\Common\Service\AccountsServiceProvider;
  31. use Grav\Common\Service\AssetsServiceProvider;
  32. use Grav\Common\Service\BackupsServiceProvider;
  33. use Grav\Common\Service\ConfigServiceProvider;
  34. use Grav\Common\Service\ErrorServiceProvider;
  35. use Grav\Common\Service\FilesystemServiceProvider;
  36. use Grav\Common\Service\FlexServiceProvider;
  37. use Grav\Common\Service\InflectorServiceProvider;
  38. use Grav\Common\Service\LoggerServiceProvider;
  39. use Grav\Common\Service\OutputServiceProvider;
  40. use Grav\Common\Service\PagesServiceProvider;
  41. use Grav\Common\Service\RequestServiceProvider;
  42. use Grav\Common\Service\SessionServiceProvider;
  43. use Grav\Common\Service\StreamsServiceProvider;
  44. use Grav\Common\Service\TaskServiceProvider;
  45. use Grav\Common\Twig\Twig;
  46. use Grav\Framework\DI\Container;
  47. use Grav\Framework\Psr7\Response;
  48. use Grav\Framework\RequestHandler\RequestHandler;
  49. use Grav\Framework\Session\Messages;
  50. use Psr\Http\Message\ResponseInterface;
  51. use Psr\Http\Message\ServerRequestInterface;
  52. use RocketTheme\Toolbox\Event\Event;
  53. use Symfony\Component\EventDispatcher\EventDispatcher;
  54. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  55. use function array_key_exists;
  56. use function call_user_func_array;
  57. use function function_exists;
  58. use function get_class;
  59. use function in_array;
  60. use function is_callable;
  61. use function is_int;
  62. use function strlen;
  63. /**
  64. * Grav container is the heart of Grav.
  65. *
  66. * @package Grav\Common
  67. */
  68. class Grav extends Container
  69. {
  70. /** @var string Processed output for the page. */
  71. public $output;
  72. /** @var static The singleton instance */
  73. protected static $instance;
  74. /**
  75. * @var array Contains all Services and ServicesProviders that are mapped
  76. * to the dependency injection container.
  77. */
  78. protected static $diMap = [
  79. AccountsServiceProvider::class,
  80. AssetsServiceProvider::class,
  81. BackupsServiceProvider::class,
  82. ConfigServiceProvider::class,
  83. ErrorServiceProvider::class,
  84. FilesystemServiceProvider::class,
  85. FlexServiceProvider::class,
  86. InflectorServiceProvider::class,
  87. LoggerServiceProvider::class,
  88. OutputServiceProvider::class,
  89. PagesServiceProvider::class,
  90. RequestServiceProvider::class,
  91. SessionServiceProvider::class,
  92. StreamsServiceProvider::class,
  93. TaskServiceProvider::class,
  94. 'browser' => Browser::class,
  95. 'cache' => Cache::class,
  96. 'events' => EventDispatcher::class,
  97. 'exif' => Exif::class,
  98. 'plugins' => Plugins::class,
  99. 'scheduler' => Scheduler::class,
  100. 'taxonomy' => Taxonomy::class,
  101. 'themes' => Themes::class,
  102. 'twig' => Twig::class,
  103. 'uri' => Uri::class,
  104. ];
  105. /**
  106. * @var array All middleware processors that are processed in $this->process()
  107. */
  108. protected $middleware = [
  109. 'initializeProcessor',
  110. 'pluginsProcessor',
  111. 'themesProcessor',
  112. 'requestProcessor',
  113. 'tasksProcessor',
  114. 'backupsProcessor',
  115. 'schedulerProcessor',
  116. 'assetsProcessor',
  117. 'twigProcessor',
  118. 'pagesProcessor',
  119. 'debuggerAssetsProcessor',
  120. 'renderProcessor',
  121. ];
  122. /** @var array */
  123. protected $initialized = [];
  124. /**
  125. * Reset the Grav instance.
  126. *
  127. * @return void
  128. */
  129. public static function resetInstance(): void
  130. {
  131. if (self::$instance) {
  132. // @phpstan-ignore-next-line
  133. self::$instance = null;
  134. }
  135. }
  136. /**
  137. * Return the Grav instance. Create it if it's not already instanced
  138. *
  139. * @param array $values
  140. * @return Grav
  141. */
  142. public static function instance(array $values = [])
  143. {
  144. if (null === self::$instance) {
  145. self::$instance = static::load($values);
  146. /** @var ClassLoader|null $loader */
  147. $loader = self::$instance['loader'] ?? null;
  148. if ($loader) {
  149. // Load fix for Deferred Twig Extension
  150. $loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
  151. }
  152. } elseif ($values) {
  153. $instance = self::$instance;
  154. foreach ($values as $key => $value) {
  155. $instance->offsetSet($key, $value);
  156. }
  157. }
  158. return self::$instance;
  159. }
  160. /**
  161. * Get Grav version.
  162. *
  163. * @return string
  164. */
  165. public function getVersion(): string
  166. {
  167. return GRAV_VERSION;
  168. }
  169. /**
  170. * @return bool
  171. */
  172. public function isSetup(): bool
  173. {
  174. return isset($this->initialized['setup']);
  175. }
  176. /**
  177. * Setup Grav instance using specific environment.
  178. *
  179. * @param string|null $environment
  180. * @return $this
  181. */
  182. public function setup(string $environment = null)
  183. {
  184. if (isset($this->initialized['setup'])) {
  185. return $this;
  186. }
  187. $this->initialized['setup'] = true;
  188. // Force environment if passed to the method.
  189. if ($environment) {
  190. Setup::$environment = $environment;
  191. }
  192. // Initialize setup and streams.
  193. $this['setup'];
  194. $this['streams'];
  195. return $this;
  196. }
  197. /**
  198. * Initialize CLI environment.
  199. *
  200. * Call after `$grav->setup($environment)`
  201. *
  202. * - Load configuration
  203. * - Initialize logger
  204. * - Disable debugger
  205. * - Set timezone, locale
  206. * - Load plugins (call PluginsLoadedEvent)
  207. * - Set Pages and Users type to be used in the site
  208. *
  209. * This method WILL NOT initialize assets, twig or pages.
  210. *
  211. * @return $this
  212. */
  213. public function initializeCli()
  214. {
  215. InitializeProcessor::initializeCli($this);
  216. return $this;
  217. }
  218. /**
  219. * Process a request
  220. *
  221. * @return void
  222. */
  223. public function process(): void
  224. {
  225. if (isset($this->initialized['process'])) {
  226. return;
  227. }
  228. // Initialize Grav if needed.
  229. $this->setup();
  230. $this->initialized['process'] = true;
  231. $container = new Container(
  232. [
  233. 'initializeProcessor' => function () {
  234. return new InitializeProcessor($this);
  235. },
  236. 'backupsProcessor' => function () {
  237. return new BackupsProcessor($this);
  238. },
  239. 'pluginsProcessor' => function () {
  240. return new PluginsProcessor($this);
  241. },
  242. 'themesProcessor' => function () {
  243. return new ThemesProcessor($this);
  244. },
  245. 'schedulerProcessor' => function () {
  246. return new SchedulerProcessor($this);
  247. },
  248. 'requestProcessor' => function () {
  249. return new RequestProcessor($this);
  250. },
  251. 'tasksProcessor' => function () {
  252. return new TasksProcessor($this);
  253. },
  254. 'assetsProcessor' => function () {
  255. return new AssetsProcessor($this);
  256. },
  257. 'twigProcessor' => function () {
  258. return new TwigProcessor($this);
  259. },
  260. 'pagesProcessor' => function () {
  261. return new PagesProcessor($this);
  262. },
  263. 'debuggerAssetsProcessor' => function () {
  264. return new DebuggerAssetsProcessor($this);
  265. },
  266. 'renderProcessor' => function () {
  267. return new RenderProcessor($this);
  268. },
  269. ]
  270. );
  271. $default = static function () {
  272. return new Response(404, ['Expires' => 0, 'Cache-Control' => 'no-store, max-age=0'], 'Not Found');
  273. };
  274. $collection = new RequestHandler($this->middleware, $default, $container);
  275. $response = $collection->handle($this['request']);
  276. $body = $response->getBody();
  277. /** @var Messages $messages */
  278. $messages = $this['messages'];
  279. // Prevent caching if session messages were displayed in the page.
  280. $noCache = $messages->isCleared();
  281. if ($noCache) {
  282. $response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
  283. }
  284. // Handle ETag and If-None-Match headers.
  285. if ($response->getHeaderLine('ETag') === '1') {
  286. $etag = md5($body);
  287. $response = $response->withHeader('ETag', '"' . $etag . '"');
  288. $search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
  289. if ($noCache === false && $search === $etag) {
  290. $response = $response->withStatus(304);
  291. $body = '';
  292. }
  293. }
  294. // Echo page content.
  295. $this->header($response);
  296. echo $body;
  297. $this['debugger']->render();
  298. // Response object can turn off all shutdown processing. This can be used for example to speed up AJAX responses.
  299. // Note that using this feature will also turn off response compression.
  300. if ($response->getHeaderLine('Grav-Internal-SkipShutdown') !== '1') {
  301. register_shutdown_function([$this, 'shutdown']);
  302. }
  303. }
  304. /**
  305. * Terminates Grav request with a response.
  306. *
  307. * Please use this method instead of calling `die();` or `exit();`. Note that you need to create a response object.
  308. *
  309. * @param ResponseInterface $response
  310. * @return void
  311. */
  312. public function close(ResponseInterface $response): void
  313. {
  314. // Make sure nothing extra gets written to the response.
  315. while (ob_get_level()) {
  316. ob_end_clean();
  317. }
  318. // Close the session.
  319. if (isset($this['session'])) {
  320. $this['session']->close();
  321. }
  322. /** @var ServerRequestInterface $request */
  323. $request = $this['request'];
  324. /** @var Debugger $debugger */
  325. $debugger = $this['debugger'];
  326. $response = $debugger->logRequest($request, $response);
  327. $body = $response->getBody();
  328. /** @var Messages $messages */
  329. $messages = $this['messages'];
  330. // Prevent caching if session messages were displayed in the page.
  331. $noCache = $messages->isCleared();
  332. if ($noCache) {
  333. $response = $response->withHeader('Cache-Control', 'no-store, max-age=0');
  334. }
  335. // Handle ETag and If-None-Match headers.
  336. if ($response->getHeaderLine('ETag') === '1') {
  337. $etag = md5($body);
  338. $response = $response->withHeader('ETag', '"' . $etag . '"');
  339. $search = trim($this['request']->getHeaderLine('If-None-Match'), '"');
  340. if ($noCache === false && $search === $etag) {
  341. $response = $response->withStatus(304);
  342. $body = '';
  343. }
  344. }
  345. // Echo page content.
  346. $this->header($response);
  347. echo $body;
  348. exit();
  349. }
  350. /**
  351. * @param ResponseInterface $response
  352. * @return void
  353. * @deprecated 1.7 Do not use
  354. */
  355. public function exit(ResponseInterface $response): void
  356. {
  357. $this->close($response);
  358. }
  359. /**
  360. * Terminates Grav request and redirects browser to another location.
  361. *
  362. * Please use this method instead of calling `header("Location: {$url}", true, 302); exit();`.
  363. *
  364. * @param string $route Internal route.
  365. * @param int|null $code Redirection code (30x)
  366. * @return void
  367. */
  368. public function redirect($route, $code = null): void
  369. {
  370. $response = $this->getRedirectResponse($route, $code);
  371. $this->close($response);
  372. }
  373. /**
  374. * Returns redirect response object from Grav.
  375. *
  376. * @param string $route Internal route.
  377. * @param int|null $code Redirection code (30x)
  378. * @return ResponseInterface
  379. */
  380. public function getRedirectResponse($route, $code = null): ResponseInterface
  381. {
  382. /** @var Uri $uri */
  383. $uri = $this['uri'];
  384. // Clean route for redirect
  385. $route = preg_replace("#^\/[\\\/]+\/#", '/', $route);
  386. if ($code < 300 || $code > 399) {
  387. $code = null;
  388. }
  389. if (null === $code) {
  390. // Check for redirect code in the route: e.g. /new/[301], /new[301]/route or /new[301].html
  391. $regex = '/.*(\[(30[1-7])\])(.\w+|\/.*?)?$/';
  392. preg_match($regex, $route, $matches);
  393. if ($matches) {
  394. $route = str_replace($matches[1], '', $matches[0]);
  395. $code = $matches[2];
  396. }
  397. }
  398. if ($code === null) {
  399. $code = $this['config']->get('system.pages.redirect_default_code', 302);
  400. }
  401. if ($uri::isExternal($route)) {
  402. $url = $route;
  403. } else {
  404. $url = rtrim($uri->rootUrl(), '/') . '/';
  405. if ($this['config']->get('system.pages.redirect_trailing_slash', true)) {
  406. $url .= trim($route, '/'); // Remove trailing slash
  407. } else {
  408. $url .= ltrim($route, '/'); // Support trailing slash default routes
  409. }
  410. }
  411. if ($uri->extension() === 'json') {
  412. return new Response(200, ['Content-Type' => 'application/json'], json_encode(['code' => $code, 'redirect' => $url], JSON_THROW_ON_ERROR));
  413. }
  414. return new Response($code, ['Location' => $url]);
  415. }
  416. /**
  417. * Redirect browser to another location taking language into account (preferred)
  418. *
  419. * @param string $route Internal route.
  420. * @param int $code Redirection code (30x)
  421. * @return void
  422. */
  423. public function redirectLangSafe($route, $code = null): void
  424. {
  425. if (!$this['uri']->isExternal($route)) {
  426. $this->redirect($this['pages']->route($route), $code);
  427. } else {
  428. $this->redirect($route, $code);
  429. }
  430. }
  431. /**
  432. * Set response header.
  433. *
  434. * @param ResponseInterface|null $response
  435. * @return void
  436. */
  437. public function header(ResponseInterface $response = null): void
  438. {
  439. if (null === $response) {
  440. /** @var PageInterface $page */
  441. $page = $this['page'];
  442. $response = new Response($page->httpResponseCode(), $page->httpHeaders(), '');
  443. }
  444. header("HTTP/{$response->getProtocolVersion()} {$response->getStatusCode()} {$response->getReasonPhrase()}");
  445. foreach ($response->getHeaders() as $key => $values) {
  446. // Skip internal Grav headers.
  447. if (strpos($key, 'Grav-Internal-') === 0) {
  448. continue;
  449. }
  450. foreach ($values as $i => $value) {
  451. header($key . ': ' . $value, $i === 0);
  452. }
  453. }
  454. }
  455. /**
  456. * Set the system locale based on the language and configuration
  457. *
  458. * @return void
  459. */
  460. public function setLocale(): void
  461. {
  462. // Initialize Locale if set and configured.
  463. if ($this['language']->enabled() && $this['config']->get('system.languages.override_locale')) {
  464. $language = $this['language']->getLanguage();
  465. setlocale(LC_ALL, strlen($language) < 3 ? ($language . '_' . strtoupper($language)) : $language);
  466. } elseif ($this['config']->get('system.default_locale')) {
  467. setlocale(LC_ALL, $this['config']->get('system.default_locale'));
  468. }
  469. }
  470. /**
  471. * @param object $event
  472. * @return object
  473. */
  474. public function dispatchEvent($event)
  475. {
  476. /** @var EventDispatcherInterface $events */
  477. $events = $this['events'];
  478. $eventName = get_class($event);
  479. $timestamp = microtime(true);
  480. $event = $events->dispatch($event);
  481. /** @var Debugger $debugger */
  482. $debugger = $this['debugger'];
  483. $debugger->addEvent($eventName, $event, $events, $timestamp);
  484. return $event;
  485. }
  486. /**
  487. * Fires an event with optional parameters.
  488. *
  489. * @param string $eventName
  490. * @param Event|null $event
  491. * @return Event
  492. */
  493. public function fireEvent($eventName, Event $event = null)
  494. {
  495. /** @var EventDispatcherInterface $events */
  496. $events = $this['events'];
  497. if (null === $event) {
  498. $event = new Event();
  499. }
  500. $timestamp = microtime(true);
  501. $events->dispatch($event, $eventName);
  502. /** @var Debugger $debugger */
  503. $debugger = $this['debugger'];
  504. $debugger->addEvent($eventName, $event, $events, $timestamp);
  505. return $event;
  506. }
  507. /**
  508. * Set the final content length for the page and flush the buffer
  509. *
  510. * @return void
  511. */
  512. public function shutdown(): void
  513. {
  514. // Prevent user abort allowing onShutdown event to run without interruptions.
  515. if (function_exists('ignore_user_abort')) {
  516. @ignore_user_abort(true);
  517. }
  518. // Close the session allowing new requests to be handled.
  519. if (isset($this['session'])) {
  520. $this['session']->close();
  521. }
  522. /** @var Config $config */
  523. $config = $this['config'];
  524. if ($config->get('system.debugger.shutdown.close_connection', true)) {
  525. // Flush the response and close the connection to allow time consuming tasks to be performed without leaving
  526. // the connection to the client open. This will make page loads to feel much faster.
  527. // FastCGI allows us to flush all response data to the client and finish the request.
  528. $success = function_exists('fastcgi_finish_request') ? @fastcgi_finish_request() : false;
  529. if (!$success) {
  530. // Unfortunately without FastCGI there is no way to force close the connection.
  531. // We need to ask browser to close the connection for us.
  532. if ($config->get('system.cache.gzip')) {
  533. // Flush gzhandler buffer if gzip setting was enabled to get the size of the compressed output.
  534. ob_end_flush();
  535. } elseif ($config->get('system.cache.allow_webserver_gzip')) {
  536. // Let web server to do the hard work.
  537. header('Content-Encoding: identity');
  538. } elseif (function_exists('apache_setenv')) {
  539. // Without gzip we have no other choice than to prevent server from compressing the output.
  540. // This action turns off mod_deflate which would prevent us from closing the connection.
  541. @apache_setenv('no-gzip', '1');
  542. } else {
  543. // Fall back to unknown content encoding, it prevents most servers from deflating the content.
  544. header('Content-Encoding: none');
  545. }
  546. // Get length and close the connection.
  547. header('Content-Length: ' . ob_get_length());
  548. header('Connection: close');
  549. ob_end_flush();
  550. @ob_flush();
  551. flush();
  552. }
  553. }
  554. // Run any time consuming tasks.
  555. $this->fireEvent('onShutdown');
  556. }
  557. /**
  558. * Magic Catch All Function
  559. *
  560. * Used to call closures.
  561. *
  562. * Source: http://stackoverflow.com/questions/419804/closures-as-class-members
  563. *
  564. * @param string $method
  565. * @param array $args
  566. * @return mixed|null
  567. */
  568. public function __call($method, $args)
  569. {
  570. $closure = $this->{$method} ?? null;
  571. return is_callable($closure) ? $closure(...$args) : null;
  572. }
  573. /**
  574. * Measure how long it takes to do an action.
  575. *
  576. * @param string $timerId
  577. * @param string $timerTitle
  578. * @param callable $callback
  579. * @return mixed Returns value returned by the callable.
  580. */
  581. public function measureTime(string $timerId, string $timerTitle, callable $callback)
  582. {
  583. $debugger = $this['debugger'];
  584. $debugger->startTimer($timerId, $timerTitle);
  585. $result = $callback();
  586. $debugger->stopTimer($timerId);
  587. return $result;
  588. }
  589. /**
  590. * Initialize and return a Grav instance
  591. *
  592. * @param array $values
  593. * @return static
  594. */
  595. protected static function load(array $values)
  596. {
  597. $container = new static($values);
  598. $container['debugger'] = new Debugger();
  599. $container['grav'] = function (Container $container) {
  600. user_error('Calling $grav[\'grav\'] or {{ grav.grav }} is deprecated since Grav 1.6, just use $grav or {{ grav }}', E_USER_DEPRECATED);
  601. return $container;
  602. };
  603. $container->registerServices();
  604. return $container;
  605. }
  606. /**
  607. * Register all services
  608. * Services are defined in the diMap. They can either only the class
  609. * of a Service Provider or a pair of serviceKey => serviceClass that
  610. * gets directly mapped into the container.
  611. *
  612. * @return void
  613. */
  614. protected function registerServices(): void
  615. {
  616. foreach (self::$diMap as $serviceKey => $serviceClass) {
  617. if (is_int($serviceKey)) {
  618. $this->register(new $serviceClass);
  619. } else {
  620. $this[$serviceKey] = function ($c) use ($serviceClass) {
  621. return new $serviceClass($c);
  622. };
  623. }
  624. }
  625. }
  626. /**
  627. * This attempts to find media, other files, and download them
  628. *
  629. * @param string $path
  630. * @return PageInterface|false
  631. */
  632. public function fallbackUrl($path)
  633. {
  634. $this->fireEvent('onPageFallBackUrl');
  635. /** @var Uri $uri */
  636. $uri = $this['uri'];
  637. /** @var Config $config */
  638. $config = $this['config'];
  639. $uri_extension = strtolower($uri->extension());
  640. $fallback_types = $config->get('system.media.allowed_fallback_types', null);
  641. $supported_types = $config->get('media.types');
  642. // Check whitelist first, then ensure extension is a valid media type
  643. if (!empty($fallback_types) && !in_array($uri_extension, $fallback_types, true)) {
  644. return false;
  645. }
  646. if (!array_key_exists($uri_extension, $supported_types)) {
  647. return false;
  648. }
  649. $path_parts = pathinfo($path);
  650. /** @var Pages $pages */
  651. $pages = $this['pages'];
  652. $page = $pages->find($path_parts['dirname'], true);
  653. if ($page) {
  654. $media = $page->media()->all();
  655. $parsed_url = parse_url(rawurldecode($uri->basename()));
  656. $media_file = $parsed_url['path'];
  657. // if this is a media object, try actions first
  658. if (isset($media[$media_file])) {
  659. /** @var Medium $medium */
  660. $medium = $media[$media_file];
  661. foreach ($uri->query(null, true) as $action => $params) {
  662. if (in_array($action, ImageMedium::$magic_actions, true)) {
  663. call_user_func_array([&$medium, $action], explode(',', $params));
  664. }
  665. }
  666. Utils::download($medium->path(), false);
  667. }
  668. // unsupported media type, try to download it...
  669. if ($uri_extension) {
  670. $extension = $uri_extension;
  671. } elseif (isset($path_parts['extension'])) {
  672. $extension = $path_parts['extension'];
  673. } else {
  674. $extension = null;
  675. }
  676. if ($extension) {
  677. $download = true;
  678. if (in_array(ltrim($extension, '.'), $config->get('system.media.unsupported_inline_types', []), true)) {
  679. $download = false;
  680. }
  681. Utils::download($page->path() . DIRECTORY_SEPARATOR . $uri->basename(), $download);
  682. }
  683. }
  684. // Nothing found
  685. return false;
  686. }
  687. }