OEmbed.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. <?php
  2. /**
  3. * OEmbed
  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\OEmbed;
  11. use Grav\Common\GravTrait;
  12. use Grav\Common\Data\Data;
  13. use RocketTheme\Toolbox\Event\Event;
  14. /**
  15. * OEmbed
  16. */
  17. class OEmbed implements OEmbedInterface
  18. {
  19. use GravTrait;
  20. /**
  21. * @var \Grav\Common\Data\Data
  22. */
  23. protected $base_config;
  24. /**
  25. * @var \Grav\Common\Data\Data
  26. */
  27. protected $config;
  28. /**
  29. * @var string
  30. */
  31. protected $embedCode = '';
  32. /**
  33. * @var array
  34. */
  35. protected $attributes;
  36. /**
  37. * @var array
  38. */
  39. protected $params;
  40. /**
  41. * @var array
  42. */
  43. protected $oembed;
  44. protected $protocol;
  45. /** -------------
  46. * Public methods
  47. * --------------
  48. */
  49. /**
  50. * Constructor.
  51. */
  52. public function __construct(array $config = [])
  53. {
  54. $this->base_config = $this->config = new Data($config);
  55. $schemes = $this->base_config->get('schemes', []);
  56. if (!is_array($schemes)) {
  57. $schemes = [$schemes];
  58. }
  59. foreach ($schemes as $index => $scheme) {
  60. $scheme = preg_quote($scheme);
  61. $schemes[$index] = preg_replace_callback('~((?:\\\\\*){1,2})(.[^\\\\]?|$)~',
  62. function($match) {
  63. // Remove control characters
  64. $separator = preg_replace('~[^\p{L}]~i', '', $match[2]);
  65. $star = strlen(str_replace('\\', '', $match[1]));
  66. if (ctype_alnum($separator)) {
  67. $replace = '.*?';
  68. } else {
  69. $separator = (strlen($separator) == 0) ? substr($match[2], -1) : $separator;
  70. $replace = (strlen($match[2]) > 0) ? "[^$separator ]+" : '[^\"\&\?\. ]+';
  71. }
  72. // Wrap one star result in parenthesis
  73. $replace = ($star > 1) ? $replace : "($replace)";
  74. return $replace . $match[2];
  75. }, $scheme);
  76. }
  77. $this->base_config->set('schemes', $schemes);
  78. }
  79. public function init($embedCode, $config = [])
  80. {
  81. $this->reset();
  82. // Normalize URL to embed
  83. $url = $this->parseUrl($embedCode);
  84. $this->embedCode = $this->canonicalize($embedCode);
  85. $this->oembed = new Data((array) $this->getOEmbed());
  86. // Get media attributes and object parameters
  87. $attributes = [
  88. 'width'=> $this->oembed->get('width', 0),
  89. 'height' => $this->oembed->get('height', 0),
  90. 'protocol' => ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://",
  91. ];
  92. // $this->config->merge($config);
  93. // $attributes = $this->config->get('media', []);
  94. $params = array_replace_recursive($this->config->get('params', []), $url['query']);
  95. // Copy media attributes from object parameters
  96. $attr_keys = ['width', 'height', 'adjust', 'preview', 'responsive'];
  97. foreach ($attr_keys as $key) {
  98. if (isset($params[$key])) {
  99. $attributes[$key] = $params[$key];
  100. unset($params[$key]);
  101. }
  102. }
  103. // Set media attributes and object parameters
  104. $this->attributes($attributes);
  105. $this->params($params);
  106. }
  107. public function canonicalize($embedCode)
  108. {
  109. $schemes = $this->config->get('schemes', []);
  110. foreach ($schemes as $scheme) {
  111. preg_match("~$scheme~i", $embedCode, $matches);
  112. if ($matches && $this->validId(end($matches))) {
  113. return end($matches);
  114. }
  115. }
  116. }
  117. /**
  118. * Check if a media id is valid.
  119. *
  120. * @param string $id Id to check against the oembed stream.
  121. *
  122. * @return boolean TRUE if id is valid, FALSE otherwise. Throws errors
  123. * on invalid ids.
  124. */
  125. protected function validId($id)
  126. {
  127. $endpoint = $this->config->get('endpoint', '');
  128. $endpoint = $this->format($endpoint, ['{:id}' => $id]);
  129. if (!$id || !$endpoint) {
  130. return false;
  131. }
  132. $response = \Requests::head($endpoint);
  133. // If a head request fails, try to send a get request
  134. if ($response->status_code != 200) {
  135. $response = \Requests::get($endpoint);
  136. }
  137. if ( $response->status_code == 401 ) {
  138. throw new \Exception('Embedding has been disabled for this media.');
  139. } elseif ( $response->status_code == 404 ) {
  140. throw new \Exception('The media ID was not found.');
  141. } elseif ( $response->status_code == 501 ) {
  142. throw new \Exception('Media informations can not be retrieved.');
  143. } elseif ( $response->status_code != 200 ) {
  144. throw new \Exception('The media ID is invalid or the media was deleted.');
  145. } elseif (!$response->success) {
  146. $response->throw_for_status();
  147. }
  148. return true;
  149. }
  150. public function reset()
  151. {
  152. // Reset values
  153. $this->embedCode = '';
  154. $this->oembed = null;
  155. $this->attributes([], true);
  156. $this->params([], true);
  157. $this->config = new Data($this->base_config->toArray());
  158. }
  159. public function id()
  160. {
  161. return $this->embedCode;
  162. }
  163. public function slug()
  164. {
  165. $slug = strtolower($this->name()) . '://' . $this->id();
  166. return $slug;
  167. }
  168. public function name()
  169. {
  170. $name = get_class($this);
  171. $name = substr($name, strrpos($name, '\\') + 1);
  172. if ($this->embedCode) {
  173. $name = $this->config->get('name', $name);
  174. if (mb_strlen($name) == 0 && $this->oembed) {
  175. $this->oembed->get('provider_name', '');
  176. }
  177. }
  178. return $name;
  179. }
  180. public function title()
  181. {
  182. $title = '';
  183. if ($this->oembed) {
  184. $title = $this->oembed->get('title', '');
  185. }
  186. return $title;
  187. }
  188. public function description()
  189. {
  190. $description = '';
  191. if ($this->oembed) {
  192. $description = $this->oembed->get('description', '');
  193. }
  194. return $description;
  195. }
  196. public function url()
  197. {
  198. $url = '';
  199. if ($this->embedCode && $this->oembed) {
  200. $url = $this->format($this->config->get('url', ''), ['{:url}' => '']);
  201. if (strlen($url) == 0) {
  202. $url = $this->oembed->get('url', $url);
  203. } else {
  204. $protocol = isset($this->attributes['protocol']) ? $this->attributes['protocol'] : '//';
  205. $url = $protocol . $url;
  206. }
  207. }
  208. return $url;
  209. }
  210. public function website()
  211. {
  212. $website = '';
  213. if ($this->oembed) {
  214. $website = $this->oembed->get('provider_url', '');
  215. }
  216. return $website;
  217. }
  218. /**
  219. * Returns a png img
  220. *
  221. * @param array $stub or string $alias
  222. * @return Resource or null if not available
  223. */
  224. public function icon() {
  225. $icon = '';
  226. $endpoint = '';
  227. if ($this->oembed) {
  228. $endpoint = $this->format($this->config->get('endpoint', ''));
  229. }
  230. if (!$endpoint) {
  231. return $icon;
  232. }
  233. $pieces = parse_url($endpoint);
  234. $url = $pieces['host'];
  235. // Grab favicon from Google cache
  236. $icon = 'http://www.google.com/s2/favicons?domain=';
  237. $icon .= urlencode($url);
  238. return $icon;
  239. }
  240. public function thumbnail()
  241. {
  242. $thumbnail = '';
  243. if ($this->oembed) {
  244. $thumbnail = $this->oembed->get('thumbnail_url', '');
  245. }
  246. return $thumbnail;
  247. }
  248. public function type()
  249. {
  250. $type = $this->config->get('type', 'generic');
  251. if ($type === 'generic' && $this->embedCode && $this->oembed) {
  252. $type = $this->oembed->get('type', $type);
  253. }
  254. return $type;
  255. }
  256. public function author($key = 'name')
  257. {
  258. $author = '';
  259. if ($this->embedCode && $this->oembed) {
  260. $author = $this->oembed->get('author_' . strtolower($key), '');
  261. }
  262. return $author;
  263. }
  264. public function attributes($var = null, $reset = false)
  265. {
  266. if ($var !== null) {
  267. if ($reset) {
  268. $this->attributes = $var;
  269. } else {
  270. $this->attributes = array_replace_recursive($this->attributes, $var);
  271. }
  272. }
  273. if (!is_array($this->attributes)) {
  274. $this->attributes = [];
  275. }
  276. return $this->attributes;
  277. }
  278. public function params($var = null, $reset = false)
  279. {
  280. if ($var !== null) {
  281. if ($reset) {
  282. $this->params = $var;
  283. } else {
  284. $this->params = array_replace_recursive($this->params, $var);
  285. }
  286. }
  287. if (!is_array($this->params)) {
  288. $this->params = [];
  289. }
  290. return $this->params;
  291. }
  292. public function getEmbedCode($params = [])
  293. {
  294. $params = array_replace_recursive($this->params(), $params);
  295. $url = $this->url();
  296. $query = http_build_query($params);
  297. if (mb_strlen($query) > 0) {
  298. $query = (false === strpos($url, '?') ? '?' : '&') . $query;
  299. }
  300. return $url . $query;
  301. }
  302. /**
  303. * Returns information about the media. See http://www.oembed.com/.
  304. *
  305. * @return
  306. * If oEmbed information is available, an array containing 'title', 'type',
  307. * 'url', and other information as specified by the oEmbed standard.
  308. * Otherwise, NULL.
  309. */
  310. public function getOEmbed()
  311. {
  312. if ($this->oembed) {
  313. return $this->oembed;
  314. }
  315. $endpoint = $this->format($this->config->get('endpoint', ''));
  316. if (!$endpoint) {
  317. return [];
  318. }
  319. $response = \Requests::get($endpoint);
  320. if (!$response->success) {
  321. $response->throw_for_status();
  322. }
  323. return json_decode($response->body, true);
  324. }
  325. /**
  326. * Return the domain(s) of this media resource
  327. *
  328. * @return string
  329. */
  330. public function getDomains()
  331. {
  332. // Get domains of media resources
  333. $schemes = $this->base_config->get('schemes', []);
  334. // Ensure domains are of type array
  335. if (!is_array($schemes)) {
  336. $schemes = [$schemes];
  337. }
  338. $domains = [];
  339. foreach ($schemes as $scheme) {
  340. // Trick: extract domains from scheme attributes
  341. $domain = parse_url(str_replace('\.', '.', "http://$scheme"), PHP_URL_HOST);
  342. // Take out the www. in front of domain
  343. $domains[] = preg_replace("/^www\./", '', $domain);
  344. }
  345. // Faster alternative to PHP’s array unique function
  346. return array_keys(array_flip($domains));
  347. }
  348. public function onTwigTemplateVariables(Event $event)
  349. {
  350. $mediaembed = $event['mediaembed'];
  351. foreach ($this->config->get('assets', []) as $asset) {
  352. if (is_string($asset) && strlen($asset) > 0) {
  353. $mediaembed->add($asset);
  354. }
  355. }
  356. }
  357. /**
  358. * Convenience wrapper for `echo $ServiceProvider`
  359. *
  360. * @return string
  361. */
  362. public function __toString()
  363. {
  364. return $this->getEmbedCode();
  365. }
  366. /** -------------------------------
  367. * Private/protected helper methods
  368. * --------------------------------
  369. */
  370. protected function format($string, $params = [])
  371. {
  372. $keys = ['id', 'name', 'url'];
  373. foreach ($keys as $key) {
  374. if (!isset($params["{:$key}"])) {
  375. $params["{:$key}"] = $this->{$key}();
  376. }
  377. }
  378. $params += [
  379. '{:canonical}' => $this->config->get('canonical', ''),
  380. ];
  381. // Format URL placeholder with params
  382. $keys = ['{:url}', '{:canonical}'];
  383. foreach ($keys as $key) {
  384. $params[$key] = urlencode(str_ireplace(
  385. array_keys($params), $params, $params[$key])
  386. );
  387. }
  388. // Replace OEmbed calls with response
  389. $string = preg_replace_callback('~\{\:oembed(?:\.(?=\w))([\.\w_]+)?\}~i',
  390. function($match) {
  391. $ombed = $this->getOEmbed();
  392. return $oembed ? $oembed->get($match[1], '') : $match[0];
  393. }, $string);
  394. return str_ireplace(array_keys($params), $params, $string);
  395. }
  396. protected function parseUrl($url)
  397. {
  398. if (!filter_var($url, FILTER_VALIDATE_URL)) {
  399. return [];
  400. }
  401. // Parse URL
  402. $url = html_entity_decode($url, ENT_COMPAT | ENT_HTML401, 'UTF-8');
  403. $parts = parse_url($url);
  404. $parts['url'] = $url;
  405. // Get top-level domain from URL
  406. $parts['domain'] = isset($parts['host']) ? $parts['host'] : '';
  407. if ( preg_match('~(?P<domain>[a-z0-9][a-z0-9\-]{1,63}\.[a-z\.]{2,6})$~i', $parts['domain'], $match) ) {
  408. $parts['domain'] = $match['domain'];
  409. }
  410. if (isset($parts['query'])) {
  411. parse_str(urldecode($parts['query']), $parts['query']);
  412. } else {
  413. $parts['query'] = [];
  414. }
  415. return $parts;
  416. }
  417. }