Няма описание

class-wp-oembed.php 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. <?php
  2. /**
  3. * API for fetching the HTML to embed remote content based on a provided URL
  4. *
  5. * Used internally by the WP_Embed class, but is designed to be generic.
  6. *
  7. * @link https://wordpress.org/support/article/embeds/
  8. * @link http://oembed.com/
  9. *
  10. * @package WordPress
  11. * @subpackage oEmbed
  12. */
  13. /**
  14. * Core class used to implement oEmbed functionality.
  15. *
  16. * @since 2.9.0
  17. */
  18. class WP_oEmbed {
  19. /**
  20. * A list of oEmbed providers.
  21. *
  22. * @since 2.9.0
  23. * @var array
  24. */
  25. public $providers = array();
  26. /**
  27. * A list of an early oEmbed providers.
  28. *
  29. * @since 4.0.0
  30. * @var array
  31. */
  32. public static $early_providers = array();
  33. /**
  34. * A list of private/protected methods, used for backward compatibility.
  35. *
  36. * @since 4.2.0
  37. * @var array
  38. */
  39. private $compat_methods = array( '_fetch_with_format', '_parse_json', '_parse_xml', '_parse_xml_body' );
  40. /**
  41. * Constructor.
  42. *
  43. * @since 2.9.0
  44. */
  45. public function __construct() {
  46. $host = urlencode( home_url() );
  47. $providers = array(
  48. '#https?://((m|www)\.)?youtube\.com/watch.*#i' => array( 'https://www.youtube.com/oembed', true ),
  49. '#https?://((m|www)\.)?youtube\.com/playlist.*#i' => array( 'https://www.youtube.com/oembed', true ),
  50. '#https?://youtu\.be/.*#i' => array( 'https://www.youtube.com/oembed', true ),
  51. '#https?://(.+\.)?vimeo\.com/.*#i' => array( 'https://vimeo.com/api/oembed.{format}', true ),
  52. '#https?://(www\.)?dailymotion\.com/.*#i' => array( 'https://www.dailymotion.com/services/oembed', true ),
  53. '#https?://dai\.ly/.*#i' => array( 'https://www.dailymotion.com/services/oembed', true ),
  54. '#https?://(www\.)?flickr\.com/.*#i' => array( 'https://www.flickr.com/services/oembed/', true ),
  55. '#https?://flic\.kr/.*#i' => array( 'https://www.flickr.com/services/oembed/', true ),
  56. '#https?://(.+\.)?smugmug\.com/.*#i' => array( 'https://api.smugmug.com/services/oembed/', true ),
  57. '#https?://(www\.)?scribd\.com/(doc|document)/.*#i' => array( 'https://www.scribd.com/services/oembed', true ),
  58. '#https?://wordpress\.tv/.*#i' => array( 'https://wordpress.tv/oembed/', true ),
  59. '#https?://(.+\.)?polldaddy\.com/.*#i' => array( 'https://api.crowdsignal.com/oembed', true ),
  60. '#https?://poll\.fm/.*#i' => array( 'https://api.crowdsignal.com/oembed', true ),
  61. '#https?://(.+\.)?survey\.fm/.*#i' => array( 'https://api.crowdsignal.com/oembed', true ),
  62. '#https?://(www\.)?twitter\.com/\w{1,15}/status(es)?/.*#i' => array( 'https://publish.twitter.com/oembed', true ),
  63. '#https?://(www\.)?twitter\.com/\w{1,15}$#i' => array( 'https://publish.twitter.com/oembed', true ),
  64. '#https?://(www\.)?twitter\.com/\w{1,15}/likes$#i' => array( 'https://publish.twitter.com/oembed', true ),
  65. '#https?://(www\.)?twitter\.com/\w{1,15}/lists/.*#i' => array( 'https://publish.twitter.com/oembed', true ),
  66. '#https?://(www\.)?twitter\.com/\w{1,15}/timelines/.*#i' => array( 'https://publish.twitter.com/oembed', true ),
  67. '#https?://(www\.)?twitter\.com/i/moments/.*#i' => array( 'https://publish.twitter.com/oembed', true ),
  68. '#https?://(www\.)?soundcloud\.com/.*#i' => array( 'https://soundcloud.com/oembed', true ),
  69. '#https?://(.+?\.)?slideshare\.net/.*#i' => array( 'https://www.slideshare.net/api/oembed/2', true ),
  70. '#https?://(open|play)\.spotify\.com/.*#i' => array( 'https://embed.spotify.com/oembed/', true ),
  71. '#https?://(.+\.)?imgur\.com/.*#i' => array( 'https://api.imgur.com/oembed', true ),
  72. '#https?://(www\.)?meetu(\.ps|p\.com)/.*#i' => array( 'https://api.meetup.com/oembed', true ),
  73. '#https?://(www\.)?issuu\.com/.+/docs/.+#i' => array( 'https://issuu.com/oembed_wp', true ),
  74. '#https?://(www\.)?mixcloud\.com/.*#i' => array( 'https://www.mixcloud.com/oembed', true ),
  75. '#https?://(www\.|embed\.)?ted\.com/talks/.*#i' => array( 'https://www.ted.com/services/v1/oembed.{format}', true ),
  76. '#https?://(www\.)?(animoto|video214)\.com/play/.*#i' => array( 'https://animoto.com/oembeds/create', true ),
  77. '#https?://(.+)\.tumblr\.com/post/.*#i' => array( 'https://www.tumblr.com/oembed/1.0', true ),
  78. '#https?://(www\.)?kickstarter\.com/projects/.*#i' => array( 'https://www.kickstarter.com/services/oembed', true ),
  79. '#https?://kck\.st/.*#i' => array( 'https://www.kickstarter.com/services/oembed', true ),
  80. '#https?://cloudup\.com/.*#i' => array( 'https://cloudup.com/oembed', true ),
  81. '#https?://(www\.)?reverbnation\.com/.*#i' => array( 'https://www.reverbnation.com/oembed', true ),
  82. '#https?://videopress\.com/v/.*#' => array( 'https://public-api.wordpress.com/oembed/?for=' . $host, true ),
  83. '#https?://(www\.)?reddit\.com/r/[^/]+/comments/.*#i' => array( 'https://www.reddit.com/oembed', true ),
  84. '#https?://(www\.)?speakerdeck\.com/.*#i' => array( 'https://speakerdeck.com/oembed.{format}', true ),
  85. '#https?://(www\.)?screencast\.com/.*#i' => array( 'https://api.screencast.com/external/oembed', true ),
  86. '#https?://([a-z0-9-]+\.)?amazon\.(com|com\.mx|com\.br|ca)/.*#i' => array( 'https://read.amazon.com/kp/api/oembed', true ),
  87. '#https?://([a-z0-9-]+\.)?amazon\.(co\.uk|de|fr|it|es|in|nl|ru)/.*#i' => array( 'https://read.amazon.co.uk/kp/api/oembed', true ),
  88. '#https?://([a-z0-9-]+\.)?amazon\.(co\.jp|com\.au)/.*#i' => array( 'https://read.amazon.com.au/kp/api/oembed', true ),
  89. '#https?://([a-z0-9-]+\.)?amazon\.cn/.*#i' => array( 'https://read.amazon.cn/kp/api/oembed', true ),
  90. '#https?://(www\.)?a\.co/.*#i' => array( 'https://read.amazon.com/kp/api/oembed', true ),
  91. '#https?://(www\.)?amzn\.to/.*#i' => array( 'https://read.amazon.com/kp/api/oembed', true ),
  92. '#https?://(www\.)?amzn\.eu/.*#i' => array( 'https://read.amazon.co.uk/kp/api/oembed', true ),
  93. '#https?://(www\.)?amzn\.in/.*#i' => array( 'https://read.amazon.in/kp/api/oembed', true ),
  94. '#https?://(www\.)?amzn\.asia/.*#i' => array( 'https://read.amazon.com.au/kp/api/oembed', true ),
  95. '#https?://(www\.)?z\.cn/.*#i' => array( 'https://read.amazon.cn/kp/api/oembed', true ),
  96. '#https?://www\.someecards\.com/.+-cards/.+#i' => array( 'https://www.someecards.com/v2/oembed/', true ),
  97. '#https?://www\.someecards\.com/usercards/viewcard/.+#i' => array( 'https://www.someecards.com/v2/oembed/', true ),
  98. '#https?://some\.ly\/.+#i' => array( 'https://www.someecards.com/v2/oembed/', true ),
  99. '#https?://(www\.)?tiktok\.com/.*/video/.*#i' => array( 'https://www.tiktok.com/oembed', true ),
  100. );
  101. if ( ! empty( self::$early_providers['add'] ) ) {
  102. foreach ( self::$early_providers['add'] as $format => $data ) {
  103. $providers[ $format ] = $data;
  104. }
  105. }
  106. if ( ! empty( self::$early_providers['remove'] ) ) {
  107. foreach ( self::$early_providers['remove'] as $format ) {
  108. unset( $providers[ $format ] );
  109. }
  110. }
  111. self::$early_providers = array();
  112. /**
  113. * Filters the list of sanctioned oEmbed providers.
  114. *
  115. * Since WordPress 4.4, oEmbed discovery is enabled for all users and allows embedding of sanitized
  116. * iframes. The providers in this list are sanctioned, meaning they are trusted and allowed to
  117. * embed any content, such as iframes, videos, JavaScript, and arbitrary HTML.
  118. *
  119. * Supported providers:
  120. *
  121. * | Provider | Flavor | Since |
  122. * | ------------ | ----------------------------------------- | ------- |
  123. * | Dailymotion | dailymotion.com | 2.9.0 |
  124. * | Flickr | flickr.com | 2.9.0 |
  125. * | Scribd | scribd.com | 2.9.0 |
  126. * | Vimeo | vimeo.com | 2.9.0 |
  127. * | WordPress.tv | wordpress.tv | 2.9.0 |
  128. * | YouTube | youtube.com/watch | 2.9.0 |
  129. * | Crowdsignal | polldaddy.com | 3.0.0 |
  130. * | SmugMug | smugmug.com | 3.0.0 |
  131. * | YouTube | youtu.be | 3.0.0 |
  132. * | Twitter | twitter.com | 3.4.0 |
  133. * | Slideshare | slideshare.net | 3.5.0 |
  134. * | SoundCloud | soundcloud.com | 3.5.0 |
  135. * | Dailymotion | dai.ly | 3.6.0 |
  136. * | Flickr | flic.kr | 3.6.0 |
  137. * | Spotify | spotify.com | 3.6.0 |
  138. * | Imgur | imgur.com | 3.9.0 |
  139. * | Meetup.com | meetup.com | 3.9.0 |
  140. * | Meetup.com | meetu.ps | 3.9.0 |
  141. * | Animoto | animoto.com | 4.0.0 |
  142. * | Animoto | video214.com | 4.0.0 |
  143. * | Issuu | issuu.com | 4.0.0 |
  144. * | Mixcloud | mixcloud.com | 4.0.0 |
  145. * | Crowdsignal | poll.fm | 4.0.0 |
  146. * | TED | ted.com | 4.0.0 |
  147. * | YouTube | youtube.com/playlist | 4.0.0 |
  148. * | Tumblr | tumblr.com | 4.2.0 |
  149. * | Kickstarter | kickstarter.com | 4.2.0 |
  150. * | Kickstarter | kck.st | 4.2.0 |
  151. * | Cloudup | cloudup.com | 4.3.0 |
  152. * | ReverbNation | reverbnation.com | 4.4.0 |
  153. * | VideoPress | videopress.com | 4.4.0 |
  154. * | Reddit | reddit.com | 4.4.0 |
  155. * | Speaker Deck | speakerdeck.com | 4.4.0 |
  156. * | Twitter | twitter.com/timelines | 4.5.0 |
  157. * | Twitter | twitter.com/moments | 4.5.0 |
  158. * | Twitter | twitter.com/user | 4.7.0 |
  159. * | Twitter | twitter.com/likes | 4.7.0 |
  160. * | Twitter | twitter.com/lists | 4.7.0 |
  161. * | Screencast | screencast.com | 4.8.0 |
  162. * | Amazon | amazon.com (com.mx, com.br, ca) | 4.9.0 |
  163. * | Amazon | amazon.de (fr, it, es, in, nl, ru, co.uk) | 4.9.0 |
  164. * | Amazon | amazon.co.jp (com.au) | 4.9.0 |
  165. * | Amazon | amazon.cn | 4.9.0 |
  166. * | Amazon | a.co | 4.9.0 |
  167. * | Amazon | amzn.to (eu, in, asia) | 4.9.0 |
  168. * | Amazon | z.cn | 4.9.0 |
  169. * | Someecards | someecards.com | 4.9.0 |
  170. * | Someecards | some.ly | 4.9.0 |
  171. * | Crowdsignal | survey.fm | 5.1.0 |
  172. * | TikTok | tiktok.com | 5.4.0 |
  173. *
  174. * No longer supported providers:
  175. *
  176. * | Provider | Flavor | Since | Removed |
  177. * | ------------ | -------------------- | --------- | --------- |
  178. * | Qik | qik.com | 2.9.0 | 3.9.0 |
  179. * | Viddler | viddler.com | 2.9.0 | 4.0.0 |
  180. * | Revision3 | revision3.com | 2.9.0 | 4.2.0 |
  181. * | Blip | blip.tv | 2.9.0 | 4.4.0 |
  182. * | Rdio | rdio.com | 3.6.0 | 4.4.1 |
  183. * | Rdio | rd.io | 3.6.0 | 4.4.1 |
  184. * | Vine | vine.co | 4.1.0 | 4.9.0 |
  185. * | Photobucket | photobucket.com | 2.9.0 | 5.1.0 |
  186. * | Funny or Die | funnyordie.com | 3.0.0 | 5.1.0 |
  187. * | CollegeHumor | collegehumor.com | 4.0.0 | 5.3.1 |
  188. * | Hulu | hulu.com | 2.9.0 | 5.5.0 |
  189. * | Instagram | instagram.com | 3.5.0 | 5.5.2 |
  190. * | Instagram | instagr.am | 3.5.0 | 5.5.2 |
  191. * | Instagram TV | instagram.com | 5.1.0 | 5.5.2 |
  192. * | Instagram TV | instagr.am | 5.1.0 | 5.5.2 |
  193. * | Facebook | facebook.com | 4.7.0 | 5.5.2 |
  194. *
  195. * @see wp_oembed_add_provider()
  196. *
  197. * @since 2.9.0
  198. *
  199. * @param array[] $providers An array of arrays containing data about popular oEmbed providers.
  200. */
  201. $this->providers = apply_filters( 'oembed_providers', $providers );
  202. // Fix any embeds that contain new lines in the middle of the HTML which breaks wpautop().
  203. add_filter( 'oembed_dataparse', array( $this, '_strip_newlines' ), 10, 3 );
  204. }
  205. /**
  206. * Exposes private/protected methods for backward compatibility.
  207. *
  208. * @since 4.0.0
  209. *
  210. * @param string $name Method to call.
  211. * @param array $arguments Arguments to pass when calling.
  212. * @return mixed|false Return value of the callback, false otherwise.
  213. */
  214. public function __call( $name, $arguments ) {
  215. if ( in_array( $name, $this->compat_methods, true ) ) {
  216. return $this->$name( ...$arguments );
  217. }
  218. return false;
  219. }
  220. /**
  221. * Takes a URL and returns the corresponding oEmbed provider's URL, if there is one.
  222. *
  223. * @since 4.0.0
  224. *
  225. * @see WP_oEmbed::discover()
  226. *
  227. * @param string $url The URL to the content.
  228. * @param string|array $args {
  229. * Optional. Additional provider arguments. Default empty.
  230. *
  231. * @type bool $discover Optional. Determines whether to attempt to discover link tags
  232. * at the given URL for an oEmbed provider when the provider URL
  233. * is not found in the built-in providers list. Default true.
  234. * }
  235. * @return string|false The oEmbed provider URL on success, false on failure.
  236. */
  237. public function get_provider( $url, $args = '' ) {
  238. $args = wp_parse_args( $args );
  239. $provider = false;
  240. if ( ! isset( $args['discover'] ) ) {
  241. $args['discover'] = true;
  242. }
  243. foreach ( $this->providers as $matchmask => $data ) {
  244. list( $providerurl, $regex ) = $data;
  245. // Turn the asterisk-type provider URLs into regex.
  246. if ( ! $regex ) {
  247. $matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i';
  248. $matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask );
  249. }
  250. if ( preg_match( $matchmask, $url ) ) {
  251. $provider = str_replace( '{format}', 'json', $providerurl ); // JSON is easier to deal with than XML.
  252. break;
  253. }
  254. }
  255. if ( ! $provider && $args['discover'] ) {
  256. $provider = $this->discover( $url );
  257. }
  258. return $provider;
  259. }
  260. /**
  261. * Adds an oEmbed provider.
  262. *
  263. * The provider is added just-in-time when wp_oembed_add_provider() is called before
  264. * the {@see 'plugins_loaded'} hook.
  265. *
  266. * The just-in-time addition is for the benefit of the {@see 'oembed_providers'} filter.
  267. *
  268. * @since 4.0.0
  269. *
  270. * @see wp_oembed_add_provider()
  271. *
  272. * @param string $format Format of URL that this provider can handle. You can use
  273. * asterisks as wildcards.
  274. * @param string $provider The URL to the oEmbed provider..
  275. * @param bool $regex Optional. Whether the $format parameter is in a regex format.
  276. * Default false.
  277. */
  278. public static function _add_provider_early( $format, $provider, $regex = false ) {
  279. if ( empty( self::$early_providers['add'] ) ) {
  280. self::$early_providers['add'] = array();
  281. }
  282. self::$early_providers['add'][ $format ] = array( $provider, $regex );
  283. }
  284. /**
  285. * Removes an oEmbed provider.
  286. *
  287. * The provider is removed just-in-time when wp_oembed_remove_provider() is called before
  288. * the {@see 'plugins_loaded'} hook.
  289. *
  290. * The just-in-time removal is for the benefit of the {@see 'oembed_providers'} filter.
  291. *
  292. * @since 4.0.0
  293. *
  294. * @see wp_oembed_remove_provider()
  295. *
  296. * @param string $format The format of URL that this provider can handle. You can use
  297. * asterisks as wildcards.
  298. */
  299. public static function _remove_provider_early( $format ) {
  300. if ( empty( self::$early_providers['remove'] ) ) {
  301. self::$early_providers['remove'] = array();
  302. }
  303. self::$early_providers['remove'][] = $format;
  304. }
  305. /**
  306. * Takes a URL and attempts to return the oEmbed data.
  307. *
  308. * @see WP_oEmbed::fetch()
  309. *
  310. * @since 4.8.0
  311. *
  312. * @param string $url The URL to the content that should be attempted to be embedded.
  313. * @param string|array $args Optional. Additional arguments for retrieving embed HTML.
  314. * See wp_oembed_get() for accepted arguments. Default empty.
  315. * @return object|false The result in the form of an object on success, false on failure.
  316. */
  317. public function get_data( $url, $args = '' ) {
  318. $args = wp_parse_args( $args );
  319. $provider = $this->get_provider( $url, $args );
  320. if ( ! $provider ) {
  321. return false;
  322. }
  323. $data = $this->fetch( $provider, $url, $args );
  324. if ( false === $data ) {
  325. return false;
  326. }
  327. return $data;
  328. }
  329. /**
  330. * The do-it-all function that takes a URL and attempts to return the HTML.
  331. *
  332. * @see WP_oEmbed::fetch()
  333. * @see WP_oEmbed::data2html()
  334. *
  335. * @since 2.9.0
  336. *
  337. * @param string $url The URL to the content that should be attempted to be embedded.
  338. * @param string|array $args Optional. Additional arguments for retrieving embed HTML.
  339. * See wp_oembed_get() for accepted arguments. Default empty.
  340. * @return string|false The UNSANITIZED (and potentially unsafe) HTML that should be used to embed
  341. * on success, false on failure.
  342. */
  343. public function get_html( $url, $args = '' ) {
  344. /**
  345. * Filters the oEmbed result before any HTTP requests are made.
  346. *
  347. * This allows one to short-circuit the default logic, perhaps by
  348. * replacing it with a routine that is more optimal for your setup.
  349. *
  350. * Returning a non-null value from the filter will effectively short-circuit retrieval
  351. * and return the passed value instead.
  352. *
  353. * @since 4.5.3
  354. *
  355. * @param null|string $result The UNSANITIZED (and potentially unsafe) HTML that should be used to embed.
  356. * Default null to continue retrieving the result.
  357. * @param string $url The URL to the content that should be attempted to be embedded.
  358. * @param string|array $args Optional. Additional arguments for retrieving embed HTML.
  359. * See wp_oembed_get() for accepted arguments. Default empty.
  360. */
  361. $pre = apply_filters( 'pre_oembed_result', null, $url, $args );
  362. if ( null !== $pre ) {
  363. return $pre;
  364. }
  365. $data = $this->get_data( $url, $args );
  366. if ( false === $data ) {
  367. return false;
  368. }
  369. /**
  370. * Filters the HTML returned by the oEmbed provider.
  371. *
  372. * @since 2.9.0
  373. *
  374. * @param string|false $data The returned oEmbed HTML (false if unsafe).
  375. * @param string $url URL of the content to be embedded.
  376. * @param string|array $args Optional. Additional arguments for retrieving embed HTML.
  377. * See wp_oembed_get() for accepted arguments. Default empty.
  378. */
  379. return apply_filters( 'oembed_result', $this->data2html( $data, $url ), $url, $args );
  380. }
  381. /**
  382. * Attempts to discover link tags at the given URL for an oEmbed provider.
  383. *
  384. * @since 2.9.0
  385. *
  386. * @param string $url The URL that should be inspected for discovery `<link>` tags.
  387. * @return string|false The oEmbed provider URL on success, false on failure.
  388. */
  389. public function discover( $url ) {
  390. $providers = array();
  391. $args = array(
  392. 'limit_response_size' => 153600, // 150 KB
  393. );
  394. /**
  395. * Filters oEmbed remote get arguments.
  396. *
  397. * @since 4.0.0
  398. *
  399. * @see WP_Http::request()
  400. *
  401. * @param array $args oEmbed remote get arguments.
  402. * @param string $url URL to be inspected.
  403. */
  404. $args = apply_filters( 'oembed_remote_get_args', $args, $url );
  405. // Fetch URL content.
  406. $request = wp_safe_remote_get( $url, $args );
  407. $html = wp_remote_retrieve_body( $request );
  408. if ( $html ) {
  409. /**
  410. * Filters the link types that contain oEmbed provider URLs.
  411. *
  412. * @since 2.9.0
  413. *
  414. * @param string[] $format Array of oEmbed link types. Accepts 'application/json+oembed',
  415. * 'text/xml+oembed', and 'application/xml+oembed' (incorrect,
  416. * used by at least Vimeo).
  417. */
  418. $linktypes = apply_filters(
  419. 'oembed_linktypes',
  420. array(
  421. 'application/json+oembed' => 'json',
  422. 'text/xml+oembed' => 'xml',
  423. 'application/xml+oembed' => 'xml',
  424. )
  425. );
  426. // Strip <body>.
  427. $html_head_end = stripos( $html, '</head>' );
  428. if ( $html_head_end ) {
  429. $html = substr( $html, 0, $html_head_end );
  430. }
  431. // Do a quick check.
  432. $tagfound = false;
  433. foreach ( $linktypes as $linktype => $format ) {
  434. if ( stripos( $html, $linktype ) ) {
  435. $tagfound = true;
  436. break;
  437. }
  438. }
  439. if ( $tagfound && preg_match_all( '#<link([^<>]+)/?>#iU', $html, $links ) ) {
  440. foreach ( $links[1] as $link ) {
  441. $atts = shortcode_parse_atts( $link );
  442. if ( ! empty( $atts['type'] ) && ! empty( $linktypes[ $atts['type'] ] ) && ! empty( $atts['href'] ) ) {
  443. $providers[ $linktypes[ $atts['type'] ] ] = htmlspecialchars_decode( $atts['href'] );
  444. // Stop here if it's JSON (that's all we need).
  445. if ( 'json' === $linktypes[ $atts['type'] ] ) {
  446. break;
  447. }
  448. }
  449. }
  450. }
  451. }
  452. // JSON is preferred to XML.
  453. if ( ! empty( $providers['json'] ) ) {
  454. return $providers['json'];
  455. } elseif ( ! empty( $providers['xml'] ) ) {
  456. return $providers['xml'];
  457. } else {
  458. return false;
  459. }
  460. }
  461. /**
  462. * Connects to a oEmbed provider and returns the result.
  463. *
  464. * @since 2.9.0
  465. *
  466. * @param string $provider The URL to the oEmbed provider.
  467. * @param string $url The URL to the content that is desired to be embedded.
  468. * @param string|array $args Optional. Additional arguments for retrieving embed HTML.
  469. * See wp_oembed_get() for accepted arguments. Default empty.
  470. * @return object|false The result in the form of an object on success, false on failure.
  471. */
  472. public function fetch( $provider, $url, $args = '' ) {
  473. $args = wp_parse_args( $args, wp_embed_defaults( $url ) );
  474. $provider = add_query_arg( 'maxwidth', (int) $args['width'], $provider );
  475. $provider = add_query_arg( 'maxheight', (int) $args['height'], $provider );
  476. $provider = add_query_arg( 'url', urlencode( $url ), $provider );
  477. $provider = add_query_arg( 'dnt', 1, $provider );
  478. /**
  479. * Filters the oEmbed URL to be fetched.
  480. *
  481. * @since 2.9.0
  482. * @since 4.9.0 The `dnt` (Do Not Track) query parameter was added to all oEmbed provider URLs.
  483. *
  484. * @param string $provider URL of the oEmbed provider.
  485. * @param string $url URL of the content to be embedded.
  486. * @param array $args Optional. Additional arguments for retrieving embed HTML.
  487. * See wp_oembed_get() for accepted arguments. Default empty.
  488. */
  489. $provider = apply_filters( 'oembed_fetch_url', $provider, $url, $args );
  490. foreach ( array( 'json', 'xml' ) as $format ) {
  491. $result = $this->_fetch_with_format( $provider, $format );
  492. if ( is_wp_error( $result ) && 'not-implemented' === $result->get_error_code() ) {
  493. continue;
  494. }
  495. return ( $result && ! is_wp_error( $result ) ) ? $result : false;
  496. }
  497. return false;
  498. }
  499. /**
  500. * Fetches result from an oEmbed provider for a specific format and complete provider URL
  501. *
  502. * @since 3.0.0
  503. *
  504. * @param string $provider_url_with_args URL to the provider with full arguments list (url, maxheight, etc.)
  505. * @param string $format Format to use.
  506. * @return object|false|WP_Error The result in the form of an object on success, false on failure.
  507. */
  508. private function _fetch_with_format( $provider_url_with_args, $format ) {
  509. $provider_url_with_args = add_query_arg( 'format', $format, $provider_url_with_args );
  510. /** This filter is documented in wp-includes/class-wp-oembed.php */
  511. $args = apply_filters( 'oembed_remote_get_args', array(), $provider_url_with_args );
  512. $response = wp_safe_remote_get( $provider_url_with_args, $args );
  513. if ( 501 == wp_remote_retrieve_response_code( $response ) ) {
  514. return new WP_Error( 'not-implemented' );
  515. }
  516. $body = wp_remote_retrieve_body( $response );
  517. if ( ! $body ) {
  518. return false;
  519. }
  520. $parse_method = "_parse_$format";
  521. return $this->$parse_method( $body );
  522. }
  523. /**
  524. * Parses a json response body.
  525. *
  526. * @since 3.0.0
  527. *
  528. * @param string $response_body
  529. * @return object|false
  530. */
  531. private function _parse_json( $response_body ) {
  532. $data = json_decode( trim( $response_body ) );
  533. return ( $data && is_object( $data ) ) ? $data : false;
  534. }
  535. /**
  536. * Parses an XML response body.
  537. *
  538. * @since 3.0.0
  539. *
  540. * @param string $response_body
  541. * @return object|false
  542. */
  543. private function _parse_xml( $response_body ) {
  544. if ( ! function_exists( 'libxml_disable_entity_loader' ) ) {
  545. return false;
  546. }
  547. if ( PHP_VERSION_ID < 80000 ) {
  548. // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading
  549. // is disabled by default, so this function is no longer needed to protect against XXE attacks.
  550. // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
  551. $loader = libxml_disable_entity_loader( true );
  552. }
  553. $errors = libxml_use_internal_errors( true );
  554. $return = $this->_parse_xml_body( $response_body );
  555. libxml_use_internal_errors( $errors );
  556. if ( PHP_VERSION_ID < 80000 && isset( $loader ) ) {
  557. // phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.libxml_disable_entity_loaderDeprecated
  558. libxml_disable_entity_loader( $loader );
  559. }
  560. return $return;
  561. }
  562. /**
  563. * Serves as a helper function for parsing an XML response body.
  564. *
  565. * @since 3.6.0
  566. *
  567. * @param string $response_body
  568. * @return stdClass|false
  569. */
  570. private function _parse_xml_body( $response_body ) {
  571. if ( ! function_exists( 'simplexml_import_dom' ) || ! class_exists( 'DOMDocument', false ) ) {
  572. return false;
  573. }
  574. $dom = new DOMDocument;
  575. $success = $dom->loadXML( $response_body );
  576. if ( ! $success ) {
  577. return false;
  578. }
  579. if ( isset( $dom->doctype ) ) {
  580. return false;
  581. }
  582. foreach ( $dom->childNodes as $child ) {
  583. if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType ) {
  584. return false;
  585. }
  586. }
  587. $xml = simplexml_import_dom( $dom );
  588. if ( ! $xml ) {
  589. return false;
  590. }
  591. $return = new stdClass;
  592. foreach ( $xml as $key => $value ) {
  593. $return->$key = (string) $value;
  594. }
  595. return $return;
  596. }
  597. /**
  598. * Converts a data object from WP_oEmbed::fetch() and returns the HTML.
  599. *
  600. * @since 2.9.0
  601. *
  602. * @param object $data A data object result from an oEmbed provider.
  603. * @param string $url The URL to the content that is desired to be embedded.
  604. * @return string|false The HTML needed to embed on success, false on failure.
  605. */
  606. public function data2html( $data, $url ) {
  607. if ( ! is_object( $data ) || empty( $data->type ) ) {
  608. return false;
  609. }
  610. $return = false;
  611. switch ( $data->type ) {
  612. case 'photo':
  613. if ( empty( $data->url ) || empty( $data->width ) || empty( $data->height ) ) {
  614. break;
  615. }
  616. if ( ! is_string( $data->url ) || ! is_numeric( $data->width ) || ! is_numeric( $data->height ) ) {
  617. break;
  618. }
  619. $title = ! empty( $data->title ) && is_string( $data->title ) ? $data->title : '';
  620. $return = '<a href="' . esc_url( $url ) . '"><img src="' . esc_url( $data->url ) . '" alt="' . esc_attr( $title ) . '" width="' . esc_attr( $data->width ) . '" height="' . esc_attr( $data->height ) . '" /></a>';
  621. break;
  622. case 'video':
  623. case 'rich':
  624. if ( ! empty( $data->html ) && is_string( $data->html ) ) {
  625. $return = $data->html;
  626. }
  627. break;
  628. case 'link':
  629. if ( ! empty( $data->title ) && is_string( $data->title ) ) {
  630. $return = '<a href="' . esc_url( $url ) . '">' . esc_html( $data->title ) . '</a>';
  631. }
  632. break;
  633. default:
  634. $return = false;
  635. }
  636. /**
  637. * Filters the returned oEmbed HTML.
  638. *
  639. * Use this filter to add support for custom data types, or to filter the result.
  640. *
  641. * @since 2.9.0
  642. *
  643. * @param string $return The returned oEmbed HTML.
  644. * @param object $data A data object result from an oEmbed provider.
  645. * @param string $url The URL of the content to be embedded.
  646. */
  647. return apply_filters( 'oembed_dataparse', $return, $data, $url );
  648. }
  649. /**
  650. * Strips any new lines from the HTML.
  651. *
  652. * @since 2.9.0 as strip_scribd_newlines()
  653. * @since 3.0.0
  654. *
  655. * @param string $html Existing HTML.
  656. * @param object $data Data object from WP_oEmbed::data2html()
  657. * @param string $url The original URL passed to oEmbed.
  658. * @return string Possibly modified $html
  659. */
  660. public function _strip_newlines( $html, $data, $url ) {
  661. if ( false === strpos( $html, "\n" ) ) {
  662. return $html;
  663. }
  664. $count = 1;
  665. $found = array();
  666. $token = '__PRE__';
  667. $search = array( "\t", "\n", "\r", ' ' );
  668. $replace = array( '__TAB__', '__NL__', '__CR__', '__SPACE__' );
  669. $tokenized = str_replace( $search, $replace, $html );
  670. preg_match_all( '#(<pre[^>]*>.+?</pre>)#i', $tokenized, $matches, PREG_SET_ORDER );
  671. foreach ( $matches as $i => $match ) {
  672. $tag_html = str_replace( $replace, $search, $match[0] );
  673. $tag_token = $token . $i;
  674. $found[ $tag_token ] = $tag_html;
  675. $html = str_replace( $tag_html, $tag_token, $html, $count );
  676. }
  677. $replaced = str_replace( $replace, $search, $html );
  678. $stripped = str_replace( array( "\r\n", "\n" ), '', $replaced );
  679. $pre = array_values( $found );
  680. $tokens = array_keys( $found );
  681. return str_replace( $tokens, $pre, $stripped );
  682. }
  683. }