PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 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 All 506 releases
← All changes | functions.opengraph.php +514 -57 13.8.3 → 16.3-a.5 View file →
@@ -10,11 +10,99 @@
10 10 *
11 11 * @package automattic/jetpack
12 12 */
13 13
14 +use Automattic\Block_Scanner;
15 +use Automattic\Jetpack\Connection\Manager as Connection_Manager;
16 +use Automattic\Jetpack\Current_Plan;
17 +use Automattic\Jetpack\Post_Media\Images;
18 +use Automattic\Jetpack\Status;
14 19 use Automattic\Jetpack\Status\Host;
15 20
21 +if ( ! defined( 'ABSPATH' ) ) {
22 + exit( 0 );
23 +}
24 +
16 25 add_action( 'wp_head', 'jetpack_og_tags' );
26 +
27 +/**
28 + * Keep Twitter Cards from analyzing the body of a gated post.
29 + *
30 + * Twitter_Cards::twitter_cards_tags() runs at priority 11 and, when a card type
31 + * is already set, emits only the site default image instead of images and
32 + * videos parsed out of the body.
33 + *
34 + * @since 16.2
35 + *
36 + * @param array $tags Open Graph tags.
37 + * @return array
38 + */
39 +function jetpack_og_gate_twitter_cards( $tags ) {
40 + /** This filter is documented in class.jetpack.php */
41 + if ( apply_filters( 'jetpack_disable_twitter_cards', false ) ) {
42 + return $tags;
43 + }
44 +
45 + if ( ! is_singular() ) {
46 + return $tags;
47 + }
48 +
49 + $post = get_post();
50 + if ( ! $post instanceof WP_Post || ! \Automattic\Jetpack\SEO\Content_Gate::is_gated( $post ) ) {
51 + return $tags;
52 + }
53 +
54 + if ( empty( $tags['twitter:text:title'] ) ) {
55 + $tags['twitter:text:title'] = get_the_title( $post );
56 + }
57 +
58 + // Twitter_Cards stops once a card type is set, so carry the featured image
59 + // across here; it is chosen separately and never parsed out of the body.
60 + $card_type = 'summary';
61 + if ( empty( $tags['twitter:image'] ) ) {
62 + $image = Images::get_image(
63 + $post->ID,
64 + array(
65 + 'width' => 144,
66 + 'height' => 144,
67 + 'from_slideshow' => false,
68 + 'from_gallery' => false,
69 + 'from_attachment' => false,
70 + 'from_blocks' => false,
71 + 'from_html' => false,
72 + )
73 + );
74 +
75 + // Twitter's limits: 4096px maximum, and 300x157 minimum for a large card.
76 + if (
77 + ! empty( $image['src'] )
78 + && isset( $image['src_width'] ) && isset( $image['src_height'] )
79 + && (int) $image['src_width'] <= 4096
80 + && (int) $image['src_height'] <= 4096
81 + ) {
82 + if ( (int) $image['src_width'] >= 300 && (int) $image['src_height'] >= 157 ) {
83 + $card_type = 'summary_large_image';
84 + $tags['twitter:image'] = esc_url( add_query_arg( 'w', 640, $image['src'] ) );
85 + } else {
86 + $tags['twitter:image'] = esc_url( add_query_arg( 'w', 144, $image['src'] ) );
87 + }
88 +
89 + if ( ! empty( $image['alt_text'] ) ) {
90 + $alt_length = 420;
91 + $tags['twitter:image:alt'] = strlen( $image['alt_text'] ) > $alt_length
92 + ? esc_attr( mb_substr( $image['alt_text'], 0, $alt_length ) . '…' )
93 + : esc_attr( $image['alt_text'] );
94 + }
95 + }
96 + }
97 +
98 + if ( empty( $tags['twitter:card'] ) ) {
99 + $tags['twitter:card'] = $card_type;
100 + }
101 +
102 + return $tags;
103 +}
104 +add_filter( 'jetpack_open_graph_tags', 'jetpack_og_gate_twitter_cards', 10 );
17 105 add_action( 'web_stories_story_head', 'jetpack_og_tags' );
18 106
19 107 // Add a Fediverse Open Graph Tag when an author has connected their Mastodon account.
20 108 add_filter( 'jetpack_open_graph_tags', 'jetpack_add_fediverse_creator_open_graph_tag', 10, 1 );
@@ -98,16 +186,14 @@
98 186 $tags['og:type'] = 'website';
99 187 $tags['og:title'] = wp_get_document_title();
100 188
101 189 $archive = get_queried_object();
102 - if ( ! empty( $archive ) ) {
103 - if ( is_category() || is_tag() || is_tax() ) {
104 - $tags['og:url'] = get_term_link( $archive->term_id, $archive->taxonomy );
105 - $tags['og:description'] = $archive->description;
106 - } elseif ( is_post_type_archive() ) {
190 + if ( $archive instanceof WP_Term ) {
191 + $tags['og:url'] = get_term_link( $archive->term_id, $archive->taxonomy );
192 + $tags['og:description'] = $archive->description;
193 + } elseif ( ! empty( $archive ) && is_post_type_archive() ) {
107 194 $tags['og:url'] = get_post_type_archive_link( $archive->name );
108 195 $tags['og:description'] = $archive->description;
109 - }
110 196 }
111 197 } elseif ( is_singular() && is_a( $data, 'WP_Post' ) ) {
112 198 $tags['og:type'] = 'article';
113 199 if ( empty( $data->post_title ) ) {
@@ -117,9 +203,9 @@
117 203 $tags['og:title'] = wp_kses( apply_filters( 'the_title', $data->post_title, $data->ID ), array() );
118 204 }
119 205
120 206 $tags['og:url'] = get_permalink( $data->ID );
121 - if ( ! post_password_required() ) {
207 + if ( ! post_password_required( $data ) ) {
122 208 /*
123 209 * If the post author set an excerpt, use that.
124 210 * Otherwise, pick the post content that comes before the More tag if there is one.
125 211 * Do not use the post content if it contains premium content.
@@ -125,11 +211,17 @@
125 211 * Do not use the post content if it contains premium content.
126 212 */
127 213 if ( ! empty( $data->post_excerpt ) ) {
128 214 $tags['og:description'] = jetpack_og_get_description( $data->post_excerpt );
129 - } elseif ( ! has_block( 'premium-content/container', $data->post_content ) ) {
130 - $excerpt = explode( '<!--more-->', $data->post_content )[0];
131 - $tags['og:description'] = jetpack_og_get_description( $excerpt );
215 + } else {
216 + $content = \Automattic\Jetpack\SEO\Content_Gate::is_gated( $data )
217 + ? \Automattic\Jetpack\SEO\Content_Gate::public_teaser( $data )
218 + : $data->post_content;
219 +
220 + if ( '' !== $content ) {
221 + $excerpt = explode( '<!--more-->', $content )[0];
222 + $tags['og:description'] = jetpack_og_get_description( $excerpt );
223 + }
132 224 }
133 225 }
134 226
135 227 $tags['article:published_time'] = gmdate( 'c', strtotime( $data->post_date_gmt ) );
@@ -245,9 +337,9 @@
245 337
246 338 foreach ( (array) $tags as $tag_property => $tag_content ) {
247 339 // to accommodate multiple images.
248 340 $tag_content = (array) $tag_content;
249 - $tag_content = array_unique( $tag_content );
341 + $tag_content = array_unique( array_filter( $tag_content, 'is_scalar' ) );
250 342
251 343 foreach ( $tag_content as $tag_content_single ) {
252 344 if ( empty( $tag_content_single ) && ! in_array( $tag_property, $allowed_empty_tags, true ) ) {
253 345 continue; // Only allow certain empty tags.
@@ -326,20 +418,28 @@
326 418 if ( is_singular() && ! is_home() ) {
327 419 // Grab obvious image if post is an attachment page for an image.
328 420 if ( is_attachment( get_the_ID() ) && str_starts_with( get_post_mime_type(), 'image' ) ) {
329 421 $image['src'] = wp_get_attachment_url( get_the_ID() );
330 - $image['alt_text'] = Jetpack_PostImages::get_alt_text( get_the_ID() );
422 + $image['alt_text'] = Images::get_alt_text( get_the_ID() );
331 423 }
332 424
333 425 // Attempt to find something good for this post using our generalized PostImages code.
334 - if ( empty( $image ) && class_exists( 'Jetpack_PostImages' ) ) {
335 - $post_image = Jetpack_PostImages::get_image(
336 - get_the_ID(),
337 - array(
338 - 'width' => $width,
339 - 'height' => $height,
340 - )
426 + if ( empty( $image ) ) {
427 + $image_args = array(
428 + 'width' => $width,
429 + 'height' => $height,
341 430 );
431 + // Every source but the featured image is parsed out of the body.
432 + if ( \Automattic\Jetpack\SEO\Content_Gate::is_gated( get_post( get_the_ID() ) ) ) {
433 + $image_args += array(
434 + 'from_slideshow' => false,
435 + 'from_gallery' => false,
436 + 'from_attachment' => false,
437 + 'from_blocks' => false,
438 + 'from_html' => false,
439 + );
440 + }
441 + $post_image = Images::get_image( get_the_ID(), $image_args );
342 442 if ( ! empty( $post_image ) && is_array( $post_image ) ) {
343 443 $image['src'] = $post_image['src'];
344 444 if ( isset( $post_image['src_width'] ) && isset( $post_image['src_height'] ) ) {
345 445 $image['width'] = $post_image['src_width'];
@@ -352,30 +452,153 @@
352 452 }
353 453 } elseif ( is_author() ) {
354 454 $author = get_queried_object();
355 455 if ( is_a( $author, 'WP_User' ) ) {
356 - $image['src'] = get_avatar_url(
456 + $image['src'] = get_avatar_url(
357 457 $author->user_email,
358 458 array(
359 459 'size' => $width,
360 460 )
361 461 );
362 - $image['alt_text'] = $author->display_name;
462 + $image['alt_text'] = $author->display_name;
363 463 }
364 464 }
365 465
466 + /*
467 + * Generate a fallback social image,
468 + * dynamically generated based on the site name,
469 + * a representative image of the site,
470 + * and a custom template used by our Social Image Generator.
471 + */
472 + if ( empty( $image ) ) {
473 + $site_image = jetpack_og_get_fallback_social_image( $width, $height );
474 + if ( ! empty( $site_image ) ) {
475 + $image['src'] = $site_image['src'];
476 + $image['width'] = $site_image['width'];
477 + $image['height'] = $site_image['height'];
478 + }
479 + }
480 +
481 + // If we didn't get an explicit alt tag from the image, set a default.
482 + if ( empty( $image['alt_text'] ) ) {
483 + /**
484 + * Filter the default Open Graph image alt text, used when the Open Graph image from the post does not have an alt text.
485 + *
486 + * @since 10.4
487 + *
488 + * @param string $str Default Open Graph image alt text.
489 + */
490 + $image['alt_text'] = apply_filters( 'jetpack_open_graph_image_default_alt_text', '' );
491 + }
492 +
493 + return $image;
494 +}
495 +
496 +/**
497 + * Get a fallback social image for the site.
498 + *
499 + * @since 14.9
500 + *
501 + * @param int $width The width of the image.
502 + * @param int $height The height of the image.
503 + *
504 + * @return array The source ('src'), 'width', 'height', and source type of the image.
505 + */
506 +function jetpack_og_get_fallback_social_image( $width, $height ) {
507 + // Default template.
508 + $template = 'edge';
509 +
510 + // Let's get the site's representative image.
511 + $site_image = jetpack_og_get_site_image( $width, $height );
512 +
513 + /**
514 + * Define your own site's representative image,
515 + * to override any fallback image found by looking through site's logo, site icon, and blavatar.
516 + * This will allow you to overwrite the default fallback image generated dynamically.
517 + *
518 + * @since 15.0
519 + *
520 + * @param array $site_image Your own site's representative image.
521 + * @param array $site_image The site's representative image picked by Jetpack. {
522 + * @type string $src The source of the image.
523 + * @type int $width The width of the image.
524 + * @type int $height The height of the image.
525 + * @type string $type The type of the image.
526 + * }
527 + */
528 + $custom_site_image = apply_filters( 'jetpack_og_default_site_image', array(), $site_image );
529 + if ( ! empty( $custom_site_image['src'] ) ) {
530 + return $custom_site_image;
531 + }
532 +
533 + if ( empty( $site_image['src'] ) ) {
534 + // When using the default blank image, use a different template in Social Image Generator.
535 + $template = 'highway';
536 + $site_image['src'] = jetpack_og_get_site_fallback_blank_image();
537 + }
538 +
539 + // Return the site image if we are in offline mode instead of attempting to generate a dynamic one.
540 + if ( ( new Status() )->is_offline_mode() ) {
541 + return $site_image;
542 + }
543 +
544 + /*
545 + * Only attempt to generate a dynamic fallback image
546 + * if we have a healthy connection to WPCOM.
547 + * and if the site has the right plan.
548 + */
549 + if (
550 + ( ( new Host() )->is_wpcom_simple()
551 + || ( new Connection_Manager() )->is_connected()
552 + )
553 + && Current_Plan::supports( 'social-image-generator' )
554 + ) {
555 + /**
556 + * Allow filtering the template to use with Social Image Generator.
557 + * Available templates: highway, dois, fullscreen, edge.
558 + *
559 + * @since 14.9
560 + *
561 + * @param string $template The template to use.
562 + */
563 + $template = apply_filters( 'jetpack_og_fallback_social_image_template', $template );
564 +
565 + // Let's generate the image.
566 + $site_image = jetpack_og_generate_fallback_social_image( $site_image, $template );
567 + }
568 +
569 + return $site_image;
570 +}
571 +
572 +/**
573 + * Get the site's representative image.
574 + *
575 + * @since 14.9
576 + *
577 + * @param int $width The width of the image.
578 + * @param int $height The height of the image.
579 + *
580 + * @return array The source ('src'), 'width', 'height', and source type of the image.
581 + */
582 +function jetpack_og_get_site_image( $width, $height ) {
366 583 // First fall back, blavatar.
367 - if ( empty( $image ) && function_exists( 'blavatar_domain' ) ) {
584 + if ( function_exists( 'blavatar_domain' ) ) {
368 585 $blavatar_domain = blavatar_domain( site_url() );
369 586 if ( blavatar_exists( $blavatar_domain ) ) {
370 - $image['src'] = blavatar_url( $blavatar_domain, 'img', $width, false, true );
371 - $image['width'] = $width;
372 - $image['height'] = $height;
587 + return array(
588 + 'src' => blavatar_url( $blavatar_domain, 'img', $width, false, true ),
589 + 'width' => $width,
590 + 'height' => $height,
591 + 'type' => 'blavatar',
592 + );
373 593 }
374 594 }
375 595
376 596 // Second fall back, Site Logo.
377 - if ( empty( $image ) && ( function_exists( 'jetpack_has_site_logo' ) && jetpack_has_site_logo() ) ) {
597 + if (
598 + function_exists( 'jetpack_has_site_logo' )
599 + && jetpack_has_site_logo()
600 + ) {
378 601 $image_id = jetpack_get_site_logo( 'id' );
379 602 $logo = wp_get_attachment_image_src( $image_id, 'full' );
380 603 if (
381 604 isset( $logo[0] ) && isset( $logo[1] ) && isset( $logo[2] )
@@ -380,17 +603,39 @@
380 603 if (
381 604 isset( $logo[0] ) && isset( $logo[1] ) && isset( $logo[2] )
382 605 && ( _jetpack_og_get_image_validate_size( $logo[1], $logo[2], $width, $height ) )
383 606 ) {
384 - $image['src'] = $logo[0];
385 - $image['width'] = $logo[1];
386 - $image['height'] = $logo[2];
387 - $image['alt_text'] = Jetpack_PostImages::get_alt_text( $image_id );
607 + return array(
608 + 'src' => $logo[0],
609 + 'width' => $logo[1],
610 + 'height' => $logo[2],
611 + 'type' => 'site_logo',
612 + );
388 613 }
389 614 }
390 615
391 - // Third fall back, Core Site Icon, if valid in size.
392 - if ( empty( $image ) && has_site_icon() ) {
616 + // Third fall back, Core's site logo.
617 + if ( has_custom_logo() ) {
618 + $custom_logo_id = get_theme_mod( 'custom_logo' );
619 + $sl_details = wp_get_attachment_image_src(
620 + $custom_logo_id,
621 + 'full'
622 + );
623 + if (
624 + isset( $sl_details[0] ) && isset( $sl_details[1] ) && isset( $sl_details[2] )
625 + && ( _jetpack_og_get_image_validate_size( $sl_details[1], $sl_details[2], $width, $height ) )
626 + ) {
627 + return array(
628 + 'src' => $sl_details[0],
629 + 'width' => $sl_details[1],
630 + 'height' => $sl_details[2],
631 + 'alt_text' => Images::get_alt_text( $custom_logo_id ),
632 + );
633 + }
634 + }
635 +
636 + // Fourth fall back, Core Site Icon, if valid in size.
637 + if ( has_site_icon() ) {
393 638 $image_id = get_option( 'site_icon' );
394 639 $icon = wp_get_attachment_image_src( $image_id, 'full' );
395 640 if (
396 641 isset( $icon[0] ) && isset( $icon[1] ) && isset( $icon[2] )
@@ -395,43 +640,163 @@
395 640 if (
396 641 isset( $icon[0] ) && isset( $icon[1] ) && isset( $icon[2] )
397 642 && ( _jetpack_og_get_image_validate_size( $icon[1], $icon[2], $width, $height ) )
398 643 ) {
399 - $image['src'] = $icon[0];
400 - $image['width'] = $icon[1];
401 - $image['height'] = $icon[2];
402 - $image['alt_text'] = Jetpack_PostImages::get_alt_text( $image_id );
644 + return array(
645 + 'src' => $icon[0],
646 + 'width' => $icon[1],
647 + 'height' => $icon[2],
648 + 'type' => 'site_icon',
649 + );
403 650 }
404 651 }
405 652
406 - // Final fall back, blank image.
407 - if ( empty( $image ) ) {
408 - /**
409 - * Filter the default Open Graph Image tag, used when no Image can be found in a post.
410 - *
411 - * @since 3.0.0
412 - *
413 - * @param string $str Default Image URL.
414 - */
415 - $image['src'] = apply_filters( 'jetpack_open_graph_image_default', 'https://s0.wp.com/i/blank.jpg' );
653 + return array(
654 + 'src' => '',
655 + 'width' => $width,
656 + 'height' => $height,
657 + 'type' => 'blank',
658 + );
659 +}
660 +
661 +/**
662 + * Get the site's fallback image.
663 + *
664 + * @since 14.9
665 + *
666 + * @return string
667 + */
668 +function jetpack_og_get_site_fallback_blank_image() {
669 + /**
670 + * Filter the default Open Graph Image tag, used when no Image can be found in a post.
671 + *
672 + * @since 3.0.0
673 + *
674 + * @param string $str Default Image URL.
675 + */
676 + return apply_filters( 'jetpack_open_graph_image_default', 'https://s0.wp.com/i/blank.jpg' );
677 +}
678 +
679 +/**
680 + * Get available templates for Social Image Generator.
681 + *
682 + * @since 14.9
683 + *
684 + * @return array The available templates.
685 + */
686 +function jetpack_og_get_available_templates() {
687 + if ( ! class_exists( '\Automattic\Jetpack\Publicize\Social_Image_Generator\Templates' ) ) {
688 + return array();
416 689 }
417 690
418 - // If we didn't get an explicit alt tag from the image, set a default.
419 - if ( empty( $image['alt_text'] ) ) {
420 - /**
421 - * Filter the default Open Graph image alt text, used when the Open Graph image from the post does not have an alt text.
422 - *
423 - * @since 10.4
424 - *
425 - * @param string $str Default Open Graph image alt text.
426 - */
427 - $image['alt_text'] = apply_filters( 'jetpack_open_graph_image_default_alt_text', '' );
691 + return \Automattic\Jetpack\Publicize\Social_Image_Generator\Templates::TEMPLATES;
692 +}
693 +
694 +/**
695 + * Get a social image token from Social Image Generator.
696 + *
697 + * @since 14.9
698 + *
699 + * @param string $site_title The site title.
700 + * @param string $image_url The image URL.
701 + * @param string $template The template to use.
702 + *
703 + * @return string|WP_Error The social image token, or a WP_Error if the token could not be generated.
704 + */
705 +function jetpack_og_get_social_image_token( $site_title, $image_url, $template ) {
706 + // Let's check if we have a cached token.
707 + $cache_key = wp_hash( $site_title . $image_url . $template );
708 + $transient_name = 'jetpack_og_social_image_token_' . $cache_key;
709 + $cached_token = get_transient( $transient_name );
710 +
711 + if ( ! empty( $cached_token ) ) {
712 + return $cached_token;
428 713 }
429 714
430 - return $image;
715 + /**
716 + * Filter the social image token for testing purposes.
717 + *
718 + * @since 14.9
719 + *
720 + * @param string|WP_Error|null $token The token to return, or null to use default behavior.
721 + */
722 + $token = apply_filters( 'jetpack_og_get_social_image_token', null );
723 + if ( null !== $token ) {
724 + return $token;
725 + }
726 +
727 + if ( ! function_exists( '\Automattic\Jetpack\Publicize\Social_Image_Generator\fetch_token' ) ) {
728 + return new WP_Error( 'jetpack_og_get_social_image_token_error', __( 'Social Image Generator is not available.', 'jetpack' ) );
729 + }
730 +
731 + $token = \Automattic\Jetpack\Publicize\Social_Image_Generator\fetch_token( $site_title, $image_url, $template );
732 +
733 + /*
734 + * We want to cache 2 types of responses:
735 + * - Successful responses with a token.
736 + * - WP_Error responses that denote a WordPress.com connection issue.
737 + */
738 + if (
739 + ! is_wp_error( $token )
740 + || (
741 + is_wp_error( $token )
742 + && 'invalid_user_permission_publicize' === $token->get_error_code()
743 + )
744 + ) {
745 + set_transient( $transient_name, $token, DAY_IN_SECONDS );
746 + }
747 +
748 + return $token;
431 749 }
432 750
433 751 /**
752 + * Generate and create a fallback social image.
753 + *
754 + * @since 14.9
755 + *
756 + * @param array $representative_image The representative image of the site.
757 + * @param string $template The template to use.
758 + *
759 + * @return array The source ('src'), 'width', and 'height' of the image.
760 + */
761 +function jetpack_og_generate_fallback_social_image( $representative_image, $template ) {
762 + $site_title = get_bloginfo( 'name' );
763 + $fallback_image = array(
764 + 'src' => $representative_image['src'],
765 + 'width' => $representative_image['width'],
766 + 'height' => $representative_image['height'],
767 + );
768 +
769 + // Ensure that we use a valid template.
770 + if (
771 + ! in_array(
772 + $template,
773 + jetpack_og_get_available_templates(),
774 + true
775 + )
776 + ) {
777 + $template = 'edge';
778 + }
779 +
780 + // Let's generate the token matching the image..
781 + $token = jetpack_og_get_social_image_token( $site_title, $representative_image['src'], $template );
782 +
783 + if ( is_wp_error( $token ) ) {
784 + return $fallback_image;
785 + }
786 +
787 + // Build the image URL and return it.
788 + return array(
789 + 'src' => sprintf(
790 + 'https://s0.wp.com/_si/?t=%s',
791 + $token
792 + ),
793 + 'width' => 1200,
794 + 'height' => 630,
795 + );
796 +}
797 +
798 +/**
434 799 * Validate the width and height against required width and height
435 800 *
436 801 * @param int $width Width of the image.
437 802 * @param int $height Height of the image.
@@ -474,8 +839,9 @@
474 839 * There should be:
475 840 * - no links
476 841 * - no shortcodes
477 842 * - no html tags or their contents
843 + * - no content within wp:query blocks
478 844 * - not too many words.
479 845 *
480 846 * @param string $description Text coming from WordPress (autogenerated or manually generated by author).
481 847 * @param WP_Post|null $data Information about our post.
@@ -482,8 +848,11 @@
482 848 *
483 849 * @return string $description Cleaned up description string.
484 850 */
485 851 function jetpack_og_get_description( $description = '', $data = null ) {
852 + // Remove content within wp:query blocks.
853 + $description = jetpack_og_remove_query_blocks( $description );
854 +
486 855 // Remove tags such as <style or <script.
487 856 $description = wp_strip_all_tags( $description );
488 857
489 858 /*
@@ -537,8 +906,96 @@
537 906 );
538 907 }
539 908
540 909 return $description;
910 +}
911 +
912 +/**
913 + * Remove content within wp:query blocks from the description.
914 + *
915 + * @since 14.9
916 + *
917 + * @param string $description The description text that may contain block markup.
918 + * @return string The description with wp:query blocks removed.
919 + */
920 +function jetpack_og_remove_query_blocks( $description ) {
921 + // Handle non-string input
922 + if ( ! is_string( $description ) ) {
923 + return '';
924 + }
925 +
926 + $output = '';
927 + $offset = 0;
928 + $depth = 0;
929 + $in_query_block = false;
930 +
931 + $scanner = Block_Scanner::create( $description );
932 + if ( ! $scanner ) {
933 + return $description;
934 + }
935 +
936 + while ( $scanner->next_delimiter() ) {
937 + $span = $scanner->get_span();
938 + $match_at = $span->start;
939 + $length = $span->length;
940 +
941 + // Check if this is a query block.
942 + if ( $scanner->is_block_type( 'query' ) ) {
943 + switch ( $scanner->get_delimiter_type() ) {
944 + case Block_Scanner::OPENER:
945 + if ( ! $in_query_block ) {
946 + // Copy content before the query block.
947 + $output .= substr( $description, $offset, $match_at - $offset );
948 + $in_query_block = true;
949 + }
950 + ++$depth;
951 + break;
952 +
953 + case Block_Scanner::CLOSER:
954 + --$depth;
955 + if ( $in_query_block && $depth === 0 ) {
956 + // We've exited the query block, continue from after it.
957 + $in_query_block = false;
958 + $offset = $match_at + $length;
959 +
960 + // Remove extra newline if present
961 + if (
962 + str_starts_with( substr( $description, $offset ), "\n" )
963 + && str_ends_with( $output, "\n" )
964 + ) {
965 + ++$offset;
966 + }
967 + }
968 + break;
969 +
970 + case Block_Scanner::VOID:
971 + // Void query blocks should be removed entirely.
972 + if ( ! $in_query_block ) {
973 + $output .= substr( $description, $offset, $match_at - $offset );
974 + $offset = $match_at + $length;
975 + // Remove extra newline if present
976 + if (
977 + str_starts_with( substr( $description, $offset ), "\n" )
978 + && str_ends_with( $output, "\n" )
979 + ) {
980 + ++$offset;
981 + }
982 + }
983 + break;
984 + }
985 + } elseif ( ! $in_query_block ) {
986 + // Not a query block, copy content including the delimiter if we're not inside a query block.
987 + $output .= substr( $description, $offset, $match_at - $offset + $length );
988 + $offset = $match_at + $length;
989 + }
990 + }
991 +
992 + // Add any remaining content after the last delimiter.
993 + if ( ! $in_query_block ) {
994 + $output .= substr( $description, $offset );
995 + }
996 +
997 + return $output;
541 998 }
542 999
543 1000 /**
544 1001 * Display a Fediverse actor Open Graph tag when the post author has a Mastodon connection.