Grav.php 24 KB

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