MediaEmbed.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. <?php
  2. /**
  3. * MediaEmbed
  4. *
  5. * This file is part of Grav MediaEmbed plugin.
  6. *
  7. * Dual licensed under the MIT or GPL Version 3 licenses, see LICENSE.
  8. * http://benjamin-regler.de/license/
  9. */
  10. namespace Grav\Plugin\MediaEmbed;
  11. use Grav\Common\Grav;
  12. use Grav\Common\GravTrait;
  13. use Grav\Plugin\MediaEmbed\Service;
  14. use RocketTheme\Toolbox\Event\Event;
  15. /**
  16. * MediaEmbed
  17. *
  18. * Helper class to embed several media sites (e.g. YouTube, Vimeo,
  19. * Soundcloud) by only providing the URL to the medium.
  20. */
  21. class MediaEmbed
  22. {
  23. /**
  24. * @var MediaEmbed
  25. */
  26. use GravTrait;
  27. /** ---------------------------
  28. * Private/protected properties
  29. * ----------------------------
  30. */
  31. /**
  32. * A unique identifier
  33. *
  34. * @var string
  35. */
  36. protected $id;
  37. /**
  38. * A key-valued array used for hashing math formulas of a page
  39. *
  40. * @var array
  41. */
  42. protected $hashes;
  43. /**
  44. * @var array
  45. */
  46. protected $config;
  47. /**
  48. * @var array
  49. */
  50. protected $assets = [];
  51. /**
  52. * @var Grav\Plugin\MediaEmbed\Service
  53. */
  54. protected $service;
  55. /** -------------
  56. * Public methods
  57. * --------------
  58. */
  59. /**
  60. * Constructor
  61. *
  62. * @param [type] $config [description]
  63. */
  64. public function __construct($config)
  65. {
  66. // Initialize Service class
  67. $this->service = new Service();
  68. $this->config = $config;
  69. $this->hashes = [];
  70. $services = $this->config->get('plugins.mediaembed.services', []);
  71. foreach ($services as $name => $config) {
  72. if (!$config['enabled']) {
  73. continue;
  74. }
  75. // Load providers in directory "services"
  76. $class = __NAMESPACE__ . "\\Services\\$name";
  77. if (!class_exists($class)) {
  78. // Fallback to a more generic one
  79. $type = isset($config['type']) ? $config['type'] : '';
  80. $class = __NAMESPACE__."\\OEmbed\\OEmbed".ucfirst($type);
  81. }
  82. // Populate config
  83. $config['media'] = $this->config->get('plugins.mediaembed.media', []);
  84. $config['name'] = $name;
  85. if (class_exists($class)) {
  86. // Load ServiceProvider
  87. $provider = new $class($config);
  88. // Register ServiceProvider
  89. $this->service->register($provider);
  90. }
  91. }
  92. }
  93. /**
  94. * Gets and sets the identifier for hashing.
  95. *
  96. * @param string $var the identifier
  97. *
  98. * @return string the identifier
  99. */
  100. public function id($var = null)
  101. {
  102. if ($var !== null) {
  103. $this->id = $var;
  104. }
  105. return $this->id;
  106. }
  107. public function prepare($content, $id = '')
  108. {
  109. // Set unique identifier based on page content
  110. $this->id(md5(time() . $id . md5($content)));
  111. // Reset class hashes before processing
  112. $this->reset();
  113. $regex = "~
  114. ( # wrap whole match in $1
  115. !\\[
  116. (?P<alt>.*?) # alt text = $2
  117. \\]
  118. \\( # literal paren
  119. [ \\t]*
  120. <?(?P<src>\S+?)>? # src url = $3
  121. [ \\t]*
  122. ( # $4
  123. (['\"]) # quote char = $5
  124. (?<title>.*?) # title = $6
  125. \\5 # matching quote
  126. [ \\t]*
  127. )? # title is optional
  128. \\)
  129. )
  130. ~xs";
  131. // Replace all mediaembed links by a (unique) hash
  132. $content = preg_replace_callback($regex, function($matches) {
  133. // Get the url and parse it
  134. $url = parse_url(htmlspecialchars_decode($matches[3]));
  135. // If there is no host set but there is a path, the file is local
  136. if (!isset($url['host']) && isset($url['path'])) {
  137. return $matches[0];
  138. }
  139. if (!isset($matches['title'])) {
  140. $matches['title'] = '';
  141. }
  142. return $this->hash($matches[0], $matches);
  143. }, $content);
  144. return $content;
  145. }
  146. public function process($content, $config = [])
  147. {
  148. /** @var Twig $twig */
  149. $twig = self::getGrav()['twig'];
  150. // Initialize unique per-page counter
  151. $uid = 1;
  152. // '~(<p>)?\s*<a[^>]*href\s*=\s*([\'"])(?P<href>.*?)\2[^>]*>(?P<code>.*?)</a>\s*(?(1)(</p>))~i',
  153. // Get all <a> tags and extract "href" attribute
  154. $content = preg_replace_callback(
  155. '~mediaembed::([0-9a-z]+)::([0-9]+)::M~i',
  156. function($match) use ($twig, &$uid, $config) {
  157. list($embed, $data) = $this->hashes[$match[0]];
  158. // Check if a service for a specific domain is registered
  159. if ($this->service->match($data['src'])) {
  160. $mediaembed = [
  161. 'uid' => $uid++,
  162. 'service' => null,
  163. 'config' => $config,
  164. 'raw' => [
  165. 'alt' => $data['alt'],
  166. 'title' => $data['title'],
  167. 'src' => html_entity_decode($data['src']),
  168. ],
  169. 'success' => true,
  170. 'message' => '',
  171. ];
  172. // Load and get data of OEmbed Media Service
  173. try {
  174. $provider = $this->service->embed($data['src']);
  175. } catch (\Exception $e) {
  176. $mediaembed['message'] = $e->getMessage();
  177. $mediaembed['success'] = false;
  178. }
  179. // Setup variables for embedding OEmbed Media Service
  180. if ($mediaembed['success']) {
  181. // Get assets/options of current provider
  182. $assets = $provider->onTwigTemplateVariables(
  183. new Event(['service' => $this->service, 'mediaembed' => $this])
  184. );
  185. // Assets are passed by value as an array
  186. if (is_array($assets)) {
  187. $this->addAssets($assets);
  188. }
  189. // Add OEmbed Service to variables
  190. $mediaembed['service'] = $provider;
  191. // TODO: Cache contents from thumbnail and url
  192. }
  193. // Embed OEmbed Media
  194. $vars = ['mediaembed' => $mediaembed];
  195. $template = 'partials/mediaembed' . TEMPLATE_EXT;
  196. $embed = $twig->processTemplate($template, $vars);
  197. } else {
  198. $text = (strlen($data['alt']) > 0) ? $data['alt'] : $data['src'];
  199. // If display link or img
  200. $link = $config->get('link');
  201. if($link == true) {
  202. $attributes = [
  203. 'href' => $data['src'],
  204. 'title' => $data['title'],
  205. ];
  206. $format = '<a%s>%s</a>';
  207. } else {
  208. $attributes = [
  209. 'src' => $data['src'],
  210. 'title' => $data['title'],
  211. 'alt' => $data['alt'],
  212. ];
  213. $format = '<img%s>';
  214. }
  215. foreach ($attributes as $key => $value) {
  216. if (strlen($value) == 0) {
  217. unset($attributes[$key]);
  218. } else {
  219. $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
  220. $attributes[$key] = $key . '="' . $value . '"';
  221. }
  222. }
  223. $attributes = $attributes ? ' ' . implode(' ', $attributes) : '';
  224. // Transform embed media to link or img for compatibility
  225. $embed = sprintf($format, $attributes, $text);
  226. }
  227. return $embed;
  228. }, $content);
  229. $this->reset();
  230. // Write content back to page
  231. return $content;
  232. }
  233. /**
  234. * Fires an event with optional parameters.
  235. *
  236. * @param string $eventName The name of the event.
  237. * @param Event $event Optional parameter to be passed to the
  238. * called methods.
  239. * @return Event
  240. */
  241. public function fireEvent($eventName, Event $event = null)
  242. {
  243. // Dispatch event; just propagate it to service class
  244. return $this->service->call($eventName, $event);
  245. }
  246. /**
  247. * Get assets of loaded media services.
  248. *
  249. * @param boolean $reset Toggle whether to reset assets after retrieving
  250. * or not.
  251. */
  252. public function getAssets($reset = true)
  253. {
  254. $assets = $this->assets;
  255. if ($reset) {
  256. $this->assets = [];
  257. }
  258. return $assets;
  259. }
  260. /**
  261. * Add assets to the queue of MediaEmbed plugin
  262. *
  263. * @param array $assets An array of assets to add.
  264. * @param boolean $append Append assets to array or reset assets.
  265. */
  266. public function addAssets($assets, $append = true)
  267. {
  268. // Append or reset assets
  269. if (!$append) {
  270. $this->assets = [];
  271. }
  272. // Wrap non-array assets in an array
  273. if (!is_array($assets)) {
  274. $assets = array($assets);
  275. }
  276. // Merge assets
  277. $assets = array_merge($this->assets, $assets);
  278. // Remove duplicates
  279. $this->assets = array_keys(array_flip($assets));
  280. }
  281. /**
  282. * Add assets to the queue of MediaEmbed plugin
  283. *
  284. * Alias for `addAssets`
  285. *
  286. * @param array $assets An array of assets to add.
  287. * @param boolean $append Append assets to array or reset assets.
  288. */
  289. public function add($assets, $append = true)
  290. {
  291. return $this->addAssets($assets, $append);
  292. }
  293. /**
  294. * Add assets to the queue of MediaEmbed plugin
  295. *
  296. * Alias for `addAssets`
  297. *
  298. * @param array $assets An array of assets to add.
  299. * @param boolean $append Append assets to array or reset assets.
  300. */
  301. public function addCss($assets, $append = true)
  302. {
  303. return $this->addAssets($assets, $append);
  304. }
  305. /**
  306. * Add assets to the queue of MediaEmbed plugin
  307. *
  308. * Alias for `addAssets`
  309. *
  310. * @param array $assets An array of assets to add.
  311. * @param boolean $append Append assets to array or reset assets.
  312. */
  313. public function addJs($assets, $append = true)
  314. {
  315. return $this->addAssets($assets, $append);
  316. }
  317. /** -------------------------------
  318. * Private/protected helper methods
  319. * --------------------------------
  320. */
  321. /**
  322. * Get cached media or media with key.
  323. *
  324. * @param string $key The key to load from the cache.
  325. * @return mixed The media content.
  326. */
  327. protected function getCachedMedia($key)
  328. {
  329. /** @var Cache $cache */
  330. $cache = $grav['cache'];
  331. // Check, if cache should be used or not
  332. if ($this->config->get('cache.enabled')) {
  333. // Get cache id and try to fetch data
  334. $cache_id = md5('mediaembed' . $key . $cache->getKey());
  335. $data = $cache->fetch($cache_id);
  336. if ((false === $data) || (time() > $data['expire'])) {
  337. // Pack and provide data with a time stamp.
  338. $data = array(
  339. 'content' => $this->service->embed($key),
  340. 'expire' => time() + $this->config->get('cache.lifetime'),
  341. );
  342. $cache->save($cache_id, $data);
  343. }
  344. // Return data contents
  345. $content = $data['content'];
  346. } else {
  347. // Just call callback and return result
  348. $content = $this->service->embed($key);
  349. }
  350. return $content;
  351. }
  352. protected function parseUrl($url)
  353. {
  354. if (!filter_var($url, FILTER_VALIDATE_URL)) {
  355. return [];
  356. }
  357. // Parse URL
  358. $url = html_entity_decode($url, ENT_COMPAT | ENT_HTML401, 'UTF-8');
  359. $parts = parse_url($url);
  360. $parts['url'] = $url;
  361. // Get top-level domain from URL
  362. $parts['domain'] = isset($parts['host']) ? $parts['host'] : '';
  363. if ( preg_match('~(?P<domain>[a-z0-9][a-z0-9\-]{1,63}\.[a-z\.]{2,6})$~i', $parts['domain'], $match) ) {
  364. $parts['domain'] = $match['domain'];
  365. }
  366. if (isset($parts['query'])) {
  367. parse_str(urldecode($parts['query']), $parts['query']);
  368. }
  369. $parts['query'] = [];
  370. return $parts;
  371. }
  372. /**
  373. * Reset MathJax class
  374. */
  375. protected function reset()
  376. {
  377. $this->hashes = [];
  378. }
  379. /**
  380. * Hash a given text.
  381. *
  382. * Called whenever a tag must be hashed when a function insert an
  383. * atomic element in the text stream. Passing $text to through this
  384. * function gives a unique text-token which will be reverted back when
  385. * calling unhash.
  386. *
  387. * @param string $text The text to be hashed
  388. * @param string $type The type (category) the text should be saved
  389. *
  390. * @return string Return a unique text-token which will be
  391. * reverted back when calling unhash.
  392. */
  393. protected function hash($text, $data = [])
  394. {
  395. static $counter = 0;
  396. // Swap back any tag hash found in $text so we do not have to `unhash`
  397. // multiple times at the end.
  398. $text = $this->unhash($text);
  399. // Then hash the block
  400. $key = implode('::', array('mediaembed', $this->id, ++$counter, 'M'));
  401. $this->hashes[$key] = [$text, $data];
  402. // String that will replace the tag
  403. return $key;
  404. }
  405. /**
  406. * Swap back in all the tags hashed by hash.
  407. *
  408. * @param string $text The text to be un-hashed
  409. *
  410. * @return string A text containing no hash inside
  411. */
  412. protected function unhash($text)
  413. {
  414. $text = preg_replace_callback(
  415. '~mediaembed::([0-9a-z]+)::([0-9]+)::M~i', function($atches) {
  416. return $this->hashes[$matches[0]][0];
  417. }, $text);
  418. return $text;
  419. }
  420. }