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' ) ) { require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media-summary.php'; } // Add Related Posts to the REST API Post response. add_action( 'rest_api_init', array( $this, 'rest_register_related_posts' ) ); } /** * Get the blog ID. * * @return Object current blog id. */ 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 */ 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 an eligible 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 */ 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'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading and checking if we need to generate a list of excuded posts, does not update anything on the site. $excludes = $this->parse_numeric_get_arg( 'relatedposts_exclude' ); $this->action_frontend_init_ajax( $excludes ); } else { if ( isset( $_GET['relatedposts_hit'] ) && isset( $_GET['relatedposts_origin'] ) && isset( $_GET['relatedposts_position'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checking if fields are set to setup tracking, nothing is changing on the site. $this->previous_post_id = (int) $_GET['relatedposts_origin']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- fetching a previous post ID for tracking, nothing is changing on the site. $this->log_click( $this->previous_post_id, get_the_ID(), sanitize_text_field( wp_unslash( $_GET['relatedposts_position'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- logging the click for tracking, nothing is changing on the site. } $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, if the 'get_the_excerpt' * hook is in the current filter list, or if the site is running an FSE/Site Editor theme. * * @filter the_content * * @param string $content Post content. * * @return string */ public function filter_add_target_to_dom( $content ) { // Do not output related posts for ActivityPub requests. if ( function_exists( '\Activitypub\is_activitypub_request' ) && \Activitypub\is_activitypub_request() ) { return $content; } if ( has_block( 'jetpack/related-posts' ) || Blocks::is_fse_theme() ) { return $content; } if ( ! $this->found_shortcode && ! doing_filter( 'get_the_excerpt' ) ) { 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 = $this->get_options(); $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() ), 'isServerRendered' => true, ); 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 - content of the post. * @uses has_shortcode * @return string $content */ 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 * @return 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. * * @return 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 object. * @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'; $title = $related_post['title']; $url = $related_post['url']; $rel = $related_post['rel']; $img = ''; $list = ''; $item_markup = sprintf( '
  • ', esc_attr( $instance_id ) ); // Thumbnail if ( ! empty( $block_attributes['show_thumbnails'] ) && ! empty( $related_post['img']['src'] ) ) { $img = sprintf( '', esc_url( $related_post['img']['src'] ), esc_attr( $related_post['img']['alt_text'] ), ( ! empty( $related_post['img']['srcset'] ) ? 'srcset="' . esc_attr( $related_post['img']['srcset'] ) . '"' : '' ) ); } // Link $item_markup .= sprintf( '', esc_attr( $label_id ), esc_url( $url ), ( ! empty( $rel ) ? 'rel="' . esc_attr( $rel ) . '"' : '' ), esc_html( $title ), $img ); // Date if ( $block_attributes['show_date'] ) { $list .= '
    ' . __( 'Date', 'jetpack' ) . '
    '; $list .= ''; } // Author if ( $block_attributes['show_author'] ) { $list .= '
    ' . __( 'Author', 'jetpack' ) . '
    '; $list .= ''; } // Context if ( ( $block_attributes['show_context'] ) && ! empty( $related_post['block_context'] ) ) { // translators: this is followed by the reason why the item is related to the current post $list .= '
    ' . __( 'In relation to', 'jetpack' ) . '
    '; $list .= ''; } // Metadata if ( ! empty( $list ) ) { $item_markup .= ''; } $item_markup .= '
  • '; return $item_markup; } /** * Render the list of related posts. * * @param array $posts The posts to render into the list. * @param array $block_attributes Block attributes. * @return string */ public function render_post_list( $posts, $block_attributes ) { $markup = ''; foreach ( $posts as $post ) { $markup .= $this->render_block_item( $post, $block_attributes ); } return sprintf( // role="list" is required for accessibility as VoiceOver ignores unstyled lists. '', count( $posts ), $markup ); } /** * Render the related posts markup. * * @param array $attributes Block attributes. * @param string $content String containing the related Posts block content. * @param WP_Block $block The block object. * @return string */ public function render_block( $attributes, $content, $block = null ) { if ( ! jetpack_is_frontend() ) { return $content; } $wrapper_attributes = array(); $post_id = get_the_ID(); $block_attributes = array( 'headline' => isset( $attributes['headline'] ) ? $attributes['headline'] : null, 'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'], 'show_author' => isset( $attributes['displayAuthor'] ) ? (bool) $attributes['displayAuthor'] : false, 'show_headline' => isset( $attributes['displayHeadline'] ) ? (bool) $attributes['displayHeadline'] : false, '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( $post_id, array( 'size' => $block_attributes['size'], 'exclude_post_ids' => $excludes, ) ); if ( empty( $related_posts ) ) { return ''; } $list_markup = $this->render_post_list( $related_posts, $block_attributes ); if ( empty( $attributes['isServerRendered'] ) ) { // The get_server_rendered_html() path won't register a block, // so only apply block supports when not server rendered. $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports(); } $headline_markup = ''; if ( isset( $block ) ) { foreach ( $block->inner_blocks as $inner_block ) { if ( 'core/heading' === $inner_block->name && ! empty( wp_strip_all_tags( $inner_block->inner_html ) ) ) { $headline_markup = trim( $inner_block->inner_html ); break; } } } if ( empty( $headline_markup ) && $block_attributes['show_headline'] === true ) { $headline = $block_attributes['headline']; if ( strlen( trim( $headline ) ) !== 0 ) { $headline_markup = sprintf( '', esc_html( $headline ) ); } } $display_markup = sprintf( '', ! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '', ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '', esc_attr( $block_attributes['layout'] ), $headline_markup, $list_markup, empty( $headline_markup ) ? esc_attr__( 'Related Posts', 'jetpack' ) : esc_attr( wp_strip_all_tags( $headline_markup ) ) ); /** * Filter the output HTML of Related Posts. * * @module related-posts * * @since 10.7 * * @param string $display_markup HTML output of Related Posts. * @param int|false get_the_ID() Post ID of the post for which we are retrieving Related Posts. * @param array $related_posts Array of related posts. * @param array $block_attributes Array of Block attributes. */ return (string) apply_filters( 'jetpack_related_posts_display_markup', $display_markup, $post_id, $related_posts, $block_attributes ); } /** * ======================== * 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 ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- requests are used to generate a list of related posts we want to exclude. if ( is_string( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $result = explode( ',', sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } elseif ( is_array( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $args = array_map( 'sanitize_text_field', wp_unslash( $_GET[ $arg ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $result = array_values( $args ); } $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; } /** * Gets options. * * @param string $option_name - option we want to get. */ 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 - input we're parsing. * @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'] ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- expecting string, but may return bools. $current['enabled'] = true; $current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual $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__ */ 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, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped $ui_settings // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set. ); } else { $template = <<
  • %s
  • EOT; printf( $template, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 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 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set. ); } } /** * Head JS/CSS for admin settings page. * * @uses esc_html__ * @return null */ public function print_setting_head() { // only dislay the Related Posts JavaScript on the Reading Settings Admin Page. $current_screen = get_current_screen(); if ( $current_screen === null ) { 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 = <<