Jetpack_RelatedPosts

The Jetpack by WordPress.com Jetpack RelatedPosts class.

Defined (1)

The class is defined in the following location(s).

/modules/related-posts/jetpack-related-posts.php  
  1. class Jetpack_RelatedPosts { 
  2. const VERSION = '20150408'; 
  3. const SHORTCODE = 'jetpack-related-posts'; 
  4.  
  5. /** 
  6. * Creates and returns a static instance of Jetpack_RelatedPosts. 
  7. * @return Jetpack_RelatedPosts 
  8. */ 
  9. public static function init() { 
  10. static $instance = NULL; 
  11.  
  12. if ( ! $instance ) { 
  13. if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init' ) ) { 
  14. $instance = WPCOM_RelatedPosts::init(); 
  15. } else { 
  16. $instance = new Jetpack_RelatedPosts( 
  17. get_current_blog_id(),  
  18. Jetpack_Options::get_option( 'id' ) 
  19. ); 
  20.  
  21. return $instance; 
  22.  
  23. /** 
  24. * Creates and returns a static instance of Jetpack_RelatedPosts_Raw. 
  25. * @return Jetpack_RelatedPosts 
  26. */ 
  27. public static function init_raw() { 
  28. static $instance = NULL; 
  29.  
  30. if ( ! $instance ) { 
  31. if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init_raw' ) ) { 
  32. $instance = WPCOM_RelatedPosts::init_raw(); 
  33. } else { 
  34. $instance = new Jetpack_RelatedPosts_Raw( 
  35. get_current_blog_id(),  
  36. Jetpack_Options::get_option( 'id' ) 
  37. ); 
  38.  
  39. return $instance; 
  40.  
  41. protected $_blog_id_local; 
  42. protected $_blog_id_wpcom; 
  43. protected $_options; 
  44. protected $_allow_feature_toggle; 
  45. protected $_blog_charset; 
  46. protected $_convert_charset; 
  47. protected $_previous_post_id; 
  48. protected $_found_shortcode = false; 
  49.  
  50. /** 
  51. * Constructor for Jetpack_RelatedPosts. 
  52. * @param int $blog_id_local 
  53. * @param int $blog_id_wpcom 
  54. * @uses get_option, add_action, apply_filters 
  55. * @return null 
  56. */ 
  57. public function __construct( $blog_id_local, $blog_id_wpcom ) { 
  58. $this->_blog_id_local = $blog_id_local; 
  59. $this->_blog_id_wpcom = $blog_id_wpcom; 
  60. $this->_blog_charset = get_option( 'blog_charset' ); 
  61. $this->_convert_charset = ( function_exists( 'iconv' ) && ! preg_match( '/^utf\-?8$/i', $this->_blog_charset ) ); 
  62.  
  63. add_action( 'admin_init', array( $this, 'action_admin_init' ) ); 
  64. add_action( 'wp', array( $this, 'action_frontend_init' ) ); 
  65.  
  66. /** 
  67. * ================= 
  68. * ACTIONS & FILTERS 
  69. * ================= 
  70. */ 
  71.  
  72. /** 
  73. * Add a checkbox field to Settings > Reading for enabling related posts. 
  74. * @action admin_init 
  75. * @uses add_settings_field, __, register_setting, add_action 
  76. * @return null 
  77. */ 
  78. public function action_admin_init() { 
  79.  
  80. // Add the setting field [jetpack_relatedposts] and place it in Settings > Reading 
  81. add_settings_field( 'jetpack_relatedposts', '<span id="jetpack_relatedposts">' . __( 'Related posts', 'jetpack' ) . '</span>', array( $this, 'print_setting_html' ), 'reading' ); 
  82. register_setting( 'reading', 'jetpack_relatedposts', array( $this, 'parse_options' ) ); 
  83. add_action('admin_head', array( $this, 'print_setting_head' ) ); 
  84.  
  85. if( 'options-reading.php' == $GLOBALS['pagenow'] ) { 
  86. // Enqueue style for live preview on the reading settings page 
  87. $this->_enqueue_assets( false, true ); 
  88.  
  89. /** 
  90. * Load related posts assets if it's a elegiable frontend page or execute search and return JSON if it's an endpoint request. 
  91. * @global $_GET 
  92. * @action wp 
  93. * @uses add_shortcode, get_the_ID 
  94. * @returns null 
  95. */ 
  96. public function action_frontend_init() { 
  97. // Add a shortcode handler that outputs nothing, this gets overridden later if we can display related content 
  98. add_shortcode( self::SHORTCODE, array( $this, 'get_target_html_unsupported' ) ); 
  99.  
  100. if ( ! $this->_enabled_for_request() ) 
  101. return; 
  102.  
  103. if ( isset( $_GET['relatedposts'] ) ) { 
  104. $excludes = array(); 
  105. if ( isset( $_GET['relatedposts_exclude'] ) ) { 
  106. $excludes = explode( ', ', $_GET['relatedposts_exclude'] ); 
  107.  
  108. $this->_action_frontend_init_ajax( $excludes ); 
  109. } else { 
  110. if ( isset( $_GET['relatedposts_hit'] ) ) { 
  111. $this->_log_click( $_GET['relatedposts_origin'], get_the_ID(), $_GET['relatedposts_position'] ); 
  112. $this->_previous_post_id = (int) $_GET['relatedposts_origin']; 
  113.  
  114. $this->_action_frontend_init_page(); 
  115.  
  116.  
  117. /** 
  118. * Adds a target to the post content to load related posts into if a shortcode for it did not already exist. 
  119. * @filter the_content 
  120. * @param string $content 
  121. * @returns string 
  122. */ 
  123. public function filter_add_target_to_dom( $content ) { 
  124. if ( !$this->_found_shortcode ) { 
  125. $content .= "\n" . $this->get_target_html(); 
  126.  
  127. return $content; 
  128.  
  129. /** 
  130. * Looks for our shortcode on the unfiltered content, this has to execute early. 
  131. * @filter the_content 
  132. * @param string $content 
  133. * @uses has_shortcode 
  134. * @returns string 
  135. */ 
  136. public function test_for_shortcode( $content ) { 
  137. $this->_found_shortcode = has_shortcode( $content, self::SHORTCODE ); 
  138.  
  139. return $content; 
  140.  
  141. /** 
  142. * Returns the HTML for the related posts section. 
  143. * @uses esc_html__, apply_filters 
  144. * @returns string 
  145. */ 
  146. public function get_target_html() { 
  147. $options = $this->get_options(); 
  148.  
  149. if ( $options['show_headline'] ) { 
  150. $headline = sprintf( 
  151. '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',  
  152. esc_html__( 'Related', 'jetpack' ) 
  153. ); 
  154. } else { 
  155. $headline = ''; 
  156.  
  157. /** 
  158. * Filter the Related Posts headline. 
  159. * @module related-posts 
  160. * @since 3.0.0 
  161. * @param string $headline Related Posts heading. 
  162. */ 
  163. $headline = apply_filters( 'jetpack_relatedposts_filter_headline', $headline ); 
  164.  
  165. if ( $this->_previous_post_id ) { 
  166. $exclude = "data-exclude='{$this->_previous_post_id}'"; 
  167. } else { 
  168. $exclude = ""; 
  169.  
  170. return <<<EOT 
  171. <div id='jp-relatedposts' class='jp-relatedposts' $exclude> 
  172. $headline 
  173. </div> 
  174. EOT; 
  175.  
  176. /** 
  177. * Returns the HTML for the related posts section if it's running in the loop or other instances where we don't support related posts. 
  178. * @returns string 
  179. */ 
  180. public function get_target_html_unsupported() { 
  181. return "\n\n<!-- Jetpack Related Posts is not supported in this context. -->\n\n"; 
  182.  
  183. /** 
  184. * ======================== 
  185. * PUBLIC UTILITY FUNCTIONS 
  186. * ======================== 
  187. */ 
  188.  
  189. /** 
  190. * Gets options set for Jetpack_RelatedPosts and merge with defaults. 
  191. * @uses Jetpack_Options::get_option, apply_filters 
  192. * @return array 
  193. */ 
  194. public function get_options() { 
  195. if ( null === $this->_options ) { 
  196. $this->_options = Jetpack_Options::get_option( 'relatedposts' ); 
  197. if ( ! is_array( $this->_options ) ) 
  198. $this->_options = array(); 
  199. if ( ! isset( $this->_options['enabled'] ) ) 
  200. $this->_options['enabled'] = true; 
  201. if ( ! isset( $this->_options['show_headline'] ) ) 
  202. $this->_options['show_headline'] = true; 
  203. if ( ! isset( $this->_options['show_thumbnails'] ) ) 
  204. $this->_options['show_thumbnails'] = false; 
  205. if ( empty( $this->_options['size'] ) || (int)$this->_options['size'] < 1 ) 
  206. $this->_options['size'] = 3; 
  207.  
  208. /** 
  209. * Filter Related Posts basic options. 
  210. * @module related-posts 
  211. * @since 2.8.0 
  212. * @param array $this->_options Array of basic Related Posts options. 
  213. */ 
  214. $this->_options = apply_filters( 'jetpack_relatedposts_filter_options', $this->_options ); 
  215.  
  216. return $this->_options; 
  217.  
  218. /** 
  219. * Parses input and returnes normalized options array. 
  220. * @param array $input 
  221. * @uses self::get_options 
  222. * @return array 
  223. */ 
  224. public function parse_options( $input ) { 
  225. $current = $this->get_options(); 
  226.  
  227. if ( !is_array( $input ) ) 
  228. $input = array(); 
  229.  
  230. if ( isset( $input['enabled'] ) && '1' == $input['enabled'] ) { 
  231. $current['enabled'] = true; 
  232. $current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] ); 
  233. $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] ); 
  234. } else { 
  235. $current['enabled'] = false; 
  236.  
  237. if ( isset( $input['size'] ) && (int)$input['size'] > 0 ) 
  238. $current['size'] = (int)$input['size']; 
  239. else 
  240. $current['size'] = null; 
  241.  
  242. return $current; 
  243.  
  244. /** 
  245. * HTML for admin settings page. 
  246. * @uses self::get_options, checked, esc_html__ 
  247. * @returns null 
  248. */ 
  249. public function print_setting_html() { 
  250. $options = $this->get_options(); 
  251.  
  252. $ui_settings_template = <<<EOT 
  253. <ul id="settings-reading-relatedposts-customize"> 
  254. <li> 
  255. <label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label> 
  256. </li> 
  257. <li> 
  258. <label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label> 
  259. </li> 
  260. </ul> 
  261. <div id='settings-reading-relatedposts-preview'> 
  262. %s 
  263. <div id="jp-relatedposts" class="jp-relatedposts"></div> 
  264. </div> 
  265. EOT; 
  266. $ui_settings = sprintf( 
  267. $ui_settings_template,  
  268. checked( $options['show_headline'], true, false ),  
  269. esc_html__( 'Show a "Related" header to more clearly separate the related section from posts', 'jetpack' ),  
  270. checked( $options['show_thumbnails'], true, false ),  
  271. esc_html__( 'Use a large and visually striking layout', 'jetpack' ),  
  272. esc_html__( 'Preview:', 'jetpack' ) 
  273. ); 
  274.  
  275. if ( !$this->_allow_feature_toggle() ) { 
  276. $template = <<<EOT 
  277. <input type="hidden" name="jetpack_relatedposts[enabled]" value="1" /> 
  278. %s 
  279. EOT; 
  280. printf( 
  281. $template,  
  282. $ui_settings 
  283. ); 
  284. } else { 
  285. $template = <<<EOT 
  286. <ul id="settings-reading-relatedposts"> 
  287. <li> 
  288. <label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label> 
  289. </li> 
  290. <li> 
  291. <label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label> 
  292. %s 
  293. </li> 
  294. </ul> 
  295. EOT; 
  296. printf( 
  297. $template,  
  298. checked( $options['enabled'], false, false ),  
  299. esc_html__( 'Hide related content after posts', 'jetpack' ),  
  300. checked( $options['enabled'], true, false ),  
  301. esc_html__( 'Show related content after posts', 'jetpack' ),  
  302. $ui_settings 
  303. ); 
  304.  
  305. /** 
  306. * Head JS/CSS for admin settings page. 
  307. * @uses esc_html__ 
  308. * @returns null 
  309. */ 
  310. public function print_setting_head() { 
  311.  
  312. // only dislay the Related Posts JavaScript on the Reading Settings Admin Page 
  313. $current_screen = get_current_screen(); 
  314. if( 'options-reading' != $current_screen->id ) 
  315. return; 
  316.  
  317. $related_headline = sprintf( 
  318. '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',  
  319. esc_html__( 'Related', 'jetpack' ) 
  320. ); 
  321.  
  322. $href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"'; 
  323. $related_with_images = <<<EOT 
  324. <div class="jp-relatedposts-items jp-relatedposts-items-visual"> 
  325. <div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image"> 
  326. <a $href_params> 
  327. <img class="jp-relatedposts-post-img" src="http://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&h=200&crop=1" width="350" alt="Big iPhone/iPad Update Now Available" scale="0"> 
  328. </a> 
  329. <h4 class="jp-relatedposts-post-title"> 
  330. <a $href_params>Big iPhone/iPad Update Now Available</a> 
  331. </h4> 
  332. <p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p> 
  333. <p class="jp-relatedposts-post-context">In "Mobile"</p> 
  334. </div> 
  335. <div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image"> 
  336. <a $href_params> 
  337. <img class="jp-relatedposts-post-img" src="http://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&h=200&crop=1" width="350" alt="The WordPress for Android App Gets a Big Facelift" scale="0"> 
  338. </a> 
  339. <h4 class="jp-relatedposts-post-title"> 
  340. <a $href_params>The WordPress for Android App Gets a Big Facelift</a> 
  341. </h4> 
  342. <p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p> 
  343. <p class="jp-relatedposts-post-context">In "Mobile"</p> 
  344. </div> 
  345. <div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image"> 
  346. <a $href_params> 
  347. <img class="jp-relatedposts-post-img" src="http://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&h=200&crop=1" width="350" alt="Upgrade Focus: VideoPress For Weddings" scale="0"> 
  348. </a> 
  349. <h4 class="jp-relatedposts-post-title"> 
  350. <a $href_params>Upgrade Focus: VideoPress For Weddings</a> 
  351. </h4> 
  352. <p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p> 
  353. <p class="jp-relatedposts-post-context">In "Upgrade"</p> 
  354. </div> 
  355. </div> 
  356. EOT; 
  357. $related_with_images = str_replace( "\n", '', $related_with_images ); 
  358. $related_without_images = <<<EOT 
  359. <div class="jp-relatedposts-items jp-relatedposts-items-minimal"> 
  360. <p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image"> 
  361. <span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span> 
  362. <span class="jp-relatedposts-post-context">In "Mobile"</span> 
  363. </p> 
  364. <p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image"> 
  365. <span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span> 
  366. <span class="jp-relatedposts-post-context">In "Mobile"</span> 
  367. </p> 
  368. <p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image"> 
  369. <span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span> 
  370. <span class="jp-relatedposts-post-context">In "Upgrade"</span> 
  371. </p> 
  372. </div> 
  373. EOT; 
  374. $related_without_images = str_replace( "\n", '', $related_without_images ); 
  375.  
  376. if ( $this->_allow_feature_toggle() ) { 
  377. $extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }'; 
  378. } else { 
  379. $extra_css = ''; 
  380.  
  381. echo <<<EOT 
  382. <style type="text/css"> 
  383. #settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); } 
  384. #settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; } 
  385. $extra_css 
  386. </style> 
  387. <script type="text/javascript"> 
  388. jQuery( document ).ready( function($) { 
  389. var update_ui = function() { 
  390. var is_enabled = true; 
  391. if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) { 
  392. if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) { 
  393. is_enabled = false; 
  394. if ( is_enabled ) { 
  395. $( '#settings-reading-relatedposts-customize' ) 
  396. .removeClass( 'disabled' ) 
  397. .find( 'input' ) 
  398. .attr( 'disabled', false ); 
  399. $( '#settings-reading-relatedposts-preview' ) 
  400. .removeClass( 'disabled' ); 
  401. } else { 
  402. $( '#settings-reading-relatedposts-customize' ) 
  403. .addClass( 'disabled' ) 
  404. .find( 'input' ) 
  405. .attr( 'disabled', true ); 
  406. $( '#settings-reading-relatedposts-preview' ) 
  407. .addClass( 'disabled' ); 
  408. }; 
  409.  
  410. var update_preview = function() { 
  411. var html = ''; 
  412. if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).size() ) { 
  413. html += '$related_headline'; 
  414. if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).size() ) { 
  415. html += '$related_with_images'; 
  416. } else { 
  417. html += '$related_without_images'; 
  418. $( '#settings-reading-relatedposts-preview .jp-relatedposts' ) 
  419. .html( html ) 
  420. .show(); 
  421. }; 
  422.  
  423. // Update on load 
  424. update_preview(); 
  425. update_ui(); 
  426.  
  427. // Update on change 
  428. $( '#settings-reading-relatedposts-customize input' ) 
  429. .change( update_preview ); 
  430. $( '#settings-reading-relatedposts' ) 
  431. .find( 'input.tog' ) 
  432. .change( update_ui ); 
  433. }); 
  434. </script> 
  435. EOT; 
  436.  
  437. /** 
  438. * Gets an array of related posts that match the given post_id. 
  439. * @param int $post_id 
  440. * @param array $args - params to use when building ElasticSearch filters to narrow down the search domain. 
  441. * @uses self::get_options, get_post_type, wp_parse_args, apply_filters 
  442. * @return array 
  443. */ 
  444. public function get_for_post_id( $post_id, array $args ) { 
  445. $options = $this->get_options(); 
  446.  
  447. if ( ! empty( $args['size'] ) ) 
  448. $options['size'] = $args['size']; 
  449.  
  450. if ( ! $options['enabled'] || 0 == (int)$post_id || empty( $options['size'] ) ) 
  451. return array(); 
  452.  
  453. $defaults = array( 
  454. 'size' => (int)$options['size'],  
  455. 'post_type' => get_post_type( $post_id ),  
  456. 'post_formats' => array(),  
  457. 'has_terms' => array(),  
  458. 'date_range' => array(),  
  459. 'exclude_post_ids' => array(),  
  460. ); 
  461. $args = wp_parse_args( $args, $defaults ); 
  462. /** 
  463. * Filter the arguments used to retrieve a list of Related Posts. 
  464. * @module related-posts 
  465. * @since 2.8.0 
  466. * @param array $args Array of options to retrieve Related Posts. 
  467. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  468. */ 
  469. $args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id ); 
  470.  
  471. $filters = $this->_get_es_filters_from_args( $post_id, $args ); 
  472. /** 
  473. * Filter ElasticSearch options used to calculate Related Posts. 
  474. * @module related-posts 
  475. * @since 2.8.0 
  476. * @param array $filters Array of ElasticSearch filters based on the post_id and args. 
  477. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  478. */ 
  479. $filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id ); 
  480.  
  481. $results = $this->_get_related_posts( $post_id, $args['size'], $filters ); 
  482. /** 
  483. * Filter the array of related posts matched by ElasticSearch. 
  484. * @module related-posts 
  485. * @since 2.8.0 
  486. * @param array $results Array of related posts matched by ElasticSearch. 
  487. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  488. */ 
  489. return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id ); 
  490.  
  491. /** 
  492. * ========================= 
  493. * PRIVATE UTILITY FUNCTIONS 
  494. * ========================= 
  495. */ 
  496.  
  497. /** 
  498. * Creates an array of ElasticSearch filters based on the post_id and args. 
  499. * @param int $post_id 
  500. * @param array $args 
  501. * @uses apply_filters, get_post_types, get_post_format_strings 
  502. * @return array 
  503. */ 
  504. protected function _get_es_filters_from_args( $post_id, array $args ) { 
  505. $filters = array(); 
  506.  
  507. /** 
  508. * Filter the terms used to search for Related Posts. 
  509. * @module related-posts 
  510. * @since 2.8.0 
  511. * @param array $args['has_terms'] Array of terms associated to the Related Posts. 
  512. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  513. */ 
  514. $args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id ); 
  515. if ( ! empty( $args['has_terms'] ) ) { 
  516. foreach( (array)$args['has_terms'] as $term ) { 
  517. if ( mb_strlen( $term->taxonomy ) ) { 
  518. switch ( $term->taxonomy ) { 
  519. case 'post_tag': 
  520. $tax_fld = 'tag.slug'; 
  521. break; 
  522. case 'category': 
  523. $tax_fld = 'category.slug'; 
  524. break; 
  525. default: 
  526. $tax_fld = 'taxonomy.' . $term->taxonomy . '.slug'; 
  527. break; 
  528. $filters[] = array( 'term' => array( $tax_fld => $term->slug ) ); 
  529.  
  530. /** 
  531. * Filter the Post Types where we search Related Posts. 
  532. * @module related-posts 
  533. * @since 2.8.0 
  534. * @param array $args['post_type'] Array of Post Types. 
  535. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  536. */ 
  537. $args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id ); 
  538. $valid_post_types = get_post_types(); 
  539. if ( is_array( $args['post_type'] ) ) { 
  540. $sanitized_post_types = array(); 
  541. foreach ( $args['post_type'] as $pt ) { 
  542. if ( in_array( $pt, $valid_post_types ) ) 
  543. $sanitized_post_types[] = $pt; 
  544. if ( ! empty( $sanitized_post_types ) ) 
  545. $filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) ); 
  546. } else if ( in_array( $args['post_type'], $valid_post_types ) && 'all' != $args['post_type'] ) { 
  547. $filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) ); 
  548.  
  549. /** 
  550. * Filter the Post Formats where we search Related Posts. 
  551. * @module related-posts 
  552. * @since 3.3.0 
  553. * @param array $args['post_formats'] Array of Post Formats. 
  554. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  555. */ 
  556. $args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id ); 
  557. $valid_post_formats = get_post_format_strings(); 
  558. $sanitized_post_formats = array(); 
  559. foreach ( $args['post_formats'] as $pf ) { 
  560. if ( array_key_exists( $pf, $valid_post_formats ) ) { 
  561. $sanitized_post_formats[] = $pf; 
  562. if ( ! empty( $sanitized_post_formats ) ) { 
  563. $filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) ); 
  564.  
  565. /** 
  566. * Filter the date range used to search Related Posts. 
  567. * @module related-posts 
  568. * @since 2.8.0 
  569. * @param array $args['date_range'] Array of a month interval where we search Related Posts. 
  570. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  571. */ 
  572. $args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id ); 
  573. if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) { 
  574. $args['date_range'] = array_map( 'intval', $args['date_range'] ); 
  575. if ( !empty( $args['date_range']['from'] ) && !empty( $args['date_range']['to'] ) ) { 
  576. $filters[] = array( 
  577. 'range' => array( 
  578. 'date_gmt' => $this->_get_coalesced_range( $args['date_range'] ),  
  579. ); 
  580.  
  581. /** 
  582. * Filter the Post IDs excluded from appearing in Related Posts. 
  583. * @module related-posts 
  584. * @since 2.9.0 
  585. * @param array $args['exclude_post_ids'] Array of Post IDs. 
  586. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  587. */ 
  588. $args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id ); 
  589. if ( !empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) { 
  590. foreach ( $args['exclude_post_ids'] as $exclude_post_id) { 
  591. $exclude_post_id = (int)$exclude_post_id; 
  592.  
  593. if ( $exclude_post_id > 0 ) 
  594. $filters[] = array( 'not' => array( 'term' => array( 'post_id' => $exclude_post_id ) ) ); 
  595.  
  596. return $filters; 
  597.  
  598. /** 
  599. * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching. 
  600. * @param array $date_range 
  601. * @return array 
  602. */ 
  603. protected function _get_coalesced_range( array $date_range ) { 
  604. $now = time(); 
  605. $coalesce_time = $this->_blog_id_wpcom % 86400; 
  606. $current_time = $now - strtotime( 'today', $now ); 
  607.  
  608. if ( $current_time < $coalesce_time && '01' == date( 'd', $now ) ) { 
  609. // Move back 1 period 
  610. return array( 
  611. 'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),  
  612. 'to' => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ),  
  613. ); 
  614. } else { 
  615. // Use current period 
  616. return array( 
  617. 'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ),  
  618. 'to' => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),  
  619. ); 
  620.  
  621. /** 
  622. * Generate and output ajax response for related posts API call. 
  623. * NOTE: Calls exit() to end all further processing after payload has been outputed. 
  624. * @param array $excludes array of post_ids to exclude 
  625. * @uses send_nosniff_header, self::get_for_post_id, get_the_ID 
  626. * @return null 
  627. */ 
  628. protected function _action_frontend_init_ajax( array $excludes ) { 
  629. define( 'DOING_AJAX', true ); 
  630.  
  631. header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8 
  632. send_nosniff_header(); 
  633.  
  634. $related_posts = $this->get_for_post_id( 
  635. get_the_ID(),  
  636. array( 
  637. 'exclude_post_ids' => $excludes,  
  638. ); 
  639.  
  640. $options = $this->get_options(); 
  641.  
  642. $response = array( 
  643. 'version' => self::VERSION,  
  644. 'show_thumbnails' => (bool) $options['show_thumbnails'],  
  645. 'items' => array(),  
  646. ); 
  647.  
  648. if ( count( $related_posts ) == $options['size'] ) 
  649. $response['items'] = $related_posts; 
  650.  
  651. echo json_encode( $response ); 
  652.  
  653. exit(); 
  654.  
  655. /** 
  656. * Returns a UTF-8 encoded array of post information for the given post_id 
  657. * @param int $post_id 
  658. * @param int $position 
  659. * @param int $origin The post id that this is related to 
  660. * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters 
  661. * @return array 
  662. */ 
  663. protected function _get_related_post_data_for_post( $post_id, $position, $origin ) { 
  664. $post = get_post( $post_id ); 
  665.  
  666. return array( 
  667. 'id' => $post->ID,  
  668. 'url' => get_permalink( $post->ID ),  
  669. 'url_meta' => array( 'origin' => $origin, 'position' => $position ),  
  670. 'title' => $this->_to_utf8( $this->_get_title( $post->post_title, $post->post_content ) ),  
  671. 'date' => get_the_date( '', $post->ID ),  
  672. 'format' => get_post_format( $post->ID ),  
  673. /** 
  674. * Filters the rel attribute for the Related Posts' links. 
  675. * @module related-posts 
  676. * @since 3.7.0 
  677. * @param string nofollow Link rel attribute for Related Posts' link. Default is nofollow. 
  678. * @param int $post->ID Post ID. 
  679. */ 
  680. 'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', 'nofollow', $post->ID ),  
  681. 'excerpt' => html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),  
  682. /** 
  683. * Filter the context displayed below each Related Post. 
  684. * @module related-posts 
  685. * @since 3.0.0 
  686. * @param string $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ) Context displayed below each related post. 
  687. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  688. */ 
  689. 'context' => apply_filters( 
  690. 'jetpack_relatedposts_filter_post_context',  
  691. $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ),  
  692. $post->ID 
  693. ),  
  694. 'img' => $this->_generate_related_post_image_params( $post->ID ),  
  695. /** 
  696. * Filter the post css classes added on HTML markup. 
  697. * @module related-posts 
  698. * @since 3.8.0 
  699. * @param array array() CSS classes added on post HTML markup. 
  700. * @param string $post_id Post ID. 
  701. */ 
  702. 'classes' => apply_filters( 
  703. 'jetpack_relatedposts_filter_post_css_classes',  
  704. array(),  
  705. $post->ID 
  706. ),  
  707. ); 
  708.  
  709. /** 
  710. * Returns either the title or a small excerpt to use as title for post. 
  711. * @param string $post_title 
  712. * @param string $post_content 
  713. * @uses strip_shortcodes, wp_trim_words, __ 
  714. * @return string 
  715. */ 
  716. protected function _get_title( $post_title, $post_content ) { 
  717. if ( ! empty( $post_title ) ) { 
  718. return wp_strip_all_tags( $post_title ); 
  719.  
  720. $post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '*' ); 
  721. if ( ! empty( $post_title ) ) { 
  722. return $post_title; 
  723.  
  724. return __( 'Untitled Post', 'jetpack' ); 
  725.  
  726. /** 
  727. * Returns a plain text post excerpt for title attribute of links. 
  728. * @param string $post_excerpt 
  729. * @param string $post_content 
  730. * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words 
  731. * @return string 
  732. */ 
  733. protected function _get_excerpt( $post_excerpt, $post_content ) { 
  734. if ( empty( $post_excerpt ) ) 
  735. $excerpt = $post_content; 
  736. else 
  737. $excerpt = $post_excerpt; 
  738.  
  739. return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '*' ); 
  740.  
  741. /** 
  742. * Generates the thumbnail image to be used for the post. Uses the 
  743. * image as returned by Jetpack_PostImages::get_image() 
  744. * @param int $post_id 
  745. * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url 
  746. * @return string 
  747. */ 
  748. protected function _generate_related_post_image_params( $post_id ) { 
  749. $options = $this->get_options(); 
  750. $image_params = array( 
  751. 'src' => '',  
  752. 'width' => 0,  
  753. 'height' => 0,  
  754. ); 
  755.  
  756. if ( ! $options['show_thumbnails'] ) { 
  757. return $image_params; 
  758.  
  759. /** 
  760. * Filter the size of the Related Posts images. 
  761. * @module related-posts 
  762. * @since 2.8.0 
  763. * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post. 
  764. */ 
  765. $thumbnail_size = apply_filters( 
  766. 'jetpack_relatedposts_filter_thumbnail_size',  
  767. array( 'width' => 350, 'height' => 200 ) 
  768. ); 
  769. if ( !is_array( $thumbnail_size ) ) { 
  770. $thumbnail_size = array( 
  771. 'width' => (int)$thumbnail_size,  
  772. 'height' => (int)$thumbnail_size 
  773. ); 
  774.  
  775. // Try to get post image 
  776. if ( class_exists( 'Jetpack_PostImages' ) ) { 
  777. $img_url = ''; 
  778. $post_image = Jetpack_PostImages::get_image( 
  779. $post_id,  
  780. $thumbnail_size 
  781. ); 
  782.  
  783. if ( is_array($post_image) ) { 
  784. $img_url = $post_image['src']; 
  785. } elseif ( class_exists( 'Jetpack_Media_Summary' ) ) { 
  786. $media = Jetpack_Media_Summary::get( $post_id ); 
  787.  
  788. if ( is_array($media) && !empty( $media['image'] ) ) { 
  789. $img_url = $media['image']; 
  790.  
  791. if ( !empty( $img_url ) ) { 
  792. $image_params['width'] = $thumbnail_size['width']; 
  793. $image_params['height'] = $thumbnail_size['height']; 
  794. $image_params['src'] = Jetpack_PostImages::fit_image_url( 
  795. $img_url,  
  796. $thumbnail_size['width'],  
  797. $thumbnail_size['height'] 
  798. ); 
  799.  
  800. return $image_params; 
  801.  
  802. /** 
  803. * Returns the string UTF-8 encoded 
  804. * @param string $text 
  805. * @return string 
  806. */ 
  807. protected function _to_utf8( $text ) { 
  808. if ( $this->_convert_charset ) { 
  809. return iconv( $this->_blog_charset, 'UTF-8', $text ); 
  810. } else { 
  811. return $text; 
  812.  
  813. /** 
  814. * ============================================= 
  815. * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM 
  816. * ============================================= 
  817. */ 
  818.  
  819. /** 
  820. * Workhorse method to return array of related posts matched by ElasticSearch. 
  821. * @param int $post_id 
  822. * @param int $size 
  823. * @param array $filters 
  824. * @uses wp_remote_post, is_wp_error, get_option, wp_remote_retrieve_body, get_post, add_query_arg, remove_query_arg, get_permalink, get_post_format, apply_filters 
  825. * @return array 
  826. */ 
  827. protected function _get_related_posts( $post_id, $size, array $filters ) { 
  828. $hits = $this->_filter_non_public_posts( 
  829. $this->_get_related_post_ids( 
  830. $post_id,  
  831. $size,  
  832. $filters 
  833. ); 
  834.  
  835. /** 
  836. * Filter the Related Posts matched by ElasticSearch. 
  837. * @module related-posts 
  838. * @since 2.9.0 
  839. * @param array $hits Array of Post IDs matched by ElasticSearch. 
  840. * @param string $post_id Post ID of the post for which we are retrieving Related Posts. 
  841. */ 
  842. $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id ); 
  843.  
  844. $related_posts = array(); 
  845. foreach ( $hits as $i => $hit ) { 
  846. $related_posts[] = $this->_get_related_post_data_for_post( $hit['id'], $i, $post_id ); 
  847. return $related_posts; 
  848.  
  849. /** 
  850. * Get array of related posts matched by ElasticSearch. 
  851. * @param int $post_id 
  852. * @param int $size 
  853. * @param array $filters 
  854. * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta 
  855. * @return array 
  856. */ 
  857. protected function _get_related_post_ids( $post_id, $size, array $filters ) { 
  858. $now_ts = time(); 
  859. $cache_meta_key = '_jetpack_related_posts_cache'; 
  860.  
  861. $body = array( 
  862. 'size' => (int) $size,  
  863. ); 
  864.  
  865. if ( !empty( $filters ) ) 
  866. $body['filter'] = array( 'and' => $filters ); 
  867.  
  868. // Load all cached values 
  869. $cache = get_post_meta( $post_id, $cache_meta_key, true ); 
  870. if ( empty( $cache ) ) 
  871. $cache = array(); 
  872.  
  873. // Build cache key 
  874. $cache_key = md5( serialize( $body ) ); 
  875.  
  876. // Cache is valid! Return cached value. 
  877. if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ][ 'expires' ] > $now_ts ) { 
  878. return $cache[ $cache_key ][ 'payload' ]; 
  879.  
  880. $response = wp_remote_post( 
  881. "https://public-api.wordpress.com/rest/v1/sites/{$this->_blog_id_wpcom}/posts/$post_id/related/",  
  882. array( 
  883. 'timeout' => 10,  
  884. 'user-agent' => 'jetpack_related_posts',  
  885. 'sslverify' => true,  
  886. 'body' => $body,  
  887. ); 
  888.  
  889. // Oh no... return nothing don't cache errors. 
  890. if ( is_wp_error( $response ) ) { 
  891. if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) ) 
  892. return $cache[ $cache_key ][ 'payload' ]; // return stale 
  893. else 
  894. return array(); 
  895.  
  896. $results = json_decode( wp_remote_retrieve_body( $response ), true ); 
  897. $related_posts = array(); 
  898. if ( is_array( $results ) && !empty( $results['hits'] ) ) { 
  899. foreach( $results['hits'] as $hit ) { 
  900. $related_posts[] = array( 
  901. 'id' => $hit['fields']['post_id'],  
  902. ); 
  903.  
  904. // Copy all valid cache values 
  905. $new_cache = array(); 
  906. foreach ( $cache as $k => $v ) { 
  907. if ( is_array( $v ) && $v[ 'expires' ] > $now_ts ) { 
  908. $new_cache[ $k ] = $v; 
  909.  
  910. // Set new cache value if valid 
  911. if ( ! empty( $related_posts ) ) { 
  912. $new_cache[ $cache_key ] = array( 
  913. 'expires' => 12 * HOUR_IN_SECONDS + $now_ts,  
  914. 'payload' => $related_posts,  
  915. ); 
  916.  
  917. // Update cache 
  918. update_post_meta( $post_id, $cache_meta_key, $new_cache ); 
  919.  
  920. return $related_posts; 
  921.  
  922. /** 
  923. * Filter out any hits that are not public anymore. 
  924. * @param array $related_posts 
  925. * @uses get_post_stati, get_post_status 
  926. * @return array 
  927. */ 
  928. protected function _filter_non_public_posts( array $related_posts ) { 
  929. $public_stati = get_post_stati( array( 'public' => true ) ); 
  930.  
  931. $filtered = array(); 
  932. foreach ( $related_posts as $hit ) { 
  933. if ( in_array( get_post_status( $hit['id'] ), $public_stati ) ) { 
  934. $filtered[] = $hit; 
  935. return $filtered; 
  936.  
  937. /** 
  938. * Generates a context for the related content (second line in related post output). 
  939. * Order of importance: 
  940. * - First category (Not 'Uncategorized') 
  941. * - First post tag 
  942. * - Number of comments 
  943. * @param int $post_id 
  944. * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n 
  945. * @return string 
  946. */ 
  947. protected function _generate_related_post_context( $post_id ) { 
  948. $categories = get_the_category( $post_id ); 
  949. if ( is_array( $categories ) ) { 
  950. foreach ( $categories as $category ) { 
  951. if ( 'uncategorized' != $category->slug && '' != trim( $category->name ) ) { 
  952. $post_cat_context = sprintf( 
  953. _x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),  
  954. $category->name 
  955. ); 
  956. /** 
  957. * Filter the "In Category" line displayed in the post context below each Related Post. 
  958. * @module related-posts 
  959. * @since 3.2.0 
  960. * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post. 
  961. * @param array $category Array containing information about the category. 
  962. */ 
  963. return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category ); 
  964.  
  965. $tags = get_the_terms( $post_id, 'post_tag' ); 
  966. if ( is_array( $tags ) ) { 
  967. foreach ( $tags as $tag ) { 
  968. if ( '' != trim( $tag->name ) ) { 
  969. $post_tag_context = sprintf( 
  970. _x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),  
  971. $tag->name 
  972. ); 
  973. /** 
  974. * Filter the "In Tag" line displayed in the post context below each Related Post. 
  975. * @module related-posts 
  976. * @since 3.2.0 
  977. * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post. 
  978. * @param array $tag Array containing information about the tag. 
  979. */ 
  980. return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag ); 
  981.  
  982. $comment_count = get_comments_number( $post_id ); 
  983. if ( $comment_count > 0 ) { 
  984. return sprintf( 
  985. _n( 'With 1 comment', 'With %s comments', $comment_count, 'jetpack' ),  
  986. number_format_i18n( $comment_count ) 
  987. ); 
  988.  
  989. return __( 'Similar post', 'jetpack' ); 
  990.  
  991. /** 
  992. * Logs clicks for clickthrough analysis and related result tuning. 
  993. * @return null 
  994. */ 
  995. protected function _log_click( $post_id, $to_post_id, $link_position ) { 
  996.  
  997.  
  998. /** 
  999. * Determines if the current post is able to use related posts. 
  1000. * @uses self::get_options, is_admin, is_single, apply_filters 
  1001. * @return bool 
  1002. */ 
  1003. protected function _enabled_for_request() { 
  1004. // Default to enabled 
  1005. $enabled = true; 
  1006.  
  1007. // Must have feature enabled 
  1008. $options = $this->get_options(); 
  1009. if ( ! $options['enabled'] ) { 
  1010. $enabled = false; 
  1011.  
  1012. // Only run for frontend pages 
  1013. if ( is_admin() ) { 
  1014. $enabled = false; 
  1015.  
  1016. // Only run for standalone posts 
  1017. if ( ! is_single() ) { 
  1018. $enabled = false; 
  1019.  
  1020. /** 
  1021. * Filter the Enabled value to allow related posts to be shown on pages as well. 
  1022. * @module related-posts 
  1023. * @since 3.3.0 
  1024. * @param bool $enabled Should Related Posts be enabled on the current page. 
  1025. */ 
  1026. return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled ); 
  1027.  
  1028. /** 
  1029. * Adds filters and enqueues assets. 
  1030. * @uses self::_enqueue_assets, self::_setup_shortcode, add_filter 
  1031. * @return null 
  1032. */ 
  1033. protected function _action_frontend_init_page() { 
  1034. $this->_enqueue_assets( true, true ); 
  1035. $this->_setup_shortcode(); 
  1036.  
  1037. add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 ); 
  1038.  
  1039. /** 
  1040. * Enqueues assets needed to do async loading of related posts. 
  1041. * @uses wp_enqueue_script, wp_enqueue_style, plugins_url 
  1042. * @return null 
  1043. */ 
  1044. protected function _enqueue_assets( $script, $style ) { 
  1045. if ( $script ) 
  1046. wp_enqueue_script( 'jetpack_related-posts', plugins_url( 'related-posts.js', __FILE__ ), array( 'jquery' ), self::VERSION ); 
  1047. if ( $style ) { 
  1048. if( is_rtl() ) { 
  1049. wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'rtl/related-posts-rtl.css', __FILE__ ), array(), self::VERSION ); 
  1050. } else { 
  1051. wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION ); 
  1052.  
  1053. /** 
  1054. * Sets up the shortcode processing. 
  1055. * @uses add_filter, add_shortcode 
  1056. * @return null 
  1057. */ 
  1058. protected function _setup_shortcode() { 
  1059. add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 ); 
  1060.  
  1061. add_shortcode( self::SHORTCODE, array( $this, 'get_target_html' ) ); 
  1062.  
  1063. protected function _allow_feature_toggle() { 
  1064. if ( null === $this->_allow_feature_toggle ) { 
  1065. /** 
  1066. * Filter the display of the Related Posts toggle in Settings > Reading. 
  1067. * @module related-posts 
  1068. * @since 2.8.0 
  1069. * @param bool false Display a feature toggle. Default to false. 
  1070. */ 
  1071. $this->_allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false ); 
  1072. return $this->_allow_feature_toggle;