PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 All 504 releases
← All changes | modules/related-posts/jetpack-related-posts.php +244 -168 12.2.3 → 16.3-a.1 View file →
@@ -6,8 +6,11 @@
6 6 */
7 7
8 8 use Automattic\Jetpack\Assets;
9 9 use Automattic\Jetpack\Blocks;
10 +use Automattic\Jetpack\Post_Media\Images;
11 +use Automattic\Jetpack\SEO\Content_Gate;
12 +use Automattic\Jetpack\Status\Request;
10 13 use Automattic\Jetpack\Sync\Settings;
11 14
12 15 /**
13 16 * The Jetpack_RelatedPosts class.
@@ -12,9 +15,9 @@
12 15 /**
13 16 * The Jetpack_RelatedPosts class.
14 17 */
15 18 class Jetpack_RelatedPosts {
16 - const VERSION = '20211209';
19 + const VERSION = '20240116';
17 20 const SHORTCODE = 'jetpack-related-posts';
18 21
19 22 /**
20 23 * Instance of the class.
@@ -122,37 +125,14 @@
122 125 }
123 126
124 127 // Add Related Posts to the REST API Post response.
125 128 add_action( 'rest_api_init', array( $this, 'rest_register_related_posts' ) );
126 -
127 - Blocks::jetpack_register_block(
128 - 'jetpack/related-posts',
129 - array(
130 - 'render_callback' => array( $this, 'render_block' ),
131 - 'supports' => array(
132 - 'color' => array(
133 - 'gradients' => true,
134 - 'link' => true,
135 - ),
136 - 'spacing' => array(
137 - 'margin' => true,
138 - 'padding' => true,
139 - ),
140 - 'typography' => array(
141 - '__experimentalFontFamily' => true,
142 - 'fontSize' => true,
143 - 'lineHeight' => true,
144 - ),
145 - 'align' => array( 'wide', 'full' ),
146 - ),
147 - )
148 - );
149 129 }
150 130
151 131 /**
152 132 * Get the blog ID.
153 133 *
154 - * @return Object current blog id.
134 + * @return mixed current blog id.
155 135 */
156 136 protected function get_blog_id() {
157 137 return Jetpack_Options::get_option( 'id' );
158 138 }
@@ -219,9 +199,9 @@
219 199 */
220 200 public function get_headline() {
221 201 $options = $this->get_options();
222 202
223 - if ( $options['show_headline'] ) {
203 + if ( ! empty( $options['show_headline'] ) ) {
224 204 $headline = sprintf(
225 205 /** This filter is already documented in modules/sharedaddy/sharing-service.php */
226 206 apply_filters( 'jetpack_sharing_headline_html', '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>', esc_html( $options['headline'] ), 'related-posts' ),
227 207 esc_html( $options['headline'] )
@@ -240,9 +220,9 @@
240 220 * @filter the_content
241 221 *
242 222 * @param string $content Post content.
243 223 *
244 - * @returns string
224 + * @return string
245 225 */
246 226 public function filter_add_target_to_dom( $content ) {
247 227 // Do not output related posts for ActivityPub requests.
248 228 if (
@@ -278,16 +258,16 @@
278 258 'displayThumbnails' => $rp_settings['show_thumbnails'],
279 259 'showHeadline' => $rp_settings['show_headline'],
280 260 'displayDate' => isset( $rp_settings['show_date'] ) ? (bool) $rp_settings['show_date'] : true,
281 261 'displayContext' => isset( $rp_settings['show_context'] ) && $rp_settings['show_context'],
282 - 'postLayout' => isset( $rp_settings['layout'] ) ? $rp_settings['layout'] : 'grid',
283 - 'postsToShow' => isset( $rp_settings['size'] ) ? $rp_settings['size'] : 3,
262 + 'postLayout' => $rp_settings['layout'] ?? 'grid',
263 + 'postsToShow' => $rp_settings['size'] ?? 3,
284 264 /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
285 265 'headline' => apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() ),
286 266 'isServerRendered' => true,
287 267 );
288 268
289 - return $this->render_block( $block_rp_settings );
269 + return $this->render_block( $block_rp_settings, '' );
290 270 }
291 271
292 272 /**
293 273 * Looks for our shortcode on the unfiltered content, this has to execute early.
@@ -340,9 +320,9 @@
340 320
341 321 /**
342 322 * 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.
343 323 *
344 - * @returns string
324 + * @return string
345 325 */
346 326 public function get_client_rendered_html_unsupported() {
347 327 if ( Settings::is_syncing() ) {
348 328 return '';
@@ -358,103 +338,114 @@
358 338
359 339 /**
360 340 * Echoes out items for the Gutenberg block
361 341 *
362 - * @param array $related_post The post oject.
342 + * @param array $related_post The post object.
363 343 * @param array $block_attributes The block attributes.
364 344 */
365 345 public function render_block_item( $related_post, $block_attributes ) {
366 346 $instance_id = 'related-posts-item-' . uniqid();
367 347 $label_id = $instance_id . '-label';
348 + $title = $related_post['title'];
349 + $url = $related_post['url'];
350 + $rel = $related_post['rel'];
351 + $img = '';
352 + $list = '';
368 353
369 354 $item_markup = sprintf(
370 - '<ul id="%1$s" aria-labelledby="%2$s" class="jp-related-posts-i2__post" role="menuitem">',
371 - esc_attr( $instance_id ),
372 - esc_attr( $label_id )
355 + '<li id="%1$s" class="jp-related-posts-i2__post">',
356 + esc_attr( $instance_id )
373 357 );
374 358
375 - $item_markup .= sprintf(
376 - '<li class="jp-related-posts-i2__post-link"><a id="%1$s" href="%2$s" %4$s>%3$s</a></li>',
377 - esc_attr( $label_id ),
378 - esc_url( $related_post['url'] ),
379 - esc_attr( $related_post['title'] ),
380 - ( ! empty( $related_post['rel'] ) ? 'rel="' . esc_attr( $related_post['rel'] ) . '"' : '' )
381 - );
382 -
359 + // Thumbnail
383 360 if ( ! empty( $block_attributes['show_thumbnails'] ) && ! empty( $related_post['img']['src'] ) ) {
384 - $img_link = sprintf(
385 - '<li class="jp-related-posts-i2__post-img-link"><a href="%1$s" %2$s><img src="%3$s" width="%4$s" height="%5$s" alt="%6$s" loading="lazy" /></a></li>',
386 - esc_url( $related_post['url'] ),
387 - ( ! empty( $related_post['rel'] ) ? 'rel="' . esc_attr( $related_post['rel'] ) . '"' : '' ),
361 + $img = sprintf(
362 + '<img loading="lazy" class="jp-related-posts-i2__post-img" src="%1$s" alt="%2$s" %3$s/>',
388 363 esc_url( $related_post['img']['src'] ),
389 - esc_attr( $related_post['img']['width'] ),
390 - esc_attr( $related_post['img']['height'] ),
391 - esc_attr( $related_post['img']['alt_text'] )
364 + esc_attr( $related_post['img']['alt_text'] ),
365 + ( ! empty( $related_post['img']['srcset'] ) ? 'srcset="' . esc_attr( $related_post['img']['srcset'] ) . '"' : '' )
392 366 );
367 + }
393 368
394 - $item_markup .= $img_link;
395 - }
369 + // Link
370 + $item_markup .= sprintf(
371 + '<a id="%1$s" href="%2$s" class="jp-related-posts-i2__post-link" %3$s>%4$s%5$s</a>',
372 + esc_attr( $label_id ),
373 + esc_url( $url ),
374 + ( ! empty( $rel ) ? 'rel="' . esc_attr( $rel ) . '"' : '' ),
375 + esc_html( $title ),
376 + $img
377 + );
396 378
379 + // Date
397 380 if ( $block_attributes['show_date'] ) {
398 - $date_tag = sprintf(
399 - '<li class="jp-related-posts-i2__post-date">%1$s</li>',
400 - esc_html( $related_post['date'] )
401 - );
402 -
403 - $item_markup .= $date_tag;
381 + $list .= '<dt>' . __( 'Date', 'jetpack' ) . '</dt>';
382 + $list .= '<dd class="jp-related-posts-i2__post-date">';
383 + $list .= esc_html( $related_post['date'] );
384 + $list .= '</dd>';
404 385 }
405 386
387 + // Author
406 388 if ( $block_attributes['show_author'] ) {
407 - $author_tag = sprintf(
408 - '<li class="jp-related-posts-i2__post-author">%1$s</li>',
409 - esc_html( $related_post['author'] )
410 - );
411 -
412 - $item_markup .= $author_tag;
389 + $list .= '<dt>' . __( 'Author', 'jetpack' ) . '</dt>';
390 + $list .= '<dd class="jp-related-posts-i2__post-author">';
391 + $list .= esc_html( $related_post['author'] );
392 + $list .= '</dd>';
413 393 }
414 394
395 + // Context
415 396 if ( ( $block_attributes['show_context'] ) && ! empty( $related_post['block_context'] ) ) {
397 + // translators: this is followed by the reason why the item is related to the current post
398 + $list .= '<dt>' . __( 'In relation to', 'jetpack' ) . '</dt>';
399 + $list .= '<dd class="jp-related-posts-i2__post-context">';
400 +
416 401 // Note: The original 'context' value is not used when rendering the block.
417 402 // It is still generated and available for the legacy rendering code path though.
418 403 // See './related-posts.js' for that usage.
419 - $context_tag = '';
420 404 $block_context = $related_post['block_context'];
405 +
421 406 if ( ! empty( $block_context['link'] ) ) {
422 - $context_tag = sprintf(
423 - '<li class="jp-related-posts-i2__post-context"><a href="%1$s">%2$s</a></li>',
407 + $list .= sprintf(
408 + '<a href="%1$s">%2$s</a>',
424 409 esc_url( $block_context['link'] ),
425 410 esc_html( $block_context['text'] )
426 411 );
427 412 } else {
428 - $context_tag = sprintf(
429 - '<li class="jp-related-posts-i2__post-context">%1$s</li>',
430 - esc_html( $block_context['text'] )
431 - );
413 + $list .= esc_html( $block_context['text'] );
432 414 }
433 415
434 - $item_markup .= $context_tag;
416 + $list .= '</dd>';
435 417 }
436 418
437 - $item_markup .= '</ul>';
419 + // Metadata
420 + if ( ! empty( $list ) ) {
421 + $item_markup .= '<dl class="jp-related-posts-i2__post-defs">' . $list . '</dl>';
422 + }
438 423
424 + $item_markup .= '</li>';
425 +
439 426 return $item_markup;
440 427 }
441 428
442 429 /**
443 - * Render a related posts row.
430 + * Render the list of related posts.
444 431 *
445 - * @param array $posts The posts to render into the row.
432 + * @param array $posts The posts to render into the list.
446 433 * @param array $block_attributes Block attributes.
434 + * @return string
447 435 */
448 - public function render_block_row( $posts, $block_attributes ) {
449 - $rows_markup = '';
436 + public function render_post_list( $posts, $block_attributes ) {
437 + $markup = '';
438 +
450 439 foreach ( $posts as $post ) {
451 - $rows_markup .= $this->render_block_item( $post, $block_attributes );
440 + $markup .= $this->render_block_item( $post, $block_attributes );
452 441 }
442 +
453 443 return sprintf(
454 - '<div class="jp-related-posts-i2__row" data-post-count="%1$s">%2$s</div>',
444 + // role="list" is required for accessibility as VoiceOver ignores unstyled lists.
445 + '<ul class="jp-related-posts-i2__list" role="list" data-post-count="%1$s">%2$s</ul>',
455 446 count( $posts ),
456 - $rows_markup
447 + $markup
457 448 );
458 449 }
459 450
460 451 /**
@@ -459,16 +450,22 @@
459 450
460 451 /**
461 452 * Render the related posts markup.
462 453 *
463 - * @param array $attributes Block attributes.
454 + * @param array $attributes Block attributes.
455 + * @param string $content String containing the related Posts block content.
456 + * @param WP_Block $block The block object.
464 457 * @return string
465 458 */
466 - public function render_block( $attributes ) {
459 + public function render_block( $attributes, $content, $block = null ) {
460 + if ( ! Request::is_frontend() ) {
461 + return $content;
462 + }
463 +
467 464 $wrapper_attributes = array();
468 465 $post_id = get_the_ID();
469 466 $block_attributes = array(
470 - 'headline' => isset( $attributes['headline'] ) ? $attributes['headline'] : null,
467 + 'headline' => $attributes['headline'] ?? null,
471 468 'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'],
472 469 'show_author' => isset( $attributes['displayAuthor'] ) ? (bool) $attributes['displayAuthor'] : false,
473 470 'show_headline' => isset( $attributes['displayHeadline'] ) ? (bool) $attributes['displayHeadline'] : false,
474 471 'show_date' => isset( $attributes['displayDate'] ) ? (bool) $attributes['displayDate'] : true,
@@ -486,34 +483,27 @@
486 483 'exclude_post_ids' => $excludes,
487 484 )
488 485 );
489 486
490 - $display_lower_row = $block_attributes['size'] > 3;
491 -
492 487 if ( empty( $related_posts ) ) {
493 488 return '';
494 489 }
495 490
496 - switch ( count( $related_posts ) ) {
497 - case 2:
498 - case 4:
499 - case 5:
500 - $top_row_end = 2;
501 - break;
491 + /*
492 + * The block renders through its own block callback, independently of the
493 + * module's front-end asset gate (enabled_for_request()). That gate only
494 + * enqueues our assets on single posts in classic themes, so a block placed
495 + * on a page (or any view the gate skips) would render as unstyled HTML.
496 + * Enqueue the stylesheet here, whenever the block actually outputs markup,
497 + * to keep it styled everywhere it can be used. We intentionally do not widen
498 + * enabled_for_request() itself: that governs the automatic the_content
499 + * insertion and was deliberately scoped in #39784 to avoid showing related
500 + * posts on classic-theme pages.
501 + */
502 + $this->enqueue_assets( false, true );
502 503
503 - default:
504 - $top_row_end = 3;
505 - break;
506 - }
504 + $list_markup = $this->render_post_list( $related_posts, $block_attributes );
507 505
508 - $upper_row_posts = array_slice( $related_posts, 0, $top_row_end );
509 - $lower_row_posts = array_slice( $related_posts, $top_row_end );
510 -
511 - $rows_markup = $this->render_block_row( $upper_row_posts, $block_attributes );
512 - if ( $display_lower_row ) {
513 - $rows_markup .= $this->render_block_row( $lower_row_posts, $block_attributes );
514 - }
515 -
516 506 if ( empty( $attributes['isServerRendered'] ) ) {
517 507 // The get_server_rendered_html() path won't register a block,
518 508 // so only apply block supports when not server rendered.
519 509 $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
@@ -519,9 +509,19 @@
519 509 $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
520 510 }
521 511
522 512 $headline_markup = '';
523 - if ( $block_attributes['show_headline'] === true ) {
513 +
514 + if ( isset( $block ) ) {
515 + foreach ( $block->inner_blocks as $inner_block ) {
516 + if ( 'core/heading' === $inner_block->name && ! empty( wp_strip_all_tags( $inner_block->inner_html ) ) ) {
517 + $headline_markup = trim( $inner_block->inner_html );
518 + break;
519 + }
520 + }
521 + }
522 +
523 + if ( empty( $headline_markup ) && $block_attributes['show_headline'] ) {
524 524 $headline = $block_attributes['headline'];
525 525 if ( strlen( trim( $headline ) ) !== 0 ) {
526 526 $headline_markup = sprintf(
527 527 '<h3 class="jp-relatedposts-headline">%1$s</h3>',
@@ -528,15 +528,17 @@
528 528 esc_html( $headline )
529 529 );
530 530 }
531 531 }
532 +
532 533 $display_markup = sprintf(
533 - '<nav class="jp-relatedposts-i2%1$s"%2$s data-layout="%3$s">%4$s%5$s</nav>',
534 + '<nav class="jp-relatedposts-i2%1$s"%2$s data-layout="%3$s" aria-label="%6$s">%4$s%5$s</nav>',
534 535 ! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '',
535 536 ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '',
536 537 esc_attr( $block_attributes['layout'] ),
537 538 $headline_markup,
538 - $rows_markup
539 + $list_markup,
540 + empty( $headline_markup ) ? esc_attr__( 'Related Posts', 'jetpack' ) : esc_attr( wp_strip_all_tags( $headline_markup ) )
539 541 );
540 542
541 543 /**
542 544 * Filter the output HTML of Related Posts.
@@ -683,9 +685,9 @@
683 685 $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
684 686 $current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
685 687 $current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
686 688 $current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid';
687 - $current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' );
689 + $current['headline'] = $input['headline'] ?? esc_html__( 'Related', 'jetpack' );
688 690 } else {
689 691 $current['enabled'] = false;
690 692 }
691 693
@@ -700,14 +702,13 @@
700 702 /**
701 703 * HTML for admin settings page.
702 704 *
703 705 * @uses self::get_options, checked, esc_html__
704 - * @returns null
705 706 */
706 707 public function print_setting_html() {
707 708 $options = $this->get_options();
708 709
709 - $ui_settings_template = <<<EOT
710 + $ui_settings_template = <<<'EOT'
710 711 <p class="description">%s</p>
711 712 <ul id="settings-reading-relatedposts-customize">
712 713 <li>
713 714 <label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label>
@@ -741,9 +742,9 @@
741 742 esc_html__( 'Preview:', 'jetpack' )
742 743 );
743 744
744 745 if ( ! $this->allow_feature_toggle() ) {
745 - $template = <<<EOT
746 + $template = <<<'EOT'
746 747 <input type="hidden" name="jetpack_relatedposts[enabled]" value="1" />
747 748 %s
748 749 EOT;
749 750 printf(
@@ -750,9 +751,9 @@
750 751 $template, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
751 752 $ui_settings // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set.
752 753 );
753 754 } else {
754 - $template = <<<EOT
755 + $template = <<<'EOT'
755 756 <ul id="settings-reading-relatedposts">
756 757 <li>
757 758 <label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label>
758 759 </li>
@@ -776,9 +777,9 @@
776 777 /**
777 778 * Head JS/CSS for admin settings page.
778 779 *
779 780 * @uses esc_html__
780 - * @returns null
781 + * @return null
781 782 */
782 783 public function print_setting_head() {
783 784
784 785 // only dislay the Related Posts JavaScript on the Reading Settings Admin Page.
@@ -855,9 +856,9 @@
855 856 $extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }';
856 857 } else {
857 858 $extra_css = '';
858 859 }
859 - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
860 + // phpcs:disable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Escaped above where needed.
860 861 echo <<<EOT
861 862 <style type="text/css">
862 863 #settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); }
863 864 #settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; }
@@ -925,8 +926,9 @@
925 926 .change( update_ui );
926 927 });
927 928 </script>
928 929 EOT;
930 + // phpcs:enable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped
929 931 }
930 932
931 933 /**
932 934 * Gets an array of related posts that match the given post_id.
@@ -943,9 +945,9 @@
943 945 $options['size'] = $args['size'];
944 946 }
945 947
946 948 if (
947 - ! $options['enabled']
949 + empty( $options['enabled'] )
948 950 || 0 === (int) $post_id
949 951 || empty( $options['size'] )
950 952 ) {
951 953 return array();
@@ -1174,8 +1176,9 @@
1174 1176 * NOTE: Calls exit() to end all further processing after payload has been outputed.
1175 1177 *
1176 1178 * @param array $excludes array of post_ids to exclude.
1177 1179 * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
1180 + * @return never
1178 1181 */
1179 1182 protected function action_frontend_init_ajax( array $excludes ) {
1180 1183 define( 'DOING_AJAX', true );
1181 1184
@@ -1301,9 +1304,9 @@
1301 1304 $related_posts[ $index ]['id'] = $real_post->ID;
1302 1305 $related_posts[ $index ]['url'] = esc_url( get_permalink( $real_post ) );
1303 1306 $related_posts[ $index ]['title'] = $this->to_utf8( $this->get_title( $real_post->post_title, $real_post->post_content, $real_post->ID ) );
1304 1307 $related_posts[ $index ]['date'] = get_the_date( '', $real_post );
1305 - $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' );
1308 + $related_posts[ $index ]['excerpt'] = html_entity_decode( $this->to_utf8( $this->get_excerpt( $real_post->post_excerpt, $real_post->post_content, $real_post->ID ) ), ENT_QUOTES, 'UTF-8' );
1306 1309 $related_posts[ $index ]['img'] = $this->generate_related_post_image_params( $real_post->ID );
1307 1310 $related_posts[ $index ]['context'] = $this->generate_related_post_context( $real_post->ID );
1308 1311 }
1309 1312 } else {
@@ -1316,23 +1319,22 @@
1316 1319 }
1317 1320
1318 1321 $response = array(
1319 1322 'version' => self::VERSION,
1320 - 'show_thumbnails' => (bool) $options['show_thumbnails'],
1321 - 'show_date' => (bool) $options['show_date'],
1322 - 'show_context' => (bool) $options['show_context'],
1323 - 'layout' => (string) $options['layout'],
1324 - 'headline' => (string) $options['headline'],
1323 + 'show_thumbnails' => (bool) ( $options['show_thumbnails'] ?? false ),
1324 + 'show_date' => (bool) ( $options['show_date'] ?? true ),
1325 + 'show_context' => (bool) ( $options['show_context'] ?? true ),
1326 + 'layout' => (string) ( $options['layout'] ?? 'grid' ),
1327 + 'headline' => (string) ( $options['headline'] ?? '' ),
1325 1328 'items' => array(),
1326 1329 );
1327 1330
1328 - if ( count( $related_posts ) === $options['size'] ) {
1331 + if ( ! empty( $options['size'] ) && count( $related_posts ) === $options['size'] ) {
1329 1332 $response['items'] = $related_posts;
1330 1333 }
1331 1334
1332 - echo wp_json_encode( $response );
1333 -
1334 - exit();
1335 + // @phan-suppress-next-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
1336 + wp_send_json( $response, null, JSON_UNESCAPED_SLASHES );
1335 1337 }
1336 1338
1337 1339 /**
1338 1340 * Returns a UTF-8 encoded array of post information for the given post_id
@@ -1355,9 +1357,9 @@
1355 1357 'title' => $this->to_utf8( $this->get_title( $post->post_title, $post->post_content, $post->ID ) ),
1356 1358 'author' => $this->generate_related_post_display_author( $post->ID ),
1357 1359 'date' => get_the_date( '', $post->ID ),
1358 1360 'format' => get_post_format( $post->ID ),
1359 - 'excerpt' => html_entity_decode( $this->to_utf8( $this->get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
1361 + 'excerpt' => html_entity_decode( $this->to_utf8( $this->get_excerpt( $post->post_excerpt, $post->post_content, $post->ID ) ), ENT_QUOTES, 'UTF-8' ),
1360 1362 /**
1361 1363 * Filters the rel attribute for the Related Posts' links.
1362 1364 *
1363 1365 * @module related-posts
@@ -1426,11 +1428,14 @@
1426 1428 apply_filters( 'the_title', $post_title, $post_id )
1427 1429 );
1428 1430 }
1429 1431
1430 - $post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1431 - if ( ! empty( $post_title ) ) {
1432 - return $post_title;
1432 + // Same gate as get_excerpt(): this fallback is five words of the raw body.
1433 + if ( ! Content_Gate::is_gated( $post_id ) ) {
1434 + $post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1435 + if ( ! empty( $post_title ) ) {
1436 + return $post_title;
1437 + }
1433 1438 }
1434 1439
1435 1440 return __( 'Untitled Post', 'jetpack' );
1436 1441 }
@@ -1439,16 +1444,20 @@
1439 1444 * Returns a plain text post excerpt for title attribute of links.
1440 1445 *
1441 1446 * @param string $post_excerpt - the post excerpt.
1442 1447 * @param string $post_content - the post content.
1448 + * @param int $post_id - the post ID.
1443 1449 * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
1444 1450 * @return string
1445 1451 */
1446 - protected function get_excerpt( $post_excerpt, $post_content ) {
1447 - if ( empty( $post_excerpt ) ) {
1452 + protected function get_excerpt( $post_excerpt, $post_content, $post_id ) {
1453 + if ( ! empty( $post_excerpt ) ) {
1454 + $excerpt = $post_excerpt;
1455 + } elseif ( Content_Gate::is_gated( $post_id ) ) {
1456 + // The body fall-through never passes through the `the_content` paywall, so ask the gate directly.
1457 + return '';
1458 + } else {
1448 1459 $excerpt = $post_content;
1449 - } else {
1450 - $excerpt = $post_excerpt;
1451 1460 }
1452 1461
1453 1462 return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
1454 1463 }
@@ -1454,12 +1463,12 @@
1454 1463 }
1455 1464
1456 1465 /**
1457 1466 * Generates the thumbnail image to be used for the post. Uses the
1458 - * image as returned by Jetpack_PostImages::get_image()
1467 + * image as returned by Images::get_image()
1459 1468 *
1460 1469 * @param int $post_id - the post ID.
1461 - * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
1470 + * @uses self::get_options, apply_filters, Images::get_image, Images::fit_image_url
1462 1471 * @return string
1463 1472 */
1464 1473 protected function generate_related_post_image_params( $post_id ) {
1465 1474 $image_params = array(
@@ -1492,39 +1501,71 @@
1492 1501 );
1493 1502 }
1494 1503
1495 1504 // Try to get post image.
1496 - if ( class_exists( 'Jetpack_PostImages' ) ) {
1497 - $img_url = '';
1498 - $post_image = Jetpack_PostImages::get_image(
1499 - $post_id,
1500 - $thumbnail_size
1505 + $img_url = '';
1506 + $is_gated = Content_Gate::is_gated( $post_id );
1507 + $image_args = $thumbnail_size;
1508 + if ( $is_gated ) {
1509 + // Restrict to the featured image; every other source parses the body.
1510 + $image_args = array_merge(
1511 + $image_args,
1512 + array(
1513 + 'from_slideshow' => false,
1514 + 'from_gallery' => false,
1515 + 'from_attachment' => false,
1516 + 'from_blocks' => false,
1517 + 'from_html' => false,
1518 + )
1501 1519 );
1520 + }
1521 + $post_image = Images::get_image( $post_id, $image_args );
1502 1522
1503 - if ( is_array( $post_image ) ) {
1504 - $img_url = $post_image['src'];
1505 - } elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
1506 - $media = Jetpack_Media_Summary::get( $post_id );
1523 + if ( is_array( $post_image ) ) {
1524 + $img_url = $post_image['src'];
1525 + } elseif ( ! $is_gated && class_exists( 'Jetpack_Media_Summary' ) ) {
1526 + $media = Jetpack_Media_Summary::get( $post_id );
1507 1527
1508 - if ( is_array( $media ) && ! empty( $media['image'] ) ) {
1509 - $img_url = $media['image'];
1510 - }
1528 + if ( is_array( $media ) && ! empty( $media['image'] ) ) {
1529 + $img_url = $media['image'];
1511 1530 }
1531 + }
1512 1532
1513 - if ( ! empty( $img_url ) ) {
1514 - if ( ! empty( $post_image['alt_text'] ) ) {
1515 - $image_params['alt_text'] = $post_image['alt_text'];
1516 - } else {
1517 - $image_params['alt_text'] = '';
1518 - }
1519 - $image_params['width'] = $thumbnail_size['width'];
1520 - $image_params['height'] = $thumbnail_size['height'];
1521 - $image_params['src'] = Jetpack_PostImages::fit_image_url(
1522 - $img_url,
1523 - $thumbnail_size['width'],
1524 - $thumbnail_size['height']
1525 - );
1533 + if ( ! empty( $img_url ) ) {
1534 + if ( ! empty( $post_image['alt_text'] ) ) {
1535 + $image_params['alt_text'] = $post_image['alt_text'];
1536 + } else {
1537 + $image_params['alt_text'] = '';
1526 1538 }
1539 +
1540 + $thumbnail_width = 0;
1541 + $thumbnail_height = 0;
1542 +
1543 + if ( ! empty( $thumbnail_size['width'] ) ) {
1544 + $thumbnail_width = $thumbnail_size['width'];
1545 + $image_params['width'] = $thumbnail_width;
1546 + }
1547 +
1548 + if ( ! empty( $thumbnail_size['height'] ) ) {
1549 + $thumbnail_height = $thumbnail_size['height'];
1550 + $image_params['height'] = $thumbnail_height;
1551 + }
1552 +
1553 + $image_params['src'] = Images::fit_image_url(
1554 + $img_url,
1555 + $thumbnail_width,
1556 + $thumbnail_height
1557 + );
1558 +
1559 + // Add a srcset to handle zoomed views and high-density screens.
1560 + $srcset = Images::generate_cropped_srcset(
1561 + $post_image,
1562 + $thumbnail_width,
1563 + $thumbnail_height
1564 + );
1565 + if ( ! empty( $srcset ) ) {
1566 + $image_params['srcset'] = $srcset;
1567 + }
1527 1568 }
1528 1569
1529 1570 return $image_params;
1530 1571 }
@@ -1747,10 +1788,10 @@
1747 1788 protected function generate_related_post_context_block( $post_id ) {
1748 1789 $categories = get_the_category( $post_id );
1749 1790 if ( is_array( $categories ) ) {
1750 1791 foreach ( $categories as $category ) {
1751 - $cat_link = get_category_link( $category );
1752 - if ( 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1792 + if ( $category instanceof WP_Term && 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1793 + $cat_link = get_category_link( $category );
1753 1794 return array(
1754 1795 'text' => trim( $category->name ),
1755 1796 'link' => $cat_link,
1756 1797 );
@@ -1759,10 +1800,10 @@
1759 1800 }
1760 1801 $tags = get_the_terms( $post_id, 'post_tag' );
1761 1802 if ( is_array( $tags ) ) {
1762 1803 foreach ( $tags as $tag ) {
1763 - $tag_link = get_tag_link( $tag );
1764 - if ( '' !== trim( $tag->name ) ) {
1804 + if ( $tag instanceof WP_Term && '' !== trim( $tag->name ) ) {
1805 + $tag_link = get_tag_link( $tag );
1765 1806 return array(
1766 1807 'text' => trim( $tag->name ),
1767 1808 'link' => $tag_link,
1768 1809 );
@@ -1803,9 +1844,9 @@
1803 1844 protected function generate_related_post_context( $post_id ) {
1804 1845 $categories = get_the_category( $post_id );
1805 1846 if ( is_array( $categories ) ) {
1806 1847 foreach ( $categories as $category ) {
1807 - if ( 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1848 + if ( $category instanceof WP_Term && 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1808 1849 $post_cat_context = sprintf(
1809 1850 // Translators: The category or tag name.
1810 1851 esc_html_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1811 1852 $category->name
@@ -1827,9 +1868,9 @@
1827 1868
1828 1869 $tags = get_the_terms( $post_id, 'post_tag' );
1829 1870 if ( is_array( $tags ) ) {
1830 1871 foreach ( $tags as $tag ) {
1831 - if ( '' !== trim( $tag->name ) ) {
1872 + if ( $tag instanceof WP_Term && '' !== trim( $tag->name ) ) {
1832 1873 $post_tag_context = sprintf(
1833 1874 // Translators: the category or tag name.
1834 1875 _x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1835 1876 $tag->name
@@ -1873,13 +1914,22 @@
1873 1914
1874 1915 /**
1875 1916 * Determines if the current post is able to use related posts.
1876 1917 *
1877 - * @uses self::get_options, is_admin, is_single, apply_filters
1918 + * @since 14.0 Checks for singular instead of single to allow usage on non-posts CPTs in block themes.
1919 + * @uses self::get_options, is_admin, is_singular, apply_filters
1878 1920 * @return bool
1879 1921 */
1880 1922 protected function enabled_for_request() {
1881 - $enabled = is_single()
1923 + /*
1924 + * On block themes, allow usage on any singular view (post, page, CPT).
1925 + * On classic themes, only allow usage on single posts by default.
1926 + */
1927 + $enabled_on_singular_views = wp_is_block_theme()
1928 + ? is_singular()
1929 + : is_single();
1930 +
1931 + $enabled = $enabled_on_singular_views
1882 1932 && ! is_attachment()
1883 1933 && ! is_admin()
1884 1934 && ! is_embed()
1885 1935 && ( ! $this->allow_feature_toggle() || $this->get_option( 'enabled' ) );
@@ -1884,9 +1934,9 @@
1884 1934 && ! is_embed()
1885 1935 && ( ! $this->allow_feature_toggle() || $this->get_option( 'enabled' ) );
1886 1936
1887 1937 /**
1888 - * Filter the Enabled value to allow related posts to be shown on pages as well.
1938 + * Filter the Enabled value to allow related posts to be selectively enabled/disabled.
1889 1939 *
1890 1940 * @module related-posts
1891 1941 *
1892 1942 * @since 3.3.0
@@ -2015,8 +2065,18 @@
2015 2065 */
2016 2066 public function rest_register_related_posts() {
2017 2067 /** This filter is already documented in class.json-api-endpoints.php */
2018 2068 $post_types = apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) );
2069 +
2070 + /**
2071 + * Filter the post types that are allowed to have related posts.
2072 + *
2073 + * @since 15.3
2074 + *
2075 + * @param array $post_types The post types that are allowed to have related posts.
2076 + */
2077 + $post_types = apply_filters( 'jetpack_related_posts_rest_api_allowed_post_types', $post_types );
2078 +
2019 2079 foreach ( $post_types as $post_type ) {
2020 2080 register_rest_field(
2021 2081 $post_type,
2022 2082 'jetpack-related-posts',
@@ -2041,8 +2101,24 @@
2041 2101 *
2042 2102 * @return array
2043 2103 */
2044 2104 public function rest_get_related_posts( $object ) {
2105 + if ( ! isset( $object['id'] ) ) {
2106 + return array();
2107 + }
2108 +
2109 + // If the Related Posts option is turned off, don't get the related posts.
2110 + $options = \Jetpack_Options::get_option( 'relatedposts', array() );
2111 + if ( empty( $options['enabled'] ) || ! $options['enabled'] ) {
2112 + return array();
2113 + }
2114 +
2115 + // If the current post doesn't contain a Related Posts block, and we're also on an admin page, don't get the related posts.
2116 + // This will ensure that if the feature is enabled, we can still retrieve Related Posts via the REST API.
2117 + if ( ! has_block( 'jetpack/related-posts' ) && is_admin() ) {
2118 + return array();
2119 + }
2120 +
2045 2121 return $this->get_for_post_id( $object['id'], array( 'size' => 6 ) );
2046 2122 }
2047 2123 }
2048 2124