diff options
Diffstat (limited to 'plugins/jetpack/modules/related-posts/jetpack-related-posts.php')
-rw-r--r-- | plugins/jetpack/modules/related-posts/jetpack-related-posts.php | 1804 |
1 files changed, 1804 insertions, 0 deletions
diff --git a/plugins/jetpack/modules/related-posts/jetpack-related-posts.php b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php new file mode 100644 index 00000000..d1d4855f --- /dev/null +++ b/plugins/jetpack/modules/related-posts/jetpack-related-posts.php @@ -0,0 +1,1804 @@ +<?php +class Jetpack_RelatedPosts { + const VERSION = '20190204'; + const SHORTCODE = 'jetpack-related-posts'; + + private static $instance = null; + private static $instance_raw = null; + + /** + * Creates and returns a static instance of Jetpack_RelatedPosts. + * + * @return Jetpack_RelatedPosts + */ + public static function init() { + if ( ! self::$instance ) { + if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init' ) ) { + self::$instance = WPCOM_RelatedPosts::init(); + } else { + self::$instance = new Jetpack_RelatedPosts(); + } + } + + return self::$instance; + } + + /** + * Creates and returns a static instance of Jetpack_RelatedPosts_Raw. + * + * @return Jetpack_RelatedPosts + */ + public static function init_raw() { + if ( ! self::$instance_raw ) { + if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init_raw' ) ) { + self::$instance_raw = WPCOM_RelatedPosts::init_raw(); + } else { + self::$instance_raw = new Jetpack_RelatedPosts_Raw(); + } + } + + return self::$instance_raw; + } + + protected $_options; + protected $_allow_feature_toggle; + protected $_blog_charset; + protected $_convert_charset; + protected $_previous_post_id; + protected $_found_shortcode = false; + + /** + * Constructor for Jetpack_RelatedPosts. + * + * @uses get_option, add_action, apply_filters + * @return null + */ + public function __construct() { + $this->_blog_charset = get_option( 'blog_charset' ); + $this->_convert_charset = ( function_exists( 'iconv' ) && ! preg_match( '/^utf\-?8$/i', $this->_blog_charset ) ); + + add_action( 'admin_init', array( $this, 'action_admin_init' ) ); + add_action( 'wp', array( $this, 'action_frontend_init' ) ); + + if ( ! class_exists( 'Jetpack_Media_Summary' ) ) { + jetpack_require_lib( 'class.media-summary' ); + } + + // Add Related Posts to the REST API Post response. + add_action( 'rest_api_init', array( $this, 'rest_register_related_posts' ) ); + + jetpack_register_block( + 'jetpack/related-posts', + array( + 'render_callback' => array( $this, 'render_block' ), + ) + ); + } + + protected function get_blog_id() { + return Jetpack_Options::get_option( 'id' ); + } + + /** + * ================= + * ACTIONS & FILTERS + * ================= + */ + + /** + * Add a checkbox field to Settings > Reading for enabling related posts. + * + * @action admin_init + * @uses add_settings_field, __, register_setting, add_action + * @return null + */ + public function action_admin_init() { + + // Add the setting field [jetpack_relatedposts] and place it in Settings > Reading + add_settings_field( 'jetpack_relatedposts', '<span id="jetpack_relatedposts">' . __( 'Related posts', 'jetpack' ) . '</span>', array( $this, 'print_setting_html' ), 'reading' ); + register_setting( 'reading', 'jetpack_relatedposts', array( $this, 'parse_options' ) ); + add_action('admin_head', array( $this, 'print_setting_head' ) ); + + if( 'options-reading.php' == $GLOBALS['pagenow'] ) { + // Enqueue style for live preview on the reading settings page + $this->_enqueue_assets( false, true ); + } + } + + /** + * Load related posts assets if it's a elegiable front end page or execute search and return JSON if it's an endpoint request. + * + * @global $_GET + * @action wp + * @uses add_shortcode, get_the_ID + * @returns null + */ + public function action_frontend_init() { + // Add a shortcode handler that outputs nothing, this gets overridden later if we can display related content + add_shortcode( self::SHORTCODE, array( $this, 'get_target_html_unsupported' ) ); + + if ( ! $this->_enabled_for_request() ) + return; + + if ( isset( $_GET['relatedposts'] ) ) { + $excludes = $this->parse_numeric_get_arg( 'relatedposts_exclude' ); + $this->_action_frontend_init_ajax( $excludes ); + } else { + if ( isset( $_GET['relatedposts_hit'], $_GET['relatedposts_origin'], $_GET['relatedposts_position'] ) ) { + $this->_log_click( $_GET['relatedposts_origin'], get_the_ID(), $_GET['relatedposts_position'] ); + $this->_previous_post_id = (int) $_GET['relatedposts_origin']; + } + + $this->_action_frontend_init_page(); + } + + } + + /** + * Render insertion point. + * + * @since 4.2.0 + * + * @return string + */ + public function get_headline() { + $options = $this->get_options(); + + if ( $options['show_headline'] ) { + $headline = sprintf( + /** This filter is already documented in modules/sharedaddy/sharing-service.php */ + apply_filters( 'jetpack_sharing_headline_html', '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>', esc_html( $options['headline'] ), 'related-posts' ), + esc_html( $options['headline'] ) + ); + } else { + $headline = ''; + } + return $headline; + } + + /** + * Adds a target to the post content to load related posts into if a shortcode for it did not already exist. + * Will skip adding the target if the post content contains a Related Posts block. + * + * @filter the_content + * @param string $content + * @returns string + */ + public function filter_add_target_to_dom( $content ) { + if ( has_block( 'jetpack/related-posts', $content ) ) { + return $content; + } + + if ( ! $this->_found_shortcode ) { + $content .= "\n" . $this->get_target_html(); + } + + return $content; + } + + /** + * Looks for our shortcode on the unfiltered content, this has to execute early. + * + * @filter the_content + * @param string $content + * @uses has_shortcode + * @returns string + */ + public function test_for_shortcode( $content ) { + $this->_found_shortcode = has_shortcode( $content, self::SHORTCODE ); + + return $content; + } + + /** + * Returns the HTML for the related posts section. + * + * @uses esc_html__, apply_filters + * @returns string + */ + public function get_target_html() { + require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php'; + if ( Jetpack_Sync_Settings::is_syncing() ) { + return ''; + } + + /** + * Filter the Related Posts headline. + * + * @module related-posts + * + * @since 3.0.0 + * + * @param string $headline Related Posts heading. + */ + $headline = apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() ); + + if ( $this->_previous_post_id ) { + $exclude = "data-exclude='{$this->_previous_post_id}'"; + } else { + $exclude = ""; + } + + return <<<EOT +<div id='jp-relatedposts' class='jp-relatedposts' $exclude> + $headline +</div> +EOT; + } + + /** + * 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. + * + * @returns string + */ + public function get_target_html_unsupported() { + require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php'; + if ( Jetpack_Sync_Settings::is_syncing() ) { + return ''; + } + return "\n\n<!-- Jetpack Related Posts is not supported in this context. -->\n\n"; + } + + /** + * =============== + * GUTENBERG BLOCK + * =============== + */ + + /** + * Echoes out items for the Gutenberg block + * + * @param array $related_post The post oject. + * @param array $block_attributes The block attributes. + */ + public function render_block_item( $related_post, $block_attributes ) { + $instance_id = 'related-posts-item-' . uniqid(); + $label_id = $instance_id . '-label'; + + $item_markup = sprintf( + '<ul id="%1$s" aria-labelledby="%2$s" class="jp-related-posts-i2__post" role="menuitem">', + esc_attr( $instance_id ), + esc_attr( $label_id ) + ); + + $item_markup .= sprintf( + '<li class="jp-related-posts-i2__post-link"><a id="%1$s" href="%2$s" rel="%4$s">%3$s</a></li>', + esc_attr( $label_id ), + esc_url( $related_post['url'] ), + esc_attr( $related_post['title'] ), + esc_attr( $related_post['rel'] ) + ); + + if ( ! empty( $block_attributes['show_thumbnails'] ) && ! empty( $related_post['img']['src'] ) ) { + $img_link = sprintf( + '<li class="jp-related-posts-i2__post-img-link"><a href="%1$s" rel="%2$s"><img src="%3$s" width="%4$s" alt="%5$s" /></a></li>', + esc_url( $related_post['url'] ), + esc_attr( $related_post['rel'] ), + esc_url( $related_post['img']['src'] ), + esc_attr( $related_post['img']['width'] ), + esc_attr( $related_post['img']['alt_text'] ) + ); + + $item_markup .= $img_link; + } + + if ( $block_attributes['show_date'] ) { + $date_tag = sprintf( + '<li class="jp-related-posts-i2__post-date">%1$s</li>', + esc_html( $related_post['date'] ) + ); + + $item_markup .= $date_tag; + } + + if ( ( $block_attributes['show_context'] ) && ! empty( $related_post['context'] ) ) { + $context_tag = sprintf( + '<li class="jp-related-posts-i2__post-context">%1$s</li>', + esc_html( $related_post['context'] ) + ); + + $item_markup .= $context_tag; + } + + $item_markup .= '</ul>'; + + return $item_markup; + } + + /** + * Render a related posts row. + * + * @param array $posts The posts to render into the row. + * @param array $block_attributes Block attributes. + */ + public function render_block_row( $posts, $block_attributes ) { + $rows_markup = ''; + foreach ( $posts as $post ) { + $rows_markup .= $this->render_block_item( $post, $block_attributes ); + } + return sprintf( + '<div class="jp-related-posts-i2__row" data-post-count="%1$s">%2$s</div>', + count( $posts ), + $rows_markup + ); + } + + /** + * Render the related posts markup. + * + * @param array $attributes Block attributes. + * @return string + */ + public function render_block( $attributes ) { + $block_attributes = array( + 'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'], + 'show_date' => isset( $attributes['displayDate'] ) ? (bool) $attributes['displayDate'] : true, + 'show_context' => isset( $attributes['displayContext'] ) && $attributes['displayContext'], + 'layout' => isset( $attributes['postLayout'] ) && 'list' === $attributes['postLayout'] ? $attributes['postLayout'] : 'grid', + 'size' => ! empty( $attributes['postsToShow'] ) ? absint( $attributes['postsToShow'] ) : 3, + ); + + $excludes = $this->parse_numeric_get_arg( 'relatedposts_origin' ); + $related_posts = $this->get_for_post_id( + get_the_ID(), + array( + 'size' => $block_attributes['size'], + 'exclude_post_ids' => $excludes, + ) + ); + + $display_lower_row = $block_attributes['size'] > 3; + + if ( empty( $related_posts ) ) { + return ''; + } + + switch ( count( $related_posts ) ) { + case 2: + case 4: + case 5: + $top_row_end = 2; + break; + + default: + $top_row_end = 3; + break; + } + + $upper_row_posts = array_slice( $related_posts, 0, $top_row_end ); + $lower_row_posts = array_slice( $related_posts, $top_row_end ); + + $rows_markup = $this->render_block_row( $upper_row_posts, $block_attributes ); + if ( $display_lower_row ) { + $rows_markup .= $this->render_block_row( $lower_row_posts, $block_attributes ); + } + + $target_to_dom_priority = has_filter( + 'the_content', + array( $this, 'filter_add_target_to_dom' ) + ); + remove_filter( + 'the_content', + array( $this, 'filter_add_target_to_dom' ), + $target_to_dom_priority + ); + + /* + Below is a hack to get the block content to render correctly. + + This functionality should be covered in /inc/blocks.php but due to an error, + this has not been fixed as of this writing. + + Alda has submitted a patch to Core in order to have this issue fixed at + https://core.trac.wordpress.org/attachment/ticket/45495/do_blocks.diff and + hopefully it makes to to the final RC of WP 5.1. + */ + $priority = has_filter( 'the_content', 'wpautop' ); + remove_filter( 'the_content', 'wpautop', $priority ); + add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); + + return sprintf( + '<nav class="jp-relatedposts-i2" data-layout="%1$s">%2$s</nav>', + esc_attr( $block_attributes['layout'] ), + $rows_markup + ); + } + + /** + * ======================== + * PUBLIC UTILITY FUNCTIONS + * ======================== + */ + + /** + * Parse a numeric GET variable to an array of values. + * + * @since 6.9.0 + * + * @uses absint + * + * @param string $arg Name of the GET variable + * @return array $result Parsed value(s) + */ + public function parse_numeric_get_arg( $arg ) { + $result = array(); + + if ( isset( $_GET[ $arg ] ) ) { + if ( is_string( $_GET[ $arg ] ) ) { + $result = explode( ',', $_GET[ $arg ] ); + } elseif ( is_array( $_GET[ $arg ] ) ) { + $result = array_values( $_GET[ $arg ] ); + } + + $result = array_unique( array_filter( array_map( 'absint', $result ) ) ); + } + + return $result; + } + + /** + * Gets options set for Jetpack_RelatedPosts and merge with defaults. + * + * @uses Jetpack_Options::get_option, apply_filters + * @return array + */ + public function get_options() { + if ( null === $this->_options ) { + $this->_options = Jetpack_Options::get_option( 'relatedposts', array() ); + if ( ! is_array( $this->_options ) ) + $this->_options = array(); + if ( ! isset( $this->_options['enabled'] ) ) + $this->_options['enabled'] = true; + if ( ! isset( $this->_options['show_headline'] ) ) + $this->_options['show_headline'] = true; + if ( ! isset( $this->_options['show_thumbnails'] ) ) + $this->_options['show_thumbnails'] = false; + if ( ! isset( $this->_options['show_date'] ) ) { + $this->_options['show_date'] = true; + } + if ( ! isset( $this->_options['show_context'] ) ) { + $this->_options['show_context'] = true; + } + if ( ! isset( $this->_options['layout'] ) ) { + $this->_options['layout'] = 'grid'; + } + if ( ! isset( $this->_options['headline'] ) ) { + $this->_options['headline'] = esc_html__( 'Related', 'jetpack' ); + } + if ( empty( $this->_options['size'] ) || (int)$this->_options['size'] < 1 ) + $this->_options['size'] = 3; + + /** + * Filter Related Posts basic options. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array $this->_options Array of basic Related Posts options. + */ + $this->_options = apply_filters( 'jetpack_relatedposts_filter_options', $this->_options ); + } + + return $this->_options; + } + + public function get_option( $option_name ) { + $options = $this->get_options(); + + if ( isset( $options[ $option_name ] ) ) { + return $options[ $option_name ]; + } + + return false; + } + + /** + * Parses input and returns normalized options array. + * + * @param array $input + * @uses self::get_options + * @return array + */ + public function parse_options( $input ) { + $current = $this->get_options(); + + if ( !is_array( $input ) ) + $input = array(); + + if ( + ! isset( $input['enabled'] ) + || isset( $input['show_date'] ) + || isset( $input['show_context'] ) + || isset( $input['layout'] ) + || isset( $input['headline'] ) + ) { + $input['enabled'] = '1'; + } + + if ( '1' == $input['enabled'] ) { + $current['enabled'] = true; + $current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] ); + $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] ); + $current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] ); + $current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] ); + $current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid'; + $current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' ); + } else { + $current['enabled'] = false; + } + + if ( isset( $input['size'] ) && (int)$input['size'] > 0 ) + $current['size'] = (int)$input['size']; + else + $current['size'] = null; + + return $current; + } + + /** + * HTML for admin settings page. + * + * @uses self::get_options, checked, esc_html__ + * @returns null + */ + public function print_setting_html() { + $options = $this->get_options(); + + $ui_settings_template = <<<EOT +<p class="description">%s</p> +<ul id="settings-reading-relatedposts-customize"> + <li> + <label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label> + </li> + <li> + <label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label> + </li> + <li> + <label><input name="jetpack_relatedposts[show_date]" type="checkbox" value="1" %s /> %s</label> + </li> + <li> + <label><input name="jetpack_relatedposts[show_context]" type="checkbox" value="1" %s /> %s</label> + </li> +</ul> +<div id='settings-reading-relatedposts-preview'> + %s + <div id="jp-relatedposts" class="jp-relatedposts"></div> +</div> +EOT; + $ui_settings = sprintf( + $ui_settings_template, + esc_html__( 'The following settings will impact all related posts on your site, except for those you created via the block editor:', 'jetpack' ), + checked( $options['show_headline'], true, false ), + esc_html__( 'Highlight related content with a heading', 'jetpack' ), + checked( $options['show_thumbnails'], true, false ), + esc_html__( 'Show a thumbnail image where available', 'jetpack' ), + checked( $options['show_date'], true, false ), + esc_html__( 'Show entry date', 'jetpack' ), + checked( $options['show_context'], true, false ), + esc_html__( 'Show context (category or tag)', 'jetpack' ), + esc_html__( 'Preview:', 'jetpack' ) + ); + + if ( !$this->_allow_feature_toggle() ) { + $template = <<<EOT +<input type="hidden" name="jetpack_relatedposts[enabled]" value="1" /> +%s +EOT; + printf( + $template, + $ui_settings + ); + } else { + $template = <<<EOT +<ul id="settings-reading-relatedposts"> + <li> + <label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label> + </li> + <li> + <label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label> + %s + </li> +</ul> +EOT; + printf( + $template, + checked( $options['enabled'], false, false ), + esc_html__( 'Hide related content after posts', 'jetpack' ), + checked( $options['enabled'], true, false ), + esc_html__( 'Show related content after posts', 'jetpack' ), + $ui_settings + ); + } + } + + /** + * Head JS/CSS for admin settings page. + * + * @uses esc_html__ + * @returns null + */ + public function print_setting_head() { + + // only dislay the Related Posts JavaScript on the Reading Settings Admin Page + $current_screen = get_current_screen(); + + if ( is_null( $current_screen ) ) { + return; + } + + if( 'options-reading' != $current_screen->id ) + return; + + $related_headline = sprintf( + '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>', + esc_html__( 'Related', 'jetpack' ) + ); + + $href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"'; + $related_with_images = <<<EOT +<div class="jp-relatedposts-items jp-relatedposts-items-visual"> + <div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image"> + <a $href_params> + <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/cat-blog.png" width="350" alt="Big iPhone/iPad Update Now Available" scale="0"> + </a> + <h4 class="jp-relatedposts-post-title"> + <a $href_params>Big iPhone/iPad Update Now Available</a> + </h4> + <p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p> + <p class="jp-relatedposts-post-context">In "Mobile"</p> + </div> + <div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image"> + <a $href_params> + <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/devices.jpg" width="350" alt="The WordPress for Android App Gets a Big Facelift" scale="0"> + </a> + <h4 class="jp-relatedposts-post-title"> + <a $href_params>The WordPress for Android App Gets a Big Facelift</a> + </h4> + <p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p> + <p class="jp-relatedposts-post-context">In "Mobile"</p> + </div> + <div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image"> + <a $href_params> + <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg" width="350" alt="Upgrade Focus: VideoPress For Weddings" scale="0"> + </a> + <h4 class="jp-relatedposts-post-title"> + <a $href_params>Upgrade Focus: VideoPress For Weddings</a> + </h4> + <p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p> + <p class="jp-relatedposts-post-context">In "Upgrade"</p> + </div> +</div> +EOT; + $related_with_images = str_replace( "\n", '', $related_with_images ); + $related_without_images = <<<EOT +<div class="jp-relatedposts-items jp-relatedposts-items-minimal"> + <p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image"> + <span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span> + <span class="jp-relatedposts-post-context">In "Mobile"</span> + </p> + <p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image"> + <span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span> + <span class="jp-relatedposts-post-context">In "Mobile"</span> + </p> + <p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image"> + <span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span> + <span class="jp-relatedposts-post-context">In "Upgrade"</span> + </p> +</div> +EOT; + $related_without_images = str_replace( "\n", '', $related_without_images ); + + if ( $this->_allow_feature_toggle() ) { + $extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }'; + } else { + $extra_css = ''; + } + + echo <<<EOT +<style type="text/css"> + #settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); } + #settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; } + $extra_css +</style> +<script type="text/javascript"> + jQuery( document ).ready( function($) { + var update_ui = function() { + var is_enabled = true; + if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) { + if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) { + is_enabled = false; + } + } + if ( is_enabled ) { + $( '#settings-reading-relatedposts-customize' ) + .removeClass( 'disabled' ) + .find( 'input' ) + .attr( 'disabled', false ); + $( '#settings-reading-relatedposts-preview' ) + .removeClass( 'disabled' ); + } else { + $( '#settings-reading-relatedposts-customize' ) + .addClass( 'disabled' ) + .find( 'input' ) + .attr( 'disabled', true ); + $( '#settings-reading-relatedposts-preview' ) + .addClass( 'disabled' ); + } + }; + + var update_preview = function() { + var html = ''; + if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).length ) { + html += '$related_headline'; + } + if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).length ) { + html += '$related_with_images'; + } else { + html += '$related_without_images'; + } + $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).html( html ); + if ( $( 'input[name="jetpack_relatedposts[show_date]"]:checked' ).length ) { + $( '.jp-relatedposts-post-title' ).each( function() { + $( this ).after( $( '<span>August 8, 2005</span>' ) ); + } ); + } + if ( $( 'input[name="jetpack_relatedposts[show_context]"]:checked' ).length ) { + $( '.jp-relatedposts-post-context' ).show(); + } else { + $( '.jp-relatedposts-post-context' ).hide(); + } + $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).show(); + }; + + // Update on load + update_preview(); + update_ui(); + + // Update on change + $( '#settings-reading-relatedposts-customize input' ) + .change( update_preview ); + $( '#settings-reading-relatedposts' ) + .find( 'input.tog' ) + .change( update_ui ); + }); +</script> +EOT; + } + + /** + * Gets an array of related posts that match the given post_id. + * + * @param int $post_id Post which we want to find related posts for. + * @param array $args - params to use when building Elasticsearch filters to narrow down the search domain. + * @uses self::get_options, get_post_type, wp_parse_args, apply_filters + * @return array + */ + public function get_for_post_id( $post_id, array $args ) { + $options = $this->get_options(); + + if ( ! empty( $args['size'] ) ) { + $options['size'] = $args['size']; + } + + if ( + ! $options['enabled'] + || 0 === (int) $post_id + || empty( $options['size'] ) + ) { + return array(); + } + + $defaults = array( + 'size' => (int) $options['size'], + 'post_type' => get_post_type( $post_id ), + 'post_formats' => array(), + 'has_terms' => array(), + 'date_range' => array(), + 'exclude_post_ids' => array(), + ); + $args = wp_parse_args( $args, $defaults ); + /** + * Filter the arguments used to retrieve a list of Related Posts. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array $args Array of options to retrieve Related Posts. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id ); + + $filters = $this->_get_es_filters_from_args( $post_id, $args ); + /** + * Filter Elasticsearch options used to calculate Related Posts. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array $filters Array of Elasticsearch filters based on the post_id and args. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id ); + + $results = $this->_get_related_posts( $post_id, $args['size'], $filters ); + /** + * Filter the array of related posts matched by Elasticsearch. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array $results Array of related posts matched by Elasticsearch. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id ); + } + + /** + * ========================= + * PRIVATE UTILITY FUNCTIONS + * ========================= + */ + + /** + * Creates an array of Elasticsearch filters based on the post_id and args. + * + * @param int $post_id + * @param array $args + * @uses apply_filters, get_post_types, get_post_format_strings + * @return array + */ + protected function _get_es_filters_from_args( $post_id, array $args ) { + $filters = array(); + + /** + * Filter the terms used to search for Related Posts. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array $args['has_terms'] Array of terms associated to the Related Posts. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id ); + if ( ! empty( $args['has_terms'] ) ) { + foreach( (array)$args['has_terms'] as $term ) { + if ( mb_strlen( $term->taxonomy ) ) { + switch ( $term->taxonomy ) { + case 'post_tag': + $tax_fld = 'tag.slug'; + break; + case 'category': + $tax_fld = 'category.slug'; + break; + default: + $tax_fld = 'taxonomy.' . $term->taxonomy . '.slug'; + break; + } + $filters[] = array( 'term' => array( $tax_fld => $term->slug ) ); + } + } + } + + /** + * Filter the Post Types where we search Related Posts. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array $args['post_type'] Array of Post Types. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id ); + $valid_post_types = get_post_types(); + if ( is_array( $args['post_type'] ) ) { + $sanitized_post_types = array(); + foreach ( $args['post_type'] as $pt ) { + if ( in_array( $pt, $valid_post_types ) ) + $sanitized_post_types[] = $pt; + } + if ( ! empty( $sanitized_post_types ) ) + $filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) ); + } else if ( in_array( $args['post_type'], $valid_post_types ) && 'all' != $args['post_type'] ) { + $filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) ); + } + + /** + * Filter the Post Formats where we search Related Posts. + * + * @module related-posts + * + * @since 3.3.0 + * + * @param array $args['post_formats'] Array of Post Formats. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id ); + $valid_post_formats = get_post_format_strings(); + $sanitized_post_formats = array(); + foreach ( $args['post_formats'] as $pf ) { + if ( array_key_exists( $pf, $valid_post_formats ) ) { + $sanitized_post_formats[] = $pf; + } + } + if ( ! empty( $sanitized_post_formats ) ) { + $filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) ); + } + + /** + * Filter the date range used to search Related Posts. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array $args['date_range'] Array of a month interval where we search Related Posts. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id ); + if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) { + $args['date_range'] = array_map( 'intval', $args['date_range'] ); + if ( !empty( $args['date_range']['from'] ) && !empty( $args['date_range']['to'] ) ) { + $filters[] = array( + 'range' => array( + 'date_gmt' => $this->_get_coalesced_range( $args['date_range'] ), + ) + ); + } + } + + /** + * Filter the Post IDs excluded from appearing in Related Posts. + * + * @module related-posts + * + * @since 2.9.0 + * + * @param array $args['exclude_post_ids'] Array of Post IDs. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id ); + if ( !empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) { + $excluded_post_ids = array(); + foreach ( $args['exclude_post_ids'] as $exclude_post_id) { + $exclude_post_id = (int)$exclude_post_id; + if ( $exclude_post_id > 0 ) + $excluded_post_ids[] = $exclude_post_id; + } + $filters[] = array( 'not' => array( 'terms' => array( 'post_id' => $excluded_post_ids ) ) ); + } + + return $filters; + } + + /** + * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching. + * + * @param array $date_range + * @return array + */ + protected function _get_coalesced_range( array $date_range ) { + $now = time(); + $coalesce_time = $this->get_blog_id() % 86400; + $current_time = $now - strtotime( 'today', $now ); + + if ( $current_time < $coalesce_time && '01' == date( 'd', $now ) ) { + // Move back 1 period + return array( + 'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), + 'to' => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ), + ); + } else { + // Use current period + return array( + 'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ), + 'to' => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), + ); + } + } + + /** + * Generate and output ajax response for related posts API call. + * NOTE: Calls exit() to end all further processing after payload has been outputed. + * + * @param array $excludes array of post_ids to exclude + * @uses send_nosniff_header, self::get_for_post_id, get_the_ID + * @return null + */ + protected function _action_frontend_init_ajax( array $excludes ) { + define( 'DOING_AJAX', true ); + + header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8 + send_nosniff_header(); + + $options = $this->get_options(); + + if ( isset( $_GET['jetpackrpcustomize'] ) ) { + + // If we're in the customizer, add dummy content. + $date_now = current_time( get_option( 'date_format' ) ); + $related_posts = array( + array( + 'id' => - 1, + 'url' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png', + 'url_meta' => array( + 'origin' => 0, + 'position' => 0 + ), + 'title' => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ), + 'date' => $date_now, + 'format' => false, + 'excerpt' => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ), + 'rel' => 'nofollow', + 'context' => esc_html__( 'In "Mobile"', 'jetpack' ), + 'img' => array( + 'src' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png', + 'width' => 350, + 'height' => 200 + ), + 'classes' => array() + ), + array( + 'id' => - 1, + 'url' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg', + 'url_meta' => array( + 'origin' => 0, + 'position' => 0 + ), + 'title' => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ), + 'date' => $date_now, + 'format' => false, + 'excerpt' => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ), + 'rel' => 'nofollow', + 'context' => esc_html__( 'In "Mobile"', 'jetpack' ), + 'img' => array( + 'src' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg', + 'width' => 350, + 'height' => 200 + ), + 'classes' => array() + ), + array( + 'id' => - 1, + 'url' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg', + 'url_meta' => array( + 'origin' => 0, + 'position' => 0 + ), + 'title' => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ), + 'date' => $date_now, + 'format' => false, + 'excerpt' => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ), + 'rel' => 'nofollow', + 'context' => esc_html__( 'In "Mobile"', 'jetpack' ), + 'img' => array( + 'src' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg', + 'width' => 350, + 'height' => 200 + ), + 'classes' => array() + ), + ); + + for ( $total = 0; $total < $options['size'] - 3; $total++ ) { + $related_posts[] = $related_posts[ $total ]; + } + + $current_post = get_post(); + + // Exclude current post after filtering to make sure it's excluded and not lost during filtering. + $excluded_posts = array_merge( + /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */ + apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ), + array( $current_post->ID ) + ); + + // Fetch posts with featured image. + $with_post_thumbnails = get_posts( array( + 'posts_per_page' => $options['size'], + 'post__not_in' => $excluded_posts, + 'post_type' => $current_post->post_type, + 'meta_key' => '_thumbnail_id', + 'suppress_filters' => false, + ) ); + + // If we don't have enough, fetch posts without featured image. + if ( 0 < ( $more = $options['size'] - count( $with_post_thumbnails ) ) ) { + $no_post_thumbnails = get_posts( array( + 'posts_per_page' => $more, + 'post__not_in' => $excluded_posts, + 'post_type' => $current_post->post_type, + 'meta_query' => array( + array( + 'key' => '_thumbnail_id', + 'compare' => 'NOT EXISTS', + ), + ), + 'suppress_filters' => false, + ) ); + } else { + $no_post_thumbnails = array(); + } + + foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) { + $related_posts[ $index ]['id'] = $real_post->ID; + $related_posts[ $index ]['url'] = esc_url( get_permalink( $real_post ) ); + $related_posts[ $index ]['title'] = $this->_to_utf8( $this->_get_title( $real_post->post_title, $real_post->post_content ) ); + $related_posts[ $index ]['date'] = get_the_date( '', $real_post ); + $related_posts[ $index ]['excerpt'] = html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $real_post->post_excerpt, $real_post->post_content ) ), ENT_QUOTES, 'UTF-8' ); + $related_posts[ $index ]['img'] = $this->_generate_related_post_image_params( $real_post->ID ); + $related_posts[ $index ]['context'] = $this->_generate_related_post_context( $real_post->ID ); + } + } else { + $related_posts = $this->get_for_post_id( + get_the_ID(), + array( + 'exclude_post_ids' => $excludes, + ) + ); + } + + $response = array( + 'version' => self::VERSION, + 'show_thumbnails' => (bool) $options['show_thumbnails'], + 'show_date' => (bool) $options['show_date'], + 'show_context' => (bool) $options['show_context'], + 'layout' => (string) $options['layout'], + 'headline' => (string) $options['headline'], + 'items' => array(), + ); + + if ( count( $related_posts ) == $options['size'] ) + $response['items'] = $related_posts; + + echo json_encode( $response ); + + exit(); + } + + /** + * Returns a UTF-8 encoded array of post information for the given post_id + * + * @param int $post_id + * @param int $position + * @param int $origin The post id that this is related to + * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters + * @return array + */ + public function get_related_post_data_for_post( $post_id, $position, $origin ) { + $post = get_post( $post_id ); + + return array( + 'id' => $post->ID, + 'url' => get_permalink( $post->ID ), + 'url_meta' => array( 'origin' => $origin, 'position' => $position ), + 'title' => $this->_to_utf8( $this->_get_title( $post->post_title, $post->post_content ) ), + 'date' => get_the_date( '', $post->ID ), + 'format' => get_post_format( $post->ID ), + 'excerpt' => html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ), + /** + * Filters the rel attribute for the Related Posts' links. + * + * @module related-posts + * + * @since 3.7.0 + * + * @param string nofollow Link rel attribute for Related Posts' link. Default is nofollow. + * @param int $post->ID Post ID. + */ + 'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', 'nofollow', $post->ID ), + /** + * Filter the context displayed below each Related Post. + * + * @module related-posts + * + * @since 3.0.0 + * + * @param string $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ) Context displayed below each related post. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + 'context' => apply_filters( + 'jetpack_relatedposts_filter_post_context', + $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ), + $post->ID + ), + 'img' => $this->_generate_related_post_image_params( $post->ID ), + /** + * Filter the post css classes added on HTML markup. + * + * @module related-posts + * + * @since 3.8.0 + * + * @param array array() CSS classes added on post HTML markup. + * @param string $post_id Post ID. + */ + 'classes' => apply_filters( + 'jetpack_relatedposts_filter_post_css_classes', + array(), + $post->ID + ), + ); + } + + /** + * Returns either the title or a small excerpt to use as title for post. + * + * @param string $post_title + * @param string $post_content + * @uses strip_shortcodes, wp_trim_words, __ + * @return string + */ + protected function _get_title( $post_title, $post_content ) { + if ( ! empty( $post_title ) ) { + return wp_strip_all_tags( $post_title ); + } + + $post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' ); + if ( ! empty( $post_title ) ) { + return $post_title; + } + + return __( 'Untitled Post', 'jetpack' ); + } + + /** + * Returns a plain text post excerpt for title attribute of links. + * + * @param string $post_excerpt + * @param string $post_content + * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words + * @return string + */ + protected function _get_excerpt( $post_excerpt, $post_content ) { + if ( empty( $post_excerpt ) ) + $excerpt = $post_content; + else + $excerpt = $post_excerpt; + + return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' ); + } + + /** + * Generates the thumbnail image to be used for the post. Uses the + * image as returned by Jetpack_PostImages::get_image() + * + * @param int $post_id + * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url + * @return string + */ + protected function _generate_related_post_image_params( $post_id ) { + $options = $this->get_options(); + $image_params = array( + 'alt_text' => '', + 'src' => '', + 'width' => 0, + 'height' => 0, + ); + + /** + * Filter the size of the Related Posts images. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post. + */ + $thumbnail_size = apply_filters( + 'jetpack_relatedposts_filter_thumbnail_size', + array( 'width' => 350, 'height' => 200 ) + ); + if ( !is_array( $thumbnail_size ) ) { + $thumbnail_size = array( + 'width' => (int)$thumbnail_size, + 'height' => (int)$thumbnail_size + ); + } + + // Try to get post image + if ( class_exists( 'Jetpack_PostImages' ) ) { + $img_url = ''; + $post_image = Jetpack_PostImages::get_image( + $post_id, + $thumbnail_size + ); + + if ( is_array($post_image) ) { + $img_url = $post_image['src']; + } elseif ( class_exists( 'Jetpack_Media_Summary' ) ) { + $media = Jetpack_Media_Summary::get( $post_id ); + + if ( is_array($media) && !empty( $media['image'] ) ) { + $img_url = $media['image']; + } + } + + if ( ! empty( $img_url ) ) { + if ( ! empty( $post_image['alt_text'] ) ) { + $image_params['alt_text'] = $post_image['alt_text']; + } else { + $image_params['alt_text'] = ''; + } + $image_params['width'] = $thumbnail_size['width']; + $image_params['height'] = $thumbnail_size['height']; + $image_params['src'] = Jetpack_PostImages::fit_image_url( + $img_url, + $thumbnail_size['width'], + $thumbnail_size['height'] + ); + } + } + + return $image_params; + } + + /** + * Returns the string UTF-8 encoded + * + * @param string $text + * @return string + */ + protected function _to_utf8( $text ) { + if ( $this->_convert_charset ) { + return iconv( $this->_blog_charset, 'UTF-8', $text ); + } else { + return $text; + } + } + + /** + * ============================================= + * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM + * ============================================= + */ + + /** + * Workhorse method to return array of related posts matched by Elasticsearch. + * + * @param int $post_id + * @param int $size + * @param array $filters + * @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 + * @return array + */ + protected function _get_related_posts( $post_id, $size, array $filters ) { + $hits = $this->_filter_non_public_posts( + $this->_get_related_post_ids( + $post_id, + $size, + $filters + ) + ); + + /** + * Filter the Related Posts matched by Elasticsearch. + * + * @module related-posts + * + * @since 2.9.0 + * + * @param array $hits Array of Post IDs matched by Elasticsearch. + * @param string $post_id Post ID of the post for which we are retrieving Related Posts. + */ + $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id ); + + $related_posts = array(); + foreach ( $hits as $i => $hit ) { + $related_posts[] = $this->get_related_post_data_for_post( $hit['id'], $i, $post_id ); + } + return $related_posts; + } + + /** + * Get array of related posts matched by Elasticsearch. + * + * @param int $post_id + * @param int $size + * @param array $filters + * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta + * @return array + */ + protected function _get_related_post_ids( $post_id, $size, array $filters ) { + $now_ts = time(); + $cache_meta_key = '_jetpack_related_posts_cache'; + + $body = array( + 'size' => (int) $size, + ); + + if ( !empty( $filters ) ) + $body['filter'] = array( 'and' => $filters ); + + // Build cache key + $cache_key = md5( serialize( $body ) ); + + // Load all cached values + if ( wp_using_ext_object_cache() ) { + $transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}"; + $cache = get_transient( $transient_name ); + if ( false !== $cache ) { + return $cache; + } + } else { + $cache = get_post_meta( $post_id, $cache_meta_key, true ); + + if ( empty( $cache ) ) + $cache = array(); + + + // Cache is valid! Return cached value. + if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ][ 'expires' ] > $now_ts ) { + return $cache[ $cache_key ][ 'payload' ]; + } + } + + $response = wp_remote_post( + "https://public-api.wordpress.com/rest/v1/sites/{$this->get_blog_id()}/posts/$post_id/related/", + array( + 'timeout' => 10, + 'user-agent' => 'jetpack_related_posts', + 'sslverify' => true, + 'body' => $body, + ) + ); + + // Oh no... return nothing don't cache errors. + if ( is_wp_error( $response ) ) { + if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) ) + return $cache[ $cache_key ][ 'payload' ]; // return stale + else + return array(); + } + + $results = json_decode( wp_remote_retrieve_body( $response ), true ); + $related_posts = array(); + if ( is_array( $results ) && !empty( $results['hits'] ) ) { + foreach( $results['hits'] as $hit ) { + $related_posts[] = array( + 'id' => $hit['fields']['post_id'], + ); + } + } + + // An empty array might indicate no related posts or that posts + // are not yet synced to WordPress.com, so we cache for only 1 + // minute in this case + if ( empty( $related_posts ) ) { + $cache_ttl = 60; + } else { + $cache_ttl = 12 * HOUR_IN_SECONDS; + } + + // Update cache + if ( wp_using_ext_object_cache() ) { + set_transient( $transient_name, $related_posts, $cache_ttl ); + } else { + // Copy all valid cache values + $new_cache = array(); + foreach ( $cache as $k => $v ) { + if ( is_array( $v ) && $v[ 'expires' ] > $now_ts ) { + $new_cache[ $k ] = $v; + } + } + + // Set new cache value + $cache_expires = $cache_ttl + $now_ts; + $new_cache[ $cache_key ] = array( + 'expires' => $cache_expires, + 'payload' => $related_posts, + ); + update_post_meta( $post_id, $cache_meta_key, $new_cache ); + } + + return $related_posts; + } + + /** + * Filter out any hits that are not public anymore. + * + * @param array $related_posts + * @uses get_post_stati, get_post_status + * @return array + */ + protected function _filter_non_public_posts( array $related_posts ) { + $public_stati = get_post_stati( array( 'public' => true ) ); + + $filtered = array(); + foreach ( $related_posts as $hit ) { + if ( in_array( get_post_status( $hit['id'] ), $public_stati ) ) { + $filtered[] = $hit; + } + } + return $filtered; + } + + /** + * Generates a context for the related content (second line in related post output). + * Order of importance: + * - First category (Not 'Uncategorized') + * - First post tag + * - Number of comments + * + * @param int $post_id + * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n + * @return string + */ + protected function _generate_related_post_context( $post_id ) { + $categories = get_the_category( $post_id ); + if ( is_array( $categories ) ) { + foreach ( $categories as $category ) { + if ( 'uncategorized' != $category->slug && '' != trim( $category->name ) ) { + $post_cat_context = sprintf( + esc_html_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ), + $category->name + ); + /** + * Filter the "In Category" line displayed in the post context below each Related Post. + * + * @module related-posts + * + * @since 3.2.0 + * + * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post. + * @param array $category Array containing information about the category. + */ + return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category ); + } + } + } + + $tags = get_the_terms( $post_id, 'post_tag' ); + if ( is_array( $tags ) ) { + foreach ( $tags as $tag ) { + if ( '' != trim( $tag->name ) ) { + $post_tag_context = sprintf( + _x( 'In "%s"', 'in {category/tag name}', 'jetpack' ), + $tag->name + ); + /** + * Filter the "In Tag" line displayed in the post context below each Related Post. + * + * @module related-posts + * + * @since 3.2.0 + * + * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post. + * @param array $tag Array containing information about the tag. + */ + return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag ); + } + } + } + + $comment_count = get_comments_number( $post_id ); + if ( $comment_count > 0 ) { + return sprintf( + _n( 'With 1 comment', 'With %s comments', $comment_count, 'jetpack' ), + number_format_i18n( $comment_count ) + ); + } + + return __( 'Similar post', 'jetpack' ); + } + + /** + * Logs clicks for clickthrough analysis and related result tuning. + * + * @return null + */ + protected function _log_click( $post_id, $to_post_id, $link_position ) { + + } + + /** + * Determines if the current post is able to use related posts. + * + * @uses self::get_options, is_admin, is_single, apply_filters + * @return bool + */ + protected function _enabled_for_request() { + $enabled = is_single() + && ! is_admin() + && ( ! $this->_allow_feature_toggle() || $this->get_option( 'enabled' ) ); + + if ( + class_exists( 'Jetpack_AMP_Support' ) + && Jetpack_AMP_Support::is_amp_request() + ) { + $enabled = false; + } + + /** + * Filter the Enabled value to allow related posts to be shown on pages as well. + * + * @module related-posts + * + * @since 3.3.0 + * + * @param bool $enabled Should Related Posts be enabled on the current page. + */ + return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled ); + } + + /** + * Adds filters and enqueues assets. + * + * @uses self::_enqueue_assets, self::_setup_shortcode, add_filter + * @return null + */ + protected function _action_frontend_init_page() { + $this->_enqueue_assets( true, true ); + $this->_setup_shortcode(); + + add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 ); + } + + /** + * Enqueues assets needed to do async loading of related posts. + * + * @uses wp_enqueue_script, wp_enqueue_style, plugins_url + * @return null + */ + protected function _enqueue_assets( $script, $style ) { + $dependencies = is_customize_preview() ? array( 'customize-base' ) : array( 'jquery' ); + if ( $script ) { + wp_enqueue_script( + 'jetpack_related-posts', + Jetpack::get_file_url_for_environment( + '_inc/build/related-posts/related-posts.min.js', + 'modules/related-posts/related-posts.js' + ), + $dependencies, + self::VERSION + ); + $related_posts_js_options = array( + /** + * Filter each Related Post Heading structure. + * + * @since 4.0.0 + * + * @param string $str Related Post Heading structure. Default to h4. + */ + 'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ), + ); + wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options ); + } + if ( $style ){ + wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION ); + wp_style_add_data( 'jetpack_related-posts', 'rtl', 'replace' ); + } + } + + /** + * Sets up the shortcode processing. + * + * @uses add_filter, add_shortcode + * @return null + */ + protected function _setup_shortcode() { + add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 ); + + add_shortcode( self::SHORTCODE, array( $this, 'get_target_html' ) ); + } + + protected function _allow_feature_toggle() { + if ( null === $this->_allow_feature_toggle ) { + /** + * Filter the display of the Related Posts toggle in Settings > Reading. + * + * @module related-posts + * + * @since 2.8.0 + * + * @param bool false Display a feature toggle. Default to false. + */ + $this->_allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false ); + } + return $this->_allow_feature_toggle; + } + + /** + * =================================================== + * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API + * =================================================== + */ + + /** + * Add Related Posts to the REST API Post response. + * + * @since 4.4.0 + * + * @action rest_api_init + * @uses register_rest_field, self::rest_get_related_posts + * @return null + */ + public function rest_register_related_posts() { + register_rest_field( 'post', + 'jetpack-related-posts', + array( + 'get_callback' => array( $this, 'rest_get_related_posts' ), + 'update_callback' => null, + 'schema' => null, + ) + ); + } + + /** + * Build an array of Related Posts. + * By default returns cached results that are stored for up to 12 hours. + * + * @since 4.4.0 + * + * @param array $object Details of current post. + * @param string $field_name Name of field. + * @param WP_REST_Request $request Current request + * + * @uses self::get_for_post_id + * + * @return array + */ + public function rest_get_related_posts( $object, $field_name, $request ) { + return $this->get_for_post_id( $object['id'], array( 'size' => 6 ) ); + } +} + +class Jetpack_RelatedPosts_Raw extends Jetpack_RelatedPosts { + protected $_query_name; + + /** + * Allows callers of this class to tag each query with a unique name for tracking purposes. + * + * @param string $name + * @return Jetpack_RelatedPosts_Raw + */ + public function set_query_name( $name ) { + $this->_query_name = (string) $name; + return $this; + } + + /** + * The raw related posts class can be used by other plugins or themes + * to get related content. This class wraps the existing RelatedPosts + * logic thus we never want to add anything to the DOM or do anything + * for event hooks. We will also not present any settings for this + * class and keep it enabled as calls to this class is done + * programmatically. + */ + public function action_admin_init() {} + public function action_frontend_init() {} + public function get_options() { + return array( + 'enabled' => true, + ); + } + + /** + * Workhorse method to return array of related posts ids matched by Elasticsearch. + * + * @param int $post_id + * @param int $size + * @param array $filters + * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body + * @return array + */ + protected function _get_related_posts( $post_id, $size, array $filters ) { + $hits = $this->_filter_non_public_posts( + $this->_get_related_post_ids( + $post_id, + $size, + $filters + ) + ); + + /** This filter is already documented in modules/related-posts/related-posts.php */ + $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id ); + + return $hits; + } +} |