Grav.php 26 KB

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