Grav.php 25 KB

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