PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 14.1.1 All 503 releases
← All changes | functions.opengraph.php +638 -58 12.7.316.2 View file →
@@ -10,11 +10,105 @@
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;
19 +use Automattic\Jetpack\Status\Host;
20 +
21 +if ( ! defined( 'ABSPATH' ) ) {
22 + exit( 0 );
23 +}
24 +
14 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 );
15 105 add_action( 'web_stories_story_head', 'jetpack_og_tags' );
16 106
107 +// Add a Fediverse Open Graph Tag when an author has connected their Mastodon account.
108 +add_filter( 'jetpack_open_graph_tags', 'jetpack_add_fediverse_creator_open_graph_tag', 10, 1 );
109 +add_filter( 'jetpack_open_graph_output', 'jetpack_filter_fediverse_cards_output', 10, 1 );
110 +
17 111 /**
18 112 * Outputs Open Graph tags generated by Jetpack.
19 113 */
20 114 function jetpack_og_tags() {
@@ -92,16 +186,14 @@
92 186 $tags['og:type'] = 'website';
93 187 $tags['og:title'] = wp_get_document_title();
94 188
95 189 $archive = get_queried_object();
96 - if ( ! empty( $archive ) ) {
97 - if ( is_category() || is_tag() || is_tax() ) {
98 - $tags['og:url'] = get_term_link( $archive->term_id, $archive->taxonomy );
99 - $tags['og:description'] = $archive->description;
100 - } 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() ) {
101 194 $tags['og:url'] = get_post_type_archive_link( $archive->name );
102 195 $tags['og:description'] = $archive->description;
103 - }
104 196 }
105 197 } elseif ( is_singular() && is_a( $data, 'WP_Post' ) ) {
106 198 $tags['og:type'] = 'article';
107 199 if ( empty( $data->post_title ) ) {
@@ -111,9 +203,9 @@
111 203 $tags['og:title'] = wp_kses( apply_filters( 'the_title', $data->post_title, $data->ID ), array() );
112 204 }
113 205
114 206 $tags['og:url'] = get_permalink( $data->ID );
115 - if ( ! post_password_required() ) {
207 + if ( ! post_password_required( $data ) ) {
116 208 /*
117 209 * If the post author set an excerpt, use that.
118 210 * Otherwise, pick the post content that comes before the More tag if there is one.
119 211 * Do not use the post content if it contains premium content.
@@ -119,11 +211,17 @@
119 211 * Do not use the post content if it contains premium content.
120 212 */
121 213 if ( ! empty( $data->post_excerpt ) ) {
122 214 $tags['og:description'] = jetpack_og_get_description( $data->post_excerpt );
123 - } elseif ( ! has_block( 'premium-content/container', $data->post_content ) ) {
124 - $excerpt = explode( '<!--more-->', $data->post_content )[0];
125 - $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 + }
126 224 }
127 225 }
128 226
129 227 $tags['article:published_time'] = gmdate( 'c', strtotime( $data->post_date_gmt ) );
@@ -239,9 +337,9 @@
239 337
240 338 foreach ( (array) $tags as $tag_property => $tag_content ) {
241 339 // to accommodate multiple images.
242 340 $tag_content = (array) $tag_content;
243 - $tag_content = array_unique( $tag_content );
341 + $tag_content = array_unique( array_filter( $tag_content, 'is_scalar' ) );
244 342
245 343 foreach ( $tag_content as $tag_content_single ) {
246 344 if ( empty( $tag_content_single ) && ! in_array( $tag_property, $allowed_empty_tags, true ) ) {
247 345 continue; // Only allow certain empty tags.
@@ -318,22 +416,30 @@
318 416 $image = array();
319 417
320 418 if ( is_singular() && ! is_home() ) {
321 419 // Grab obvious image if post is an attachment page for an image.
322 - if ( is_attachment( get_the_ID() ) && 'image' === substr( get_post_mime_type(), 0, 5 ) ) {
420 + if ( is_attachment( get_the_ID() ) && str_starts_with( get_post_mime_type(), 'image' ) ) {
323 421 $image['src'] = wp_get_attachment_url( get_the_ID() );
324 - $image['alt_text'] = Jetpack_PostImages::get_alt_text( get_the_ID() );
422 + $image['alt_text'] = Images::get_alt_text( get_the_ID() );
325 423 }
326 424
327 425 // Attempt to find something good for this post using our generalized PostImages code.
328 - if ( empty( $image ) && class_exists( 'Jetpack_PostImages' ) ) {
329 - $post_image = Jetpack_PostImages::get_image(
330 - get_the_ID(),
331 - array(
332 - 'width' => $width,
333 - 'height' => $height,
334 - )
426 + if ( empty( $image ) ) {
427 + $image_args = array(
428 + 'width' => $width,
429 + 'height' => $height,
335 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 );
336 442 if ( ! empty( $post_image ) && is_array( $post_image ) ) {
337 443 $image['src'] = $post_image['src'];
338 444 if ( isset( $post_image['src_width'] ) && isset( $post_image['src_height'] ) ) {
339 445 $image['width'] = $post_image['src_width'];
@@ -346,30 +452,153 @@
346 452 }
347 453 } elseif ( is_author() ) {
348 454 $author = get_queried_object();
349 455 if ( is_a( $author, 'WP_User' ) ) {
350 - $image['src'] = get_avatar_url(
456 + $image['src'] = get_avatar_url(
351 457 $author->user_email,
352 458 array(
353 459 'size' => $width,
354 460 )
355 461 );
356 - $image['alt_text'] = $author->display_name;
462 + $image['alt_text'] = $author->display_name;
357 463 }
358 464 }
359 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 ) {
360 583 // First fall back, blavatar.
361 - if ( empty( $image ) && function_exists( 'blavatar_domain' ) ) {
584 + if ( function_exists( 'blavatar_domain' ) ) {
362 585 $blavatar_domain = blavatar_domain( site_url() );
363 586 if ( blavatar_exists( $blavatar_domain ) ) {
364 - $image['src'] = blavatar_url( $blavatar_domain, 'img', $width, false, true );
365 - $image['width'] = $width;
366 - $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 + );
367 593 }
368 594 }
369 595
370 596 // Second fall back, Site Logo.
371 - 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 + ) {
372 601 $image_id = jetpack_get_site_logo( 'id' );
373 602 $logo = wp_get_attachment_image_src( $image_id, 'full' );
374 603 if (
375 604 isset( $logo[0] ) && isset( $logo[1] ) && isset( $logo[2] )
@@ -374,17 +603,39 @@
374 603 if (
375 604 isset( $logo[0] ) && isset( $logo[1] ) && isset( $logo[2] )
376 605 && ( _jetpack_og_get_image_validate_size( $logo[1], $logo[2], $width, $height ) )
377 606 ) {
378 - $image['src'] = $logo[0];
379 - $image['width'] = $logo[1];
380 - $image['height'] = $logo[2];
381 - $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 + );
382 613 }
383 614 }
384 615
385 - // Third fall back, Core Site Icon, if valid in size.
386 - 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() ) {
387 638 $image_id = get_option( 'site_icon' );
388 639 $icon = wp_get_attachment_image_src( $image_id, 'full' );
389 640 if (
390 641 isset( $icon[0] ) && isset( $icon[1] ) && isset( $icon[2] )
@@ -389,43 +640,163 @@
389 640 if (
390 641 isset( $icon[0] ) && isset( $icon[1] ) && isset( $icon[2] )
391 642 && ( _jetpack_og_get_image_validate_size( $icon[1], $icon[2], $width, $height ) )
392 643 ) {
393 - $image['src'] = $icon[0];
394 - $image['width'] = $icon[1];
395 - $image['height'] = $icon[2];
396 - $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 + );
397 650 }
398 651 }
399 652
400 - // Final fall back, blank image.
401 - if ( empty( $image ) ) {
402 - /**
403 - * Filter the default Open Graph Image tag, used when no Image can be found in a post.
404 - *
405 - * @since 3.0.0
406 - *
407 - * @param string $str Default Image URL.
408 - */
409 - $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();
410 689 }
411 690
412 - // If we didn't get an explicit alt tag from the image, set a default.
413 - if ( empty( $image['alt_text'] ) ) {
414 - /**
415 - * Filter the default Open Graph image alt text, used when the Open Graph image from the post does not have an alt text.
416 - *
417 - * @since 10.4
418 - *
419 - * @param string $str Default Open Graph image alt text.
420 - */
421 - $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;
422 713 }
423 714
424 - 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;
425 749 }
426 750
427 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 +/**
428 799 * Validate the width and height against required width and height
429 800 *
430 801 * @param int $width Width of the image.
431 802 * @param int $height Height of the image.
@@ -468,8 +839,9 @@
468 839 * There should be:
469 840 * - no links
470 841 * - no shortcodes
471 842 * - no html tags or their contents
843 + * - no content within wp:query blocks
472 844 * - not too many words.
473 845 *
474 846 * @param string $description Text coming from WordPress (autogenerated or manually generated by author).
475 847 * @param WP_Post|null $data Information about our post.
@@ -476,8 +848,11 @@
476 848 *
477 849 * @return string $description Cleaned up description string.
478 850 */
479 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 +
480 855 // Remove tags such as <style or <script.
481 856 $description = wp_strip_all_tags( $description );
482 857
483 858 /*
@@ -531,5 +906,210 @@
531 906 );
532 907 }
533 908
534 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;
998 +}
999 +
1000 +/**
1001 + * Display a Fediverse actor Open Graph tag when the post author has a Mastodon connection.
1002 + *
1003 + * @see https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/
1004 + *
1005 + * @since 13.8
1006 + *
1007 + * @param array $tags Current tags.
1008 + *
1009 + * @return array
1010 + */
1011 +function jetpack_add_fediverse_creator_open_graph_tag( $tags ) {
1012 + /*
1013 + * Let's not do this on WordPress.com Simple for now,
1014 + * because of its performance impact.
1015 + * See p1723574138779019/1723572983.803009-slack-C01U2KGS2PQ
1016 + */
1017 + if ( ( new Host() )->is_wpcom_simple() ) {
1018 + return $tags;
1019 + }
1020 +
1021 + // Let's not add any tags when the ActivityPub plugin already adds its own.
1022 + $is_activitypub_opengraph_integration_active = get_option( 'activitypub_use_opengraph' );
1023 + if ( $is_activitypub_opengraph_integration_active ) {
1024 + return $tags;
1025 + }
1026 +
1027 + // We pull the Mastodon connection data from Publicize.
1028 + if ( ! function_exists( 'publicize_init' ) ) {
1029 + return $tags;
1030 + }
1031 + $publicize = publicize_init();
1032 +
1033 + global $post;
1034 + if (
1035 + ! is_singular()
1036 + || ! $post instanceof WP_Post
1037 + || ! isset( $post->ID )
1038 + || empty( $post->post_author )
1039 + ) {
1040 + return $tags;
1041 + }
1042 +
1043 + $post_mastodon_connections = array();
1044 +
1045 + // Loop through active connections.
1046 + foreach ( (array) $publicize->get_services( 'connected' ) as $service_name => $connections ) {
1047 + if ( 'mastodon' !== $service_name ) {
1048 + continue;
1049 + }
1050 +
1051 + // services can have multiple connections. Store them all in our array.
1052 + foreach ( $connections as $connection ) {
1053 + $connection_id = $publicize->get_connection_id( $connection );
1054 + $connection_meta = $publicize->get_connection_meta( $connection );
1055 +
1056 + $connection_data = $connection_meta['connection_data'] ?? array();
1057 + $mastodon_handle = $connection_meta['external_display'] ?? '';
1058 + $connection_user_id = $connection_data['user_id'] ?? 0;
1059 +
1060 + if ( empty( $mastodon_handle ) ) {
1061 + continue;
1062 + }
1063 +
1064 + // Did we skip this connection for this post?
1065 + if ( get_post_meta( $post->ID, $publicize->POST_SKIP_PUBLICIZE . $connection_id, true ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1066 + continue;
1067 + }
1068 +
1069 + $post_mastodon_connections[] = array(
1070 + 'user_id' => (int) $connection_user_id,
1071 + 'connection_id' => (int) $connection_id,
1072 + 'handle' => $mastodon_handle,
1073 + 'global' => 0 === (int) $connection_user_id,
1074 + );
1075 + }
1076 + }
1077 +
1078 + // If we have no Mastodon connections, skip.
1079 + if ( empty( $post_mastodon_connections ) ) {
1080 + return $tags;
1081 + }
1082 +
1083 + /*
1084 + * Select a single Mastodon connection to use.
1085 + * It should be either the first connection belonging to the post author,
1086 + * or the first global connection.
1087 + */
1088 + foreach ( $post_mastodon_connections as $mastodon_connection ) {
1089 + if ( (int) $post->post_author === $mastodon_connection['user_id'] ) {
1090 + $tags['fediverse:creator'] = esc_attr( $mastodon_connection['handle'] );
1091 + break;
1092 + }
1093 +
1094 + if ( $mastodon_connection['global'] ) {
1095 + $tags['fediverse:creator'] = esc_attr( $mastodon_connection['handle'] );
1096 + break;
1097 + }
1098 + }
1099 +
1100 + return $tags;
1101 +}
1102 +
1103 +/**
1104 + * Update the markup for the Open Graph tag to match the expected output for Mastodon
1105 + * (name instead of property).
1106 + *
1107 + * @since 13.8
1108 + *
1109 + * @param string $og_tag A single OG tag.
1110 + *
1111 + * @return string Result of the OG tag.
1112 + */
1113 +function jetpack_filter_fediverse_cards_output( $og_tag ) {
1114 + return ( str_contains( $og_tag, 'fediverse:' ) ) ? preg_replace( '/property="([^"]+)"/', 'name="\1"', $og_tag ) : $og_tag;
535 1115 }