_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', '' . __( 'Related posts', 'jetpack' ) . '', 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_client_rendered_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', '', 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 Post content. * * @returns string */ public function filter_add_target_to_dom( $content ) { if ( has_block( 'jetpack/related-posts', $content ) ) { return $content; } if ( ! $this->_found_shortcode ) { if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) { $content .= "\n" . $this->get_server_rendered_html(); } else { $content .= "\n" . $this->get_client_rendered_html(); } } return $content; } /** * Render static markup based on the Gutenberg block code * * @return string Rendered related posts HTML. */ public function get_server_rendered_html() { $rp_settings = Jetpack_Options::get_option( 'relatedposts', array() ); $block_rp_settings = array( 'displayThumbnails' => $rp_settings['show_thumbnails'], 'showHeadline' => $rp_settings['show_headline'], 'displayDate' => isset( $rp_settings['show_date'] ) ? (bool) $rp_settings['show_date'] : true, 'displayContext' => isset( $rp_settings['show_context'] ) && $rp_settings['show_context'], 'postLayout' => isset( $rp_settings['layout'] ) ? $rp_settings['layout'] : 'grid', 'postsToShow' => isset( $rp_settings['size'] ) ? $rp_settings['size'] : 3, /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */ 'headline' => apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() ), ); return $this->render_block( $block_rp_settings ); } /** * 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_client_rendered_html() { if ( 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 << $headline 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_client_rendered_html_unsupported() { if ( Settings::is_syncing() ) { return ''; } return "\n\n\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( ''; 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( '', count( $posts ), $rows_markup ); } /** * Render the related posts markup. * * @param array $attributes Block attributes. * @return string */ public function render_block( $attributes ) { $block_attributes = array( 'headline' => isset( $attributes['headline'] ) ? $attributes['headline'] : null, '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' ); $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 ); $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 ); } /* * 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/ticket/45495 and * made it into WordPress 5.2. * * @todo update when WP 5.2 is the minimum support version. */ $priority = has_filter( 'the_content', 'wpautop' ); remove_filter( 'the_content', 'wpautop', $priority ); add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); return sprintf( '', esc_attr( $block_attributes['layout'] ), $block_attributes['headline'], $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 = <<%s

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 = << %s EOT; printf( $template, $ui_settings ); } else { $template = <<
  • %s
  • 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( '', 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 = <<