Excerpts.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. <?php
  2. /**
  3. * @package Grav\Common\Page
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Page\Markdown;
  9. use Grav\Common\Grav;
  10. use Grav\Common\Page\Interfaces\PageInterface;
  11. use Grav\Common\Page\Medium\Link;
  12. use Grav\Common\Uri;
  13. use Grav\Common\Page\Medium\Medium;
  14. use Grav\Common\Utils;
  15. use RocketTheme\Toolbox\Event\Event;
  16. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  17. class Excerpts
  18. {
  19. /** @var PageInterface */
  20. protected $page;
  21. /** @var array */
  22. protected $config;
  23. public function __construct(PageInterface $page = null, array $config = null)
  24. {
  25. $this->page = $page ?? Grav::instance()['page'] ?? null;
  26. // Add defaults to the configuration.
  27. if (null === $config || !isset($config['markdown'], $config['images'])) {
  28. $c = Grav::instance()['config'];
  29. $config = $config ?? [];
  30. $config += [
  31. 'markdown' => $c->get('system.pages.markdown', []),
  32. 'images' => $c->get('system.images', [])
  33. ];
  34. }
  35. $this->config = $config;
  36. }
  37. public function getPage(): PageInterface
  38. {
  39. return $this->page;
  40. }
  41. public function getConfig(): array
  42. {
  43. return $this->config;
  44. }
  45. public function fireInitializedEvent($markdown): void
  46. {
  47. $grav = Grav::instance();
  48. $grav->fireEvent('onMarkdownInitialized', new Event(['markdown' => $markdown, 'page' => $this->page]));
  49. }
  50. /**
  51. * Process a Link excerpt
  52. *
  53. * @param array $excerpt
  54. * @param string $type
  55. * @return array
  56. */
  57. public function processLinkExcerpt(array $excerpt, string $type = 'link'): array
  58. {
  59. $url = htmlspecialchars_decode(rawurldecode($excerpt['element']['attributes']['href']));
  60. $url_parts = $this->parseUrl($url);
  61. // If there is a query, then parse it and build action calls.
  62. if (isset($url_parts['query'])) {
  63. $actions = array_reduce(
  64. explode('&', $url_parts['query']),
  65. static function ($carry, $item) {
  66. $parts = explode('=', $item, 2);
  67. $value = isset($parts[1]) ? rawurldecode($parts[1]) : true;
  68. $carry[$parts[0]] = $value;
  69. return $carry;
  70. },
  71. []
  72. );
  73. // Valid attributes supported.
  74. $valid_attributes = ['rel', 'target', 'id', 'class', 'classes'];
  75. // Unless told to not process, go through actions.
  76. if (array_key_exists('noprocess', $actions)) {
  77. unset($actions['noprocess']);
  78. } else {
  79. // Loop through actions for the image and call them.
  80. foreach ($actions as $attrib => $value) {
  81. $key = $attrib;
  82. if (in_array($attrib, $valid_attributes, true)) {
  83. // support both class and classes.
  84. if ($attrib === 'classes') {
  85. $attrib = 'class';
  86. }
  87. $excerpt['element']['attributes'][$attrib] = str_replace(',', ' ', $value);
  88. unset($actions[$key]);
  89. }
  90. }
  91. }
  92. $url_parts['query'] = http_build_query($actions, null, '&', PHP_QUERY_RFC3986);
  93. }
  94. // If no query elements left, unset query.
  95. if (empty($url_parts['query'])) {
  96. unset ($url_parts['query']);
  97. }
  98. // Set path to / if not set.
  99. if (empty($url_parts['path'])) {
  100. $url_parts['path'] = '';
  101. }
  102. // If scheme isn't http(s)..
  103. if (!empty($url_parts['scheme']) && !in_array($url_parts['scheme'], ['http', 'https'])) {
  104. // Handle custom streams.
  105. if ($type !== 'image' && !empty($url_parts['stream']) && !empty($url_parts['path'])) {
  106. $grav = Grav::instance();
  107. $url_parts['path'] = $grav['base_url_relative'] . '/' . $this->resolveStream("{$url_parts['scheme']}://{$url_parts['path']}");
  108. unset($url_parts['stream'], $url_parts['scheme']);
  109. }
  110. $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
  111. return $excerpt;
  112. }
  113. // Handle paths and such.
  114. $url_parts = Uri::convertUrl($this->page, $url_parts, $type);
  115. // Build the URL from the component parts and set it on the element.
  116. $excerpt['element']['attributes']['href'] = Uri::buildUrl($url_parts);
  117. return $excerpt;
  118. }
  119. /**
  120. * Process an image excerpt
  121. *
  122. * @param array $excerpt
  123. * @return array
  124. */
  125. public function processImageExcerpt(array $excerpt): array
  126. {
  127. $url = htmlspecialchars_decode(urldecode($excerpt['element']['attributes']['src']));
  128. $url_parts = $this->parseUrl($url);
  129. $media = null;
  130. $filename = null;
  131. if (!empty($url_parts['stream'])) {
  132. $filename = $url_parts['scheme'] . '://' . ($url_parts['path'] ?? '');
  133. $media = $this->page->getMedia();
  134. } else {
  135. $grav = Grav::instance();
  136. // File is also local if scheme is http(s) and host matches.
  137. $local_file = isset($url_parts['path'])
  138. && (empty($url_parts['scheme']) || in_array($url_parts['scheme'], ['http', 'https'], true))
  139. && (empty($url_parts['host']) || $url_parts['host'] === $grav['uri']->host());
  140. if ($local_file) {
  141. $filename = basename($url_parts['path']);
  142. $folder = dirname($url_parts['path']);
  143. // Get the local path to page media if possible.
  144. if ($this->page && $folder === $this->page->url(false, false, false)) {
  145. // Get the media objects for this page.
  146. $media = $this->page->getMedia();
  147. } else {
  148. // see if this is an external page to this one
  149. $base_url = rtrim($grav['base_url_relative'] . $grav['pages']->base(), '/');
  150. $page_route = '/' . ltrim(str_replace($base_url, '', $folder), '/');
  151. /** @var PageInterface $ext_page */
  152. $ext_page = $grav['pages']->dispatch($page_route, true);
  153. if ($ext_page) {
  154. $media = $ext_page->getMedia();
  155. } else {
  156. $grav->fireEvent('onMediaLocate', new Event(['route' => $page_route, 'media' => &$media]));
  157. }
  158. }
  159. }
  160. }
  161. // If there is a media file that matches the path referenced..
  162. if ($media && $filename && isset($media[$filename])) {
  163. // Get the medium object.
  164. /** @var Medium $medium */
  165. $medium = $media[$filename];
  166. // Process operations
  167. $medium = $this->processMediaActions($medium, $url_parts);
  168. $element_excerpt = $excerpt['element']['attributes'];
  169. $alt = $element_excerpt['alt'] ?? '';
  170. $title = $element_excerpt['title'] ?? '';
  171. $class = $element_excerpt['class'] ?? '';
  172. $id = $element_excerpt['id'] ?? '';
  173. $excerpt['element'] = $medium->parsedownElement($title, $alt, $class, $id, true);
  174. } else {
  175. // Not a current page media file, see if it needs converting to relative.
  176. $excerpt['element']['attributes']['src'] = Uri::buildUrl($url_parts);
  177. }
  178. return $excerpt;
  179. }
  180. /**
  181. * Process media actions
  182. *
  183. * @param Medium $medium
  184. * @param string|array $url
  185. * @return Medium|Link
  186. */
  187. public function processMediaActions($medium, $url)
  188. {
  189. $url_parts = is_string($url) ? $this->parseUrl($url) : $url;
  190. $actions = [];
  191. // if there is a query, then parse it and build action calls
  192. if (isset($url_parts['query'])) {
  193. $actions = array_reduce(
  194. explode('&', $url_parts['query']),
  195. static function ($carry, $item) {
  196. $parts = explode('=', $item, 2);
  197. $value = $parts[1] ?? null;
  198. $carry[] = ['method' => $parts[0], 'params' => $value];
  199. return $carry;
  200. },
  201. []
  202. );
  203. }
  204. $config = $this->getConfig();
  205. if (!empty($config['images']['auto_fix_orientation'])) {
  206. $actions[] = ['method' => 'fixOrientation', 'params' => ''];
  207. }
  208. $defaults = $config['images']['defaults'] ?? [];
  209. if (count($defaults)) {
  210. foreach ($defaults as $method => $params) {
  211. $actions[] = [
  212. 'method' => $method,
  213. 'params' => $params,
  214. ];
  215. }
  216. }
  217. // loop through actions for the image and call them
  218. foreach ($actions as $action) {
  219. $matches = [];
  220. if (preg_match('/\[(.*)\]/', $action['params'], $matches)) {
  221. $args = [explode(',', $matches[1])];
  222. } else {
  223. $args = explode(',', $action['params']);
  224. }
  225. $medium = call_user_func_array([$medium, $action['method']], $args);
  226. }
  227. if (isset($url_parts['fragment'])) {
  228. $medium->urlHash($url_parts['fragment']);
  229. }
  230. return $medium;
  231. }
  232. /**
  233. * Variation of parse_url() which works also with local streams.
  234. *
  235. * @param string $url
  236. * @return array|bool
  237. */
  238. protected function parseUrl(string $url)
  239. {
  240. $url_parts = Utils::multibyteParseUrl($url);
  241. if (isset($url_parts['scheme'])) {
  242. /** @var UniformResourceLocator $locator */
  243. $locator = Grav::instance()['locator'];
  244. // Special handling for the streams.
  245. if ($locator->schemeExists($url_parts['scheme'])) {
  246. if (isset($url_parts['host'])) {
  247. // Merge host and path into a path.
  248. $url_parts['path'] = $url_parts['host'] . (isset($url_parts['path']) ? '/' . $url_parts['path'] : '');
  249. unset($url_parts['host']);
  250. }
  251. $url_parts['stream'] = true;
  252. }
  253. }
  254. return $url_parts;
  255. }
  256. /**
  257. * @param string $url
  258. * @return bool|string
  259. */
  260. protected function resolveStream(string $url)
  261. {
  262. /** @var UniformResourceLocator $locator */
  263. $locator = Grav::instance()['locator'];
  264. if ($locator->isStream($url)) {
  265. return $locator->findResource($url, false) ?: $locator->findResource($url, false, true);
  266. }
  267. return $url;
  268. }
  269. }