/bp-core/classes/class-bp-core-oembed-extension.php

  1. <?php 
  2. /** 
  3. * Core component classes. 
  4. * 
  5. * @package BuddyPress 
  6. * @subpackage Core 
  7. * @since 2.6.0 
  8. */ 
  9.  
  10. // Exit if accessed directly. 
  11. defined( 'ABSPATH' ) || exit; 
  12.  
  13. /** 
  14. * API for responding and returning a custom oEmbed request. 
  15. * 
  16. * @since 2.6.0 
  17. */ 
  18. abstract class BP_Core_oEmbed_Extension { 
  19.  
  20. /** START PROPERTIES ****************************************************/ 
  21.  
  22. /** 
  23. * (required) The slug endpoint. 
  24. * 
  25. * Should be your component id. 
  26. * 
  27. * @since 2.6.0 
  28. * 
  29. * @var string 
  30. */ 
  31. public $slug_endpoint = ''; 
  32.  
  33. /** END PROPERTIES ******************************************************/ 
  34.  
  35. /** 
  36. * Constructor. 
  37. */ 
  38. final public function __construct() { 
  39. $this->setup_properties(); 
  40.  
  41. // Some rudimentary logic checking. 
  42. if ( empty( $this->slug_endpoint ) ) { 
  43. return; 
  44.  
  45. $this->setup_hooks(); 
  46. $this->custom_hooks(); 
  47.  
  48. /** REQUIRED METHODS ****************************************************/ 
  49.  
  50. /** 
  51. * Add content for your oEmbed response here. 
  52. * 
  53. * @since 2.6.0 
  54. */ 
  55. abstract protected function content(); 
  56.  
  57. /** 
  58. * Add a check for when you are on the page you want to oEmbed. 
  59. * 
  60. * You'll want to return a boolean here. eg. bp_is_single_activity(). 
  61. * 
  62. * @since 2.6.0 
  63. * 
  64. * @return bool 
  65. */ 
  66. abstract protected function is_page(); 
  67.  
  68. /** 
  69. * Validate the URL to see if it matches your item ID. 
  70. * 
  71. * @since 2.6.0 
  72. * 
  73. * @param string $url URL to validate. 
  74. * @return int Your item ID 
  75. */ 
  76. abstract protected function validate_url_to_item_id( $url ); 
  77.  
  78. /** 
  79. * Set the oEmbed response data. 
  80. * 
  81. * @since 2.6.0 
  82. * 
  83. * @param int $item_id Your item ID to do checks against. 
  84. * @return array Should contain 'content', 'title', 'author_url', 'author_name' as array 
  85. * keys. 'author_url' and 'author_name' is optional; the rest are required. 
  86. */ 
  87. abstract protected function set_oembed_response_data( $item_id ); 
  88.  
  89. /** 
  90. * Sets the fallback HTML for the oEmbed response. 
  91. * 
  92. * In a WordPress oEmbed item, the fallback HTML is a <blockquote>. This is 
  93. * usually hidden after the <iframe> is loaded. 
  94. * 
  95. * @since 2.6.0 
  96. * 
  97. * @param int $item_id Your item ID to do checks against. 
  98. * @return string Fallback HTML you want to output. 
  99. */ 
  100. abstract protected function set_fallback_html( $item_id ); 
  101.  
  102. /** OPTIONAL METHODS ****************************************************/ 
  103.  
  104. /** 
  105. * If your oEmbed endpoint requires additional arguments, set them here. 
  106. * 
  107. * @see register_rest_route() View the $args parameter for more info. 
  108. * 
  109. * @since 2.6.0 
  110. * 
  111. * @return array 
  112. */ 
  113. protected function set_route_args() { 
  114. return array(); 
  115.  
  116. /** 
  117. * Set the iframe title. 
  118. * 
  119. * If not set, this will fallback to WP's 'Embedded WordPress Post'. 
  120. * 
  121. * @since 2.6.0 
  122. * 
  123. * @param int $item_id The item ID to do checks for. 
  124. */ 
  125. protected function set_iframe_title( $item_id ) {} 
  126.  
  127. /** 
  128. * Do what you need to do here to initialize any custom hooks. 
  129. * 
  130. * @since 2.6.0 
  131. */ 
  132. protected function custom_hooks() {} 
  133.  
  134. /** 
  135. * Set permalink for oEmbed link discovery. 
  136. * 
  137. * This method will be called on the page we want to oEmbed. In most cases,  
  138. * you will not need to override this method. However, if you need to, do 
  139. * override in your extended class. 
  140. * 
  141. * @since 2.6.0 
  142. */ 
  143. protected function set_permalink() { 
  144. $url = bp_get_requested_url(); 
  145.  
  146. // Remove querystring from bp_get_requested_url(). 
  147. if ( false !== strpos( bp_get_requested_url(), '?' ) ) { 
  148. $url = substr( bp_get_requested_url(), 0, strpos( bp_get_requested_url(), '?' ) ); 
  149.  
  150. return $url; 
  151.  
  152. /** HELPERS *************************************************************/ 
  153.  
  154. /** 
  155. * Get the item ID when filtering the oEmbed HTML. 
  156. * 
  157. * Should only be used during the 'embed_html' hook. 
  158. * 
  159. * @since 2.6.0 
  160. */ 
  161. protected function get_item_id() { 
  162. return $this->is_page() ? $this->validate_url_to_item_id( $this->set_permalink() ) : buddypress()->{$this->slug_endpoint}->embedid_in_progress; 
  163.  
  164. /** SET UP **************************************************************/ 
  165.  
  166. /** 
  167. * Set up properties. 
  168. * 
  169. * @since 2.6.0 
  170. */ 
  171. protected function setup_properties() { 
  172. $this->slug_endpoint = sanitize_title( $this->slug_endpoint ); 
  173.  
  174. /** 
  175. * Hooks! We do the dirty work here, so you don't have to! :) 
  176. * 
  177. * More hooks are available in the setup_template_parts() method. 
  178. * 
  179. * @since 2.6.0 
  180. */ 
  181. protected function setup_hooks() { 
  182. add_action( 'rest_api_init', array( $this, 'register_route' ) ); 
  183. add_action( 'bp_embed_content', array( $this, 'inject_content' ) ); 
  184.  
  185. add_filter( 'embed_template', array( $this, 'setup_template_parts' ) ); 
  186. add_filter( 'post_embed_url', array( $this, 'filter_embed_url' ) ); 
  187. add_filter( 'embed_html', array( $this, 'filter_embed_html' ) ); 
  188. add_filter( 'oembed_discovery_links', array( $this, 'add_oembed_discovery_links' ) ); 
  189. add_filter( 'rest_pre_serve_request', array( $this, 'oembed_xml_request' ), 20, 4 ); 
  190.  
  191. /** HOOKS ***************************************************************/ 
  192.  
  193. /** 
  194. * Register the oEmbed REST API route. 
  195. * 
  196. * @since 2.6.0 
  197. */ 
  198. public function register_route() { 
  199. /** This filter is documented in wp-includes/class-wp-oembed-controller.php */ 
  200. $maxwidth = apply_filters( 'oembed_default_width', 600 ); 
  201.  
  202. // Required arguments. 
  203. $args = array( 
  204. 'url' => array( 
  205. 'required' => true,  
  206. 'sanitize_callback' => 'esc_url_raw',  
  207. ),  
  208. 'format' => array( 
  209. 'default' => 'json',  
  210. 'sanitize_callback' => 'wp_oembed_ensure_format',  
  211. ),  
  212. 'maxwidth' => array( 
  213. 'default' => $maxwidth,  
  214. 'sanitize_callback' => 'absint',  
  215. ); 
  216.  
  217. // Merge custom arguments here. 
  218. $args = $args + (array) $this->set_route_args(); 
  219.  
  220. register_rest_route( 'oembed/1.0', "/embed/{$this->slug_endpoint}", array( 
  221. array( 
  222. 'methods' => WP_REST_Server::READABLE,  
  223. 'callback' => array( $this, 'get_item' ),  
  224. 'args' => $args 
  225. ),  
  226. ) ); 
  227.  
  228. /** 
  229. * Set up custom embed template parts for BuddyPress use. 
  230. * 
  231. * @since 2.6.0 
  232. * 
  233. * @param string $template File path to current embed template. 
  234. * @return string 
  235. */ 
  236. public function setup_template_parts( $template ) { 
  237. // Determine if we're on our BP page. 
  238. if ( ! $this->is_page() || is_404() ) { 
  239. return $template; 
  240.  
  241. // Set up some BP-specific embed template overrides. 
  242. add_action( 'get_template_part_embed', array( $this, 'content_buffer_start' ), -999, 2 ); 
  243. add_action( 'get_footer', array( $this, 'content_buffer_end' ), -999 ); 
  244.  
  245. // Return the original WP embed template. 
  246. return $template; 
  247.  
  248. /** 
  249. * Start object buffer. 
  250. * 
  251. * We're going to override WP's get_template_part( 'embed, 'content' ) call 
  252. * and inject our own template for BuddyPress use. 
  253. * 
  254. * @since 2.6.0 
  255. * 
  256. * @param string $slug Template slug. 
  257. * @param string $name Template name. 
  258. */ 
  259. public function content_buffer_start( $slug, $name ) { 
  260. if ( 'embed' !== $slug || 'content' !== $name ) { 
  261. return; 
  262.  
  263. // Start the buffer to wipe out get_template_part( 'embed, 'content' ). 
  264. ob_start(); 
  265.  
  266. /** 
  267. * End object buffer. 
  268. * 
  269. * We're going to override WP's get_template_part( 'embed, 'content' ) call 
  270. * and inject our own template for BuddyPress use. 
  271. * 
  272. * @since 2.6.0 
  273. * 
  274. * @param string $name Template name. 
  275. */ 
  276. public function content_buffer_end( $name ) { 
  277. if ( 'embed' !== $name || is_404() ) { 
  278. return; 
  279.  
  280. // Wipe out get_template_part( 'embed, 'content' ). 
  281. ob_end_clean(); 
  282.  
  283. // Start our custom BuddyPress embed template! 
  284. echo '<div '; 
  285. post_class( 'wp-embed' ); 
  286. echo '>'; 
  287.  
  288. // Template part for our embed header. 
  289. bp_get_asset_template_part( 'embeds/header', bp_current_component() ); 
  290.  
  291. /** 
  292. * Inject BuddyPress embed content on this hook. 
  293. * 
  294. * You shouldn't really need to use this if you extend the 
  295. * {@link BP_oEmbed_Component} class. 
  296. * 
  297. * @since 2.6.0 
  298. */ 
  299. do_action( 'bp_embed_content' ); 
  300.  
  301. // Template part for our embed footer. 
  302. bp_get_asset_template_part( 'embeds/footer', bp_current_component() ); 
  303.  
  304. echo '</div>'; 
  305.  
  306. /** 
  307. * Adds oEmbed discovery links on single activity pages. 
  308. * 
  309. * @since 2.6.0 
  310. * 
  311. * @param string $retval Current discovery links. 
  312. * @return string 
  313. */ 
  314. public function add_oembed_discovery_links( $retval ) { 
  315. if ( ! $this->is_page() ) { 
  316. return $retval; 
  317.  
  318. $permalink = $this->set_permalink(); 
  319. if ( empty( $permalink ) ) { 
  320. return $retval; 
  321.  
  322. add_filter( 'rest_url' , array( $this, 'filter_rest_url' ) ); 
  323.  
  324. $retval = '<link rel="alternate" type="application/json+oembed" href="' . esc_url( get_oembed_endpoint_url( $permalink ) ) . '" />' . "\n"; 
  325.  
  326. if ( class_exists( 'SimpleXMLElement' ) ) { 
  327. $retval .= '<link rel="alternate" type="text/xml+oembed" href="' . esc_url( get_oembed_endpoint_url( $permalink, 'xml' ) ) . '" />' . "\n"; 
  328.  
  329. remove_filter( 'rest_url' , array( $this, 'filter_rest_url' ) ); 
  330.  
  331. return $retval; 
  332.  
  333. /** 
  334. * Fetch our oEmbed response data to return. 
  335. * 
  336. * A simplified version of {@link get_oembed_response_data()}. 
  337. * 
  338. * @since 2.6.0 
  339. * 
  340. * @link http://oembed.com/ View the 'Response parameters' section for more details. 
  341. * 
  342. * @param array $item Custom oEmbed response data. 
  343. * @param int $width The requested width. 
  344. * @return array 
  345. */ 
  346. protected function get_oembed_response_data( $item, $width ) { 
  347. $data = wp_parse_args( $item, array( 
  348. 'version' => '1.0',  
  349. 'provider_name' => get_bloginfo( 'name' ),  
  350. 'provider_url' => get_home_url(),  
  351. 'author_name' => get_bloginfo( 'name' ),  
  352. 'author_url' => get_home_url(),  
  353. 'title' => ucfirst( $this->slug_endpoint ),  
  354. 'type' => 'rich',  
  355. ) ); 
  356.  
  357. /** This filter is documented in /wp-includes/embed.php */ 
  358. $min_max_width = apply_filters( 'oembed_min_max_width', array( 
  359. 'min' => 200,  
  360. 'max' => 600 
  361. ) ); 
  362.  
  363. $width = min( max( $min_max_width['min'], $width ), $min_max_width['max'] ); 
  364. $height = max( ceil( $width / 16 * 9 ), 200 ); 
  365.  
  366. $data['width'] = absint( $width ); 
  367. $data['height'] = absint( $height ); 
  368.  
  369. // Set 'html' parameter. 
  370. if ( 'video' === $data['type'] || 'rich' === $data['type'] ) { 
  371. // Fake a WP post so we can use get_post_embed_html(). 
  372. $post = new stdClass; 
  373. $post->post_content = $data['content']; 
  374. $post->post_title = $data['title']; 
  375.  
  376. $data['html'] = get_post_embed_html( $data['width'], $data['height'], $post ); 
  377.  
  378. // Remove temporary parameters. 
  379. unset( $data['content'] ); 
  380.  
  381. return $data; 
  382.  
  383. /** 
  384. * Callback for the API endpoint. 
  385. * 
  386. * Returns the JSON object for the item. 
  387. * 
  388. * @since 2.6.0 
  389. * 
  390. * @param WP_REST_Request $request Full data about the request. 
  391. * @return WP_Error|array oEmbed response data or WP_Error on failure. 
  392. */ 
  393. public function get_item( $request ) { 
  394. $url = $request['url']; 
  395.  
  396. $data = false; 
  397.  
  398. $item_id = (int) $this->validate_url_to_item_id( $url ); 
  399.  
  400. if ( ! empty( $item_id ) ) { 
  401. // Add markers to tell that we're embedding a single activity. 
  402. // This is needed for various oEmbed response data filtering. 
  403. if ( empty( buddypress()->{$this->slug_endpoint} ) ) { 
  404. buddypress()->{$this->slug_endpoint} = new stdClass; 
  405. buddypress()->{$this->slug_endpoint}->embedurl_in_progress = $url; 
  406. buddypress()->{$this->slug_endpoint}->embedid_in_progress = $item_id; 
  407.  
  408. // Save custom route args as well. 
  409. $custom_args = array_keys( (array) $this->set_route_args() ); 
  410. if ( ! empty( $custom_args ) ) { 
  411. buddypress()->{$this->slug_endpoint}->embedargs_in_progress = array(); 
  412.  
  413. foreach( $custom_args as $arg ) { 
  414. if ( isset( $request[ $arg ] ) ) { 
  415. buddypress()->{$this->slug_endpoint}->embedargs_in_progress[ $arg ] = $request[ $arg ]; 
  416.  
  417. // Grab custom oEmbed response data. 
  418. $item = $this->set_oembed_response_data( $item_id ); 
  419.  
  420. // Set oEmbed response data. 
  421. $data = $this->get_oembed_response_data( $item, $request['maxwidth'] ); 
  422.  
  423. if ( ! $data ) { 
  424. return new WP_Error( 'oembed_invalid_url', get_status_header_desc( 404 ), array( 'status' => 404 ) ); 
  425.  
  426. return $data; 
  427.  
  428. /** 
  429. * If oEmbed request wants XML, return XML instead of JSON. 
  430. * 
  431. * Basically a copy of {@link _oembed_rest_pre_serve_request()}. Unfortunate 
  432. * that we have to duplicate this just for a URL check. 
  433. * 
  434. * @since 2.6.0 
  435. * 
  436. * @param bool $served Whether the request has already been served. 
  437. * @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response. 
  438. * @param WP_REST_Request $request Request used to generate the response. 
  439. * @param WP_REST_Server $server Server instance. 
  440. * @return bool 
  441. */ 
  442. public function oembed_xml_request( $served, $result, $request, $server ) { 
  443. $params = $request->get_params(); 
  444.  
  445. if ( ! isset( $params['format'] ) || 'xml' !== $params['format'] ) { 
  446. return $served; 
  447.  
  448. // Validate URL against our oEmbed endpoint. If not valid, bail. 
  449. // This is our mod to _oembed_rest_pre_serve_request(). 
  450. $query_params = $request->get_query_params(); 
  451. if ( false === $this->validate_url_to_item_id( $query_params['url'] ) ) { 
  452. return $served; 
  453.  
  454. // Embed links inside the request. 
  455. $data = $server->response_to_data( $result, false ); 
  456.  
  457. if ( ! class_exists( 'SimpleXMLElement' ) ) { 
  458. status_header( 501 ); 
  459. die( get_status_header_desc( 501 ) ); 
  460.  
  461. $result = _oembed_create_xml( $data ); 
  462.  
  463. // Bail if there's no XML. 
  464. if ( ! $result ) { 
  465. status_header( 501 ); 
  466. return get_status_header_desc( 501 ); 
  467.  
  468. if ( ! headers_sent() ) { 
  469. $server->send_header( 'Content-Type', 'text/xml; charset=' . get_option( 'blog_charset' ) ); 
  470.  
  471. echo $result; 
  472.  
  473. return true; 
  474.  
  475. /** 
  476. * Pass our BuddyPress activity permalink for embedding. 
  477. * 
  478. * @since 2.6.0 
  479. * 
  480. * @see bp_activity_embed_rest_route_callback() 
  481. * 
  482. * @param string $retval Current embed URL. 
  483. * @return string 
  484. */ 
  485. public function filter_embed_url( $retval ) { 
  486. if ( false === isset( buddypress()->{$this->slug_endpoint}->embedurl_in_progress ) && ! $this->is_page() ) { 
  487. return $retval; 
  488.  
  489. $url = $this->is_page() ? $this->set_permalink() : buddypress()->{$this->slug_endpoint}->embedurl_in_progress; 
  490. $url = trailingslashit( $url ); 
  491.  
  492. // This is for the 'WordPress Embed' block 
  493. // @see bp_activity_embed_comments_button(). 
  494. if ( 'the_permalink' !== current_filter() ) { 
  495. $url = add_query_arg( 'embed', 'true', trailingslashit( $url ) ); 
  496.  
  497. // Add custom route args to iframe. 
  498. if ( ! empty( buddypress()->{$this->slug_endpoint}->embedargs_in_progress ) ) { 
  499. foreach( buddypress()->{$this->slug_endpoint}->embedargs_in_progress as $key => $value ) { 
  500. $url = add_query_arg( $key, $value, $url ); 
  501.  
  502. return $url; 
  503.  
  504. /** 
  505. * Filters the embed HTML for our BP oEmbed endpoint. 
  506. * 
  507. * @since 2.6.0 
  508. * 
  509. * @param string $retval Current embed HTML. 
  510. * @return string 
  511. */ 
  512. public function filter_embed_html( $retval ) { 
  513. if ( false === isset( buddypress()->{$this->slug_endpoint}->embedurl_in_progress ) && ! $this->is_page() ) { 
  514. return $retval; 
  515.  
  516. $url = $this->set_permalink(); 
  517.  
  518. $item_id = $this->is_page() ? $this->validate_url_to_item_id( $url ) : buddypress()->{$this->slug_endpoint}->embedid_in_progress; 
  519.  
  520. // Change 'Embedded WordPress Post' to custom title. 
  521. $custom_title = $this->set_iframe_title( $item_id ); 
  522. if ( ! empty( $custom_title ) ) { 
  523. $title_pos = strpos( $retval, 'title=' ) + 7; 
  524. $title_end_pos = strpos( $retval, '"', $title_pos ); 
  525.  
  526. $retval = substr_replace( $retval, esc_attr( $custom_title ), $title_pos, $title_end_pos - $title_pos ); 
  527.  
  528. // Add 'max-width' CSS attribute to IFRAME. 
  529. // This will make our oEmbeds responsive. 
  530. if ( false === strpos( $retval, 'style="max-width' ) ) { 
  531. $retval = str_replace( '<iframe', '<iframe style="max-width:100%"', $retval ); 
  532.  
  533. // Remove default <blockquote>. 
  534. $retval = substr( $retval, strpos( $retval, '</blockquote>' ) + 13 ); 
  535.  
  536. // Set up new fallback HTML 
  537. // @todo Maybe use KSES? 
  538. $fallback_html = $this->set_fallback_html( $item_id ); 
  539.  
  540. /** 
  541. * Dynamic filter to return BP oEmbed HTML. 
  542. * 
  543. * @since 2.6.0 
  544. * 
  545. * @var string $retval 
  546. */ 
  547. return apply_filters( "bp_{$this->slug_endpoint}_embed_html", $fallback_html . $retval ); 
  548.  
  549. /** 
  550. * Append our custom slug endpoint to oEmbed endpoint URL. 
  551. * 
  552. * Meant to be used as a filter on 'rest_url' before any call to 
  553. * {@link get_oembed_endpoint_url()} is used. 
  554. * 
  555. * @since 2.6.0 
  556. * 
  557. * @see add_oembed_discovery_links() 
  558. * 
  559. * @param string $retval Current oEmbed endpoint URL. 
  560. * @return string 
  561. */ 
  562. public function filter_rest_url( $retval = '' ) { 
  563. return $retval . "/{$this->slug_endpoint}"; 
  564.  
  565. /** 
  566. * Inject content into the embed template. 
  567. * 
  568. * @since 2.6.0 
  569. */ 
  570. public function inject_content() { 
  571. if ( ! $this->is_page() ) { 
  572. return; 
  573.  
  574. $this->content(); 
.