Medium.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  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\Medium;
  9. use Grav\Common\File\CompiledYamlFile;
  10. use Grav\Common\Grav;
  11. use Grav\Common\Data\Data;
  12. use Grav\Common\Data\Blueprint;
  13. use Grav\Common\Media\Interfaces\MediaObjectInterface;
  14. use Grav\Common\Utils;
  15. /**
  16. * Class Medium
  17. * @package Grav\Common\Page\Medium
  18. *
  19. * @property string $mime
  20. */
  21. class Medium extends Data implements RenderableInterface, MediaObjectInterface
  22. {
  23. use ParsedownHtmlTrait;
  24. /**
  25. * @var string
  26. */
  27. protected $mode = 'source';
  28. /**
  29. * @var Medium
  30. */
  31. protected $_thumbnail = null;
  32. /**
  33. * @var array
  34. */
  35. protected $thumbnailTypes = ['page', 'default'];
  36. protected $thumbnailType = null;
  37. /**
  38. * @var Medium[]
  39. */
  40. protected $alternatives = [];
  41. /**
  42. * @var array
  43. */
  44. protected $attributes = [];
  45. /**
  46. * @var array
  47. */
  48. protected $styleAttributes = [];
  49. /**
  50. * @var array
  51. */
  52. protected $metadata = [];
  53. /**
  54. * @var array
  55. */
  56. protected $medium_querystring = [];
  57. protected $timestamp;
  58. /**
  59. * Construct.
  60. *
  61. * @param array $items
  62. * @param Blueprint $blueprint
  63. */
  64. public function __construct($items = [], Blueprint $blueprint = null)
  65. {
  66. parent::__construct($items, $blueprint);
  67. if (Grav::instance()['config']->get('system.media.enable_media_timestamp', true)) {
  68. $this->timestamp = Grav::instance()['cache']->getKey();
  69. }
  70. $this->def('mime', 'application/octet-stream');
  71. $this->reset();
  72. }
  73. public function __clone()
  74. {
  75. // Allows future compatibility as parent::__clone() works.
  76. }
  77. /**
  78. * Create a copy of this media object
  79. *
  80. * @return Medium
  81. */
  82. public function copy()
  83. {
  84. return clone $this;
  85. }
  86. /**
  87. * Return just metadata from the Medium object
  88. *
  89. * @return Data
  90. */
  91. public function meta()
  92. {
  93. return new Data($this->items);
  94. }
  95. /**
  96. * Check if this medium exists or not
  97. *
  98. * @return bool
  99. */
  100. public function exists()
  101. {
  102. $path = $this->get('filepath');
  103. if (file_exists($path)) {
  104. return true;
  105. }
  106. return false;
  107. }
  108. /**
  109. * Get file modification time for the medium.
  110. *
  111. * @return int|null
  112. */
  113. public function modified()
  114. {
  115. $path = $this->get('filepath');
  116. if (!file_exists($path)) {
  117. return null;
  118. }
  119. return filemtime($path) ?: null;
  120. }
  121. /**
  122. * @return int
  123. */
  124. public function size()
  125. {
  126. $path = $this->get('filepath');
  127. if (!file_exists($path)) {
  128. return 0;
  129. }
  130. return filesize($path) ?: 0;
  131. }
  132. /**
  133. * Set querystring to file modification timestamp (or value provided as a parameter).
  134. *
  135. * @param string|int|null $timestamp
  136. * @return $this
  137. */
  138. public function setTimestamp($timestamp = null)
  139. {
  140. $this->timestamp = (string)($timestamp ?? $this->modified());
  141. return $this;
  142. }
  143. /**
  144. * Returns an array containing just the metadata
  145. *
  146. * @return array
  147. */
  148. public function metadata()
  149. {
  150. return $this->metadata;
  151. }
  152. /**
  153. * Add meta file for the medium.
  154. *
  155. * @param string $filepath
  156. */
  157. public function addMetaFile($filepath)
  158. {
  159. $this->metadata = (array)CompiledYamlFile::instance($filepath)->content();
  160. $this->merge($this->metadata);
  161. }
  162. /**
  163. * Add alternative Medium to this Medium.
  164. *
  165. * @param int|float $ratio
  166. * @param Medium $alternative
  167. */
  168. public function addAlternative($ratio, Medium $alternative)
  169. {
  170. if (!is_numeric($ratio) || $ratio === 0) {
  171. return;
  172. }
  173. $alternative->set('ratio', $ratio);
  174. $width = $alternative->get('width');
  175. $this->alternatives[$width] = $alternative;
  176. }
  177. /**
  178. * Return string representation of the object (html).
  179. *
  180. * @return string
  181. */
  182. public function __toString()
  183. {
  184. return $this->html();
  185. }
  186. /**
  187. * Return PATH to file.
  188. *
  189. * @param bool $reset
  190. * @return string path to file
  191. */
  192. public function path($reset = true)
  193. {
  194. if ($reset) {
  195. $this->reset();
  196. }
  197. return $this->get('filepath');
  198. }
  199. /**
  200. * Return the relative path to file
  201. *
  202. * @param bool $reset
  203. * @return mixed
  204. */
  205. public function relativePath($reset = true)
  206. {
  207. $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath'));
  208. $locator = Grav::instance()['locator'];
  209. if ($locator->isStream($output)) {
  210. $output = $locator->findResource($output, false);
  211. }
  212. if ($reset) {
  213. $this->reset();
  214. }
  215. return str_replace(GRAV_ROOT, '', $output);
  216. }
  217. /**
  218. * Return URL to file.
  219. *
  220. * @param bool $reset
  221. * @return string
  222. */
  223. public function url($reset = true)
  224. {
  225. $output = preg_replace('|^' . preg_quote(GRAV_ROOT, '|') . '|', '', $this->get('filepath'));
  226. $locator = Grav::instance()['locator'];
  227. if ($locator->isStream($output)) {
  228. $output = $locator->findResource($output, false);
  229. }
  230. if ($reset) {
  231. $this->reset();
  232. }
  233. return trim(Grav::instance()['base_url'] . '/' . $this->urlQuerystring($output), '\\');
  234. }
  235. /**
  236. * Get/set querystring for the file's url
  237. *
  238. * @param string $querystring
  239. * @param bool $withQuestionmark
  240. * @return string
  241. */
  242. public function querystring($querystring = null, $withQuestionmark = true)
  243. {
  244. if (null !== $querystring) {
  245. $this->medium_querystring[] = ltrim($querystring, '?&');
  246. foreach ($this->alternatives as $alt) {
  247. $alt->querystring($querystring, $withQuestionmark);
  248. }
  249. }
  250. if (empty($this->medium_querystring)) {
  251. return '';
  252. }
  253. // join the strings
  254. $querystring = implode('&', $this->medium_querystring);
  255. // explode all strings
  256. $query_parts = explode('&', $querystring);
  257. // Join them again now ensure the elements are unique
  258. $querystring = implode('&', array_unique($query_parts));
  259. return $withQuestionmark ? ('?' . $querystring) : $querystring;
  260. }
  261. /**
  262. * Get the URL with full querystring
  263. *
  264. * @param string $url
  265. * @return string
  266. */
  267. public function urlQuerystring($url)
  268. {
  269. $querystring = $this->querystring();
  270. if (isset($this->timestamp) && !Utils::contains($querystring, $this->timestamp)) {
  271. $querystring = empty($querystring) ? ('?' . $this->timestamp) : ($querystring . '&' . $this->timestamp);
  272. }
  273. return ltrim($url . $querystring . $this->urlHash(), '/');
  274. }
  275. /**
  276. * Get/set hash for the file's url
  277. *
  278. * @param string $hash
  279. * @param bool $withHash
  280. * @return string
  281. */
  282. public function urlHash($hash = null, $withHash = true)
  283. {
  284. if ($hash) {
  285. $this->set('urlHash', ltrim($hash, '#'));
  286. }
  287. $hash = $this->get('urlHash', '');
  288. return $withHash && !empty($hash) ? '#' . $hash : $hash;
  289. }
  290. /**
  291. * Get an element (is array) that can be rendered by the Parsedown engine
  292. *
  293. * @param string $title
  294. * @param string $alt
  295. * @param string $class
  296. * @param string $id
  297. * @param bool $reset
  298. * @return array
  299. */
  300. public function parsedownElement($title = null, $alt = null, $class = null, $id = null, $reset = true)
  301. {
  302. $attributes = $this->attributes;
  303. $style = '';
  304. foreach ($this->styleAttributes as $key => $value) {
  305. if (is_numeric($key)) // Special case for inline style attributes, refer to style() method
  306. $style .= $value;
  307. else
  308. $style .= $key . ': ' . $value . ';';
  309. }
  310. if ($style) {
  311. $attributes['style'] = $style;
  312. }
  313. if (empty($attributes['title'])) {
  314. if (!empty($title)) {
  315. $attributes['title'] = $title;
  316. } elseif (!empty($this->items['title'])) {
  317. $attributes['title'] = $this->items['title'];
  318. }
  319. }
  320. if (empty($attributes['alt'])) {
  321. if (!empty($alt)) {
  322. $attributes['alt'] = $alt;
  323. } elseif (!empty($this->items['alt'])) {
  324. $attributes['alt'] = $this->items['alt'];
  325. } elseif (!empty($this->items['alt_text'])) {
  326. $attributes['alt'] = $this->items['alt_text'];
  327. } else {
  328. $attributes['alt'] = '';
  329. }
  330. }
  331. if (empty($attributes['class'])) {
  332. if (!empty($class)) {
  333. $attributes['class'] = $class;
  334. } elseif (!empty($this->items['class'])) {
  335. $attributes['class'] = $this->items['class'];
  336. }
  337. }
  338. if (empty($attributes['id'])) {
  339. if (!empty($id)) {
  340. $attributes['id'] = $id;
  341. } elseif (!empty($this->items['id'])) {
  342. $attributes['id'] = $this->items['id'];
  343. }
  344. }
  345. switch ($this->mode) {
  346. case 'text':
  347. $element = $this->textParsedownElement($attributes, false);
  348. break;
  349. case 'thumbnail':
  350. $element = $this->getThumbnail()->sourceParsedownElement($attributes, false);
  351. break;
  352. case 'source':
  353. $element = $this->sourceParsedownElement($attributes, false);
  354. break;
  355. default:
  356. $element = [];
  357. }
  358. if ($reset) {
  359. $this->reset();
  360. }
  361. $this->display('source');
  362. return $element;
  363. }
  364. /**
  365. * Parsedown element for source display mode
  366. *
  367. * @param array $attributes
  368. * @param bool $reset
  369. * @return array
  370. */
  371. protected function sourceParsedownElement(array $attributes, $reset = true)
  372. {
  373. return $this->textParsedownElement($attributes, $reset);
  374. }
  375. /**
  376. * Parsedown element for text display mode
  377. *
  378. * @param array $attributes
  379. * @param bool $reset
  380. * @return array
  381. */
  382. protected function textParsedownElement(array $attributes, $reset = true)
  383. {
  384. $text = empty($attributes['title']) ? empty($attributes['alt']) ? $this->get('filename') : $attributes['alt'] : $attributes['title'];
  385. $element = [
  386. 'name' => 'p',
  387. 'attributes' => $attributes,
  388. 'text' => $text
  389. ];
  390. if ($reset) {
  391. $this->reset();
  392. }
  393. return $element;
  394. }
  395. /**
  396. * Reset medium.
  397. *
  398. * @return $this
  399. */
  400. public function reset()
  401. {
  402. $this->attributes = [];
  403. return $this;
  404. }
  405. /**
  406. * Switch display mode.
  407. *
  408. * @param string $mode
  409. *
  410. * @return $this
  411. */
  412. public function display($mode = 'source')
  413. {
  414. if ($this->mode === $mode) {
  415. return $this;
  416. }
  417. $this->mode = $mode;
  418. return $mode === 'thumbnail' ? ($this->getThumbnail() ? $this->getThumbnail()->reset() : null) : $this->reset();
  419. }
  420. /**
  421. * Helper method to determine if this media item has a thumbnail or not
  422. *
  423. * @param string $type;
  424. *
  425. * @return bool
  426. */
  427. public function thumbnailExists($type = 'page')
  428. {
  429. $thumbs = $this->get('thumbnails');
  430. if (isset($thumbs[$type])) {
  431. return true;
  432. }
  433. return false;
  434. }
  435. /**
  436. * Switch thumbnail.
  437. *
  438. * @param string $type
  439. *
  440. * @return $this
  441. */
  442. public function thumbnail($type = 'auto')
  443. {
  444. if ($type !== 'auto' && !\in_array($type, $this->thumbnailTypes, true)) {
  445. return $this;
  446. }
  447. if ($this->thumbnailType !== $type) {
  448. $this->_thumbnail = null;
  449. }
  450. $this->thumbnailType = $type;
  451. return $this;
  452. }
  453. /**
  454. * Turn the current Medium into a Link
  455. *
  456. * @param bool $reset
  457. * @param array $attributes
  458. * @return Link
  459. */
  460. public function link($reset = true, array $attributes = [])
  461. {
  462. if ($this->mode !== 'source') {
  463. $this->display('source');
  464. }
  465. foreach ($this->attributes as $key => $value) {
  466. empty($attributes['data-' . $key]) && $attributes['data-' . $key] = $value;
  467. }
  468. empty($attributes['href']) && $attributes['href'] = $this->url();
  469. return new Link($attributes, $this);
  470. }
  471. /**
  472. * Turn the current Medium into a Link with lightbox enabled
  473. *
  474. * @param int $width
  475. * @param int $height
  476. * @param bool $reset
  477. * @return Link
  478. */
  479. public function lightbox($width = null, $height = null, $reset = true)
  480. {
  481. $attributes = ['rel' => 'lightbox'];
  482. if ($width && $height) {
  483. $attributes['data-width'] = $width;
  484. $attributes['data-height'] = $height;
  485. }
  486. return $this->link($reset, $attributes);
  487. }
  488. /**
  489. * Add a class to the element from Markdown or Twig
  490. * Example: ![Example](myimg.png?classes=float-left) or ![Example](myimg.png?classes=myclass1,myclass2)
  491. *
  492. * @return $this
  493. */
  494. public function classes()
  495. {
  496. $classes = func_get_args();
  497. if (!empty($classes)) {
  498. $this->attributes['class'] = implode(',', $classes);
  499. }
  500. return $this;
  501. }
  502. /**
  503. * Add an id to the element from Markdown or Twig
  504. * Example: ![Example](myimg.png?id=primary-img)
  505. *
  506. * @param string $id
  507. * @return $this
  508. */
  509. public function id($id)
  510. {
  511. if (is_string($id)) {
  512. $this->attributes['id'] = trim($id);
  513. }
  514. return $this;
  515. }
  516. /**
  517. * Allows to add an inline style attribute from Markdown or Twig
  518. * Example: ![Example](myimg.png?style=float:left)
  519. *
  520. * @param string $style
  521. * @return $this
  522. */
  523. public function style($style)
  524. {
  525. $this->styleAttributes[] = rtrim($style, ';') . ';';
  526. return $this;
  527. }
  528. /**
  529. * Allow any action to be called on this medium from twig or markdown
  530. *
  531. * @param string $method
  532. * @param mixed $args
  533. * @return $this
  534. */
  535. public function __call($method, $args)
  536. {
  537. $qs = $method;
  538. if (\count($args) > 1 || (\count($args) === 1 && !empty($args[0]))) {
  539. $qs .= '=' . implode(',', array_map(function ($a) {
  540. if (is_array($a)) {
  541. $a = '[' . implode(',', $a) . ']';
  542. }
  543. return rawurlencode($a);
  544. }, $args));
  545. }
  546. if (!empty($qs)) {
  547. $this->querystring($this->querystring(null, false) . '&' . $qs);
  548. }
  549. return $this;
  550. }
  551. /**
  552. * Get the thumbnail Medium object
  553. *
  554. * @return ThumbnailImageMedium
  555. */
  556. protected function getThumbnail()
  557. {
  558. if (!$this->_thumbnail) {
  559. $types = $this->thumbnailTypes;
  560. if ($this->thumbnailType !== 'auto') {
  561. array_unshift($types, $this->thumbnailType);
  562. }
  563. foreach ($types as $type) {
  564. $thumb = $this->get('thumbnails.' . $type, false);
  565. if ($thumb) {
  566. $thumb = $thumb instanceof ThumbnailImageMedium ? $thumb : MediumFactory::fromFile($thumb, ['type' => 'thumbnail']);
  567. $thumb->parent = $this;
  568. }
  569. if ($thumb) {
  570. $this->_thumbnail = $thumb;
  571. break;
  572. }
  573. }
  574. }
  575. return $this->_thumbnail;
  576. }
  577. }