Medium.php 14 KB

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