| @@ -8,8 +8,11 @@ | ||
| 8 | 8 | namespace Activitypub; |
| 9 | 9 | |
| 10 | 10 | use Activitypub\Cache\Stats_Image; |
| 11 | 11 | use Activitypub\Collection\Actors; |
| 12 | +use Activitypub\Collection\Followers; | |
| 13 | +use Activitypub\Collection\Following; | |
| 14 | +use Activitypub\Collection\Remote_Actors; | |
| 12 | 15 | |
| 13 | 16 | /** |
| 14 | 17 | * Block class. |
| 15 | 18 | */ |
| @@ -104,24 +107,24 @@ | ||
| 104 | 107 | 'objectType' => \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ), |
| 105 | 108 | 'noteLength' => ACTIVITYPUB_NOTE_LENGTH, |
| 106 | 109 | 'statsImageUrlEndpoint' => Stats_Image::is_available() ? \get_rest_url( null, ACTIVITYPUB_REST_NAMESPACE . '/stats/image-url/{user_id}/{year}' ) : '', |
| 107 | 110 | ); |
| 108 | - wp_localize_script( 'wp-editor', '_activityPubOptions', $data ); | |
| 111 | + \wp_localize_script( 'wp-editor', '_activityPubOptions', $data ); | |
| 109 | 112 | |
| 110 | 113 | // Check for our supported post types. |
| 111 | 114 | $current_screen = \get_current_screen(); |
| 112 | 115 | $ap_post_types = \get_post_types_by_support( 'activitypub' ); |
| 113 | - if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) { | |
| 116 | + if ( ! $current_screen || ! \in_array( $current_screen->post_type, $ap_post_types, true ) ) { | |
| 114 | 117 | return; |
| 115 | 118 | } |
| 116 | 119 | |
| 117 | 120 | $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php'; |
| 118 | - $plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); | |
| 119 | - wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); | |
| 121 | + $plugin_url = \plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); | |
| 122 | + \wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); | |
| 120 | 123 | |
| 121 | 124 | $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/pre-publish-panel/plugin.asset.php'; |
| 122 | - $plugin_url = plugins_url( 'build/pre-publish-panel/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); | |
| 123 | - wp_enqueue_script( 'activitypub-pre-publish-panel', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); | |
| 125 | + $plugin_url = \plugins_url( 'build/pre-publish-panel/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); | |
| 126 | + \wp_enqueue_script( 'activitypub-pre-publish-panel', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); | |
| 124 | 127 | } |
| 125 | 128 | |
| 126 | 129 | /** |
| 127 | 130 | * Enqueue the reply handle script if the in_reply_to GET param is set. |
| @@ -133,10 +136,10 @@ | ||
| 133 | 136 | return; |
| 134 | 137 | } |
| 135 | 138 | |
| 136 | 139 | $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/reply-intent/plugin.asset.php'; |
| 137 | - $plugin_url = plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); | |
| 138 | - wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); | |
| 140 | + $plugin_url = \plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); | |
| 141 | + \wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); | |
| 139 | 142 | } |
| 140 | 143 | |
| 141 | 144 | /** |
| 142 | 145 | * Register the blocks. |
| @@ -288,9 +291,9 @@ | ||
| 288 | 291 | * Register REST fields needed for blocks. |
| 289 | 292 | */ |
| 290 | 293 | public static function register_rest_fields() { |
| 291 | 294 | // Register the post_count field for Follow Me block. |
| 292 | - register_rest_field( | |
| 295 | + \register_rest_field( | |
| 293 | 296 | 'user', |
| 294 | 297 | 'post_count', |
| 295 | 298 | array( |
| 296 | 299 | /** |
| @@ -301,9 +304,9 @@ | ||
| 301 | 304 | * @param \WP_REST_Request $request The request object. |
| 302 | 305 | * @return int The number of published posts. |
| 303 | 306 | */ |
| 304 | 307 | 'get_callback' => static function ( $response, $field_name, $request ) { |
| 305 | - return (int) count_user_posts( $request->get_param( 'id' ), 'post', true ); | |
| 308 | + return (int) \count_user_posts( $request->get_param( 'id' ), 'post', true ); | |
| 306 | 309 | }, |
| 307 | 310 | 'schema' => array( |
| 308 | 311 | 'description' => 'Number of published posts', |
| 309 | 312 | 'type' => 'integer', |
| @@ -319,10 +322,10 @@ | ||
| 319 | 322 | * @param string $user_string The user string. Can be a user ID, 'blog', or 'inherit'. |
| 320 | 323 | * @return int|null The user ID, or null if the 'inherit' string is not supported in this context. |
| 321 | 324 | */ |
| 322 | 325 | public static function get_user_id( $user_string ) { |
| 323 | - if ( is_numeric( $user_string ) ) { | |
| 324 | - return absint( $user_string ); | |
| 326 | + if ( \is_numeric( $user_string ) ) { | |
| 327 | + return \absint( $user_string ); | |
| 325 | 328 | } |
| 326 | 329 | |
| 327 | 330 | // If the user string is 'blog', return the Blog User ID. |
| 328 | 331 | if ( 'blog' === $user_string ) { |
| @@ -334,32 +337,32 @@ | ||
| 334 | 337 | return null; |
| 335 | 338 | } |
| 336 | 339 | |
| 337 | 340 | // For a homepage/front page, if the Blog User is active, use it. |
| 338 | - if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) { | |
| 341 | + if ( ( \is_front_page() || \is_home() ) && ! is_user_type_disabled( 'blog' ) ) { | |
| 339 | 342 | return Actors::BLOG_USER_ID; |
| 340 | 343 | } |
| 341 | 344 | |
| 342 | 345 | // If we're in a loop, use the post author. |
| 343 | - $author_id = get_the_author_meta( 'ID' ); | |
| 346 | + $author_id = \get_the_author_meta( 'ID' ); | |
| 344 | 347 | if ( $author_id ) { |
| 345 | 348 | return $author_id; |
| 346 | 349 | } |
| 347 | 350 | |
| 348 | 351 | // For other pages, the queried object will clue us in. |
| 349 | - $queried_object = get_queried_object(); | |
| 352 | + $queried_object = \get_queried_object(); | |
| 350 | 353 | if ( ! $queried_object ) { |
| 351 | 354 | return null; |
| 352 | 355 | } |
| 353 | 356 | |
| 354 | 357 | // If we're on a user archive page, use that user's ID. |
| 355 | - if ( is_a( $queried_object, 'WP_User' ) ) { | |
| 358 | + if ( \is_a( $queried_object, 'WP_User' ) ) { | |
| 356 | 359 | return $queried_object->ID; |
| 357 | 360 | } |
| 358 | 361 | |
| 359 | 362 | // For a single post, use the post author's ID. |
| 360 | - if ( is_a( $queried_object, 'WP_Post' ) ) { | |
| 361 | - return get_the_author_meta( 'ID' ); | |
| 363 | + if ( \is_a( $queried_object, 'WP_Post' ) ) { | |
| 364 | + return \get_the_author_meta( 'ID' ); | |
| 362 | 365 | } |
| 363 | 366 | |
| 364 | 367 | // We won't properly account for some conditions, like tag archives. |
| 365 | 368 | return null; |
| @@ -380,18 +383,22 @@ | ||
| 380 | 383 | return ''; |
| 381 | 384 | } |
| 382 | 385 | |
| 383 | 386 | $attributes = \wp_parse_args( $attributes ); |
| 384 | - $block_name = 'followers' === $endpoint ? __( 'Followers', 'activitypub' ) : __( 'Following', 'activitypub' ); | |
| 387 | + $block_name = 'followers' === $endpoint ? \__( 'Followers', 'activitypub' ) : \__( 'Following', 'activitypub' ); | |
| 385 | 388 | |
| 386 | 389 | if ( empty( $content ) ) { |
| 387 | 390 | // Fallback for v1.0.0 blocks. |
| 388 | 391 | /* translators: %s: Block type (Followers or Following) */ |
| 389 | - $_title = $attributes['title'] ?? \sprintf( __( 'Fediverse %s', 'activitypub' ), $block_name ); | |
| 392 | + $_title = $attributes['title'] ?? \sprintf( \__( 'Fediverse %s', 'activitypub' ), $block_name ); | |
| 390 | 393 | $content = '<h3 class="wp-block-heading">' . \esc_html( $_title ) . '</h3>'; |
| 391 | 394 | unset( $attributes['title'], $attributes['className'] ); |
| 392 | 395 | } else { |
| 393 | 396 | $content = \implode( PHP_EOL, \wp_list_pluck( $block->parsed_block['innerBlocks'], 'innerHTML' ) ); |
| 397 | + // Hide empty headings. | |
| 398 | + if ( empty( \wp_strip_all_tags( $content ) ) ) { | |
| 399 | + $content = ''; | |
| 400 | + } | |
| 394 | 401 | } |
| 395 | 402 | |
| 396 | 403 | $user_id = self::get_user_id( $attributes['selectedUser'] ); |
| 397 | 404 | if ( \is_null( $user_id ) ) { |
| @@ -414,12 +421,12 @@ | ||
| 414 | 421 | $_show_avatars = (bool) \get_option( 'show_avatars' ); |
| 415 | 422 | |
| 416 | 423 | // Query the appropriate collection. |
| 417 | 424 | if ( 'followers' === $endpoint ) { |
| 418 | - $data = \Activitypub\Collection\Followers::query( $user_id, $_per_page ); | |
| 425 | + $data = Followers::query( $user_id, $_per_page ); | |
| 419 | 426 | $items = $data['followers']; |
| 420 | 427 | } else { |
| 421 | - $data = \Activitypub\Collection\Following::query( $user_id, $_per_page ); | |
| 428 | + $data = Following::query( $user_id, $_per_page ); | |
| 422 | 429 | $items = $data['following']; |
| 423 | 430 | } |
| 424 | 431 | |
| 425 | 432 | // Prepare items data for the Interactivity API context. |
| @@ -424,9 +431,9 @@ | ||
| 424 | 431 | |
| 425 | 432 | // Prepare items data for the Interactivity API context. |
| 426 | 433 | $prepared_items = \array_map( |
| 427 | 434 | static function ( $item ) { |
| 428 | - $actor = \Activitypub\Collection\Remote_Actors::get_actor( $item ); | |
| 435 | + $actor = Remote_Actors::get_actor( $item ); | |
| 429 | 436 | |
| 430 | 437 | // Restrict URLs to http/https schemes to prevent XSS via javascript: URIs. |
| 431 | 438 | $url = object_to_uri( $actor->get_url() ) ?: $actor->get_id(); |
| 432 | 439 | |
| @@ -473,9 +480,9 @@ | ||
| 473 | 480 | ) |
| 474 | 481 | ); |
| 475 | 482 | |
| 476 | 483 | /* translators: %s: Block type (Followers or Following) */ |
| 477 | - $nav_label = \sprintf( __( '%s navigation', 'activitypub' ), $block_name ); | |
| 484 | + $nav_label = \sprintf( \__( '%s navigation', 'activitypub' ), $block_name ); | |
| 478 | 485 | |
| 479 | 486 | \ob_start(); |
| 480 | 487 | ?> |
| 481 | 488 | <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput ?>> |
| @@ -511,10 +518,10 @@ | ||
| 511 | 518 | return $content; |
| 512 | 519 | } |
| 513 | 520 | |
| 514 | 521 | $url = $attrs['url']; |
| 515 | - $shortcode = trim( $content ); | |
| 516 | - $name = trim( $shortcode, ':' ); | |
| 522 | + $shortcode = \trim( $content ); | |
| 523 | + $name = \trim( $shortcode, ':' ); | |
| 517 | 524 | |
| 518 | 525 | /** |
| 519 | 526 | * Filters a remote media URL for caching. |
| 520 | 527 | * |
| @@ -673,13 +680,28 @@ | ||
| 673 | 680 | if ( empty( $attrs['url'] ) ) { |
| 674 | 681 | return null; |
| 675 | 682 | } |
| 676 | 683 | |
| 684 | + /* | |
| 685 | + * In feed contexts (RSS, Atom, and anything else WordPress treats as a feed) the styled | |
| 686 | + * embed card depends on plugin CSS that isn't loaded, so it degrades to an unreadable | |
| 687 | + * wall of text. Substitute the same simplified mention link the federation path uses, | |
| 688 | + * and if the remote lookup fails fall through to the plain `<a class="u-in-reply-to">` | |
| 689 | + * link below so the feed item still surfaces *some* indication that it's a reply. | |
| 690 | + */ | |
| 691 | + if ( \is_feed() ) { | |
| 692 | + $mention = self::generate_reply_link( '', array( 'attrs' => $attrs ) ); | |
| 693 | + if ( ! empty( $mention ) ) { | |
| 694 | + return $mention; | |
| 695 | + } | |
| 696 | + $attrs['embedPost'] = false; | |
| 697 | + } | |
| 698 | + | |
| 677 | 699 | $show_embed = isset( $attrs['embedPost'] ) && $attrs['embedPost']; |
| 678 | 700 | |
| 679 | - $wrapper_attrs = get_block_wrapper_attributes( | |
| 701 | + $wrapper_attrs = \get_block_wrapper_attributes( | |
| 680 | 702 | array( |
| 681 | - 'aria-label' => __( 'Reply', 'activitypub' ), | |
| 703 | + 'aria-label' => \__( 'Reply', 'activitypub' ), | |
| 682 | 704 | 'class' => 'activitypub-reply-block', |
| 683 | 705 | 'data-in-reply-to' => $attrs['url'], |
| 684 | 706 | ) |
| 685 | 707 | ); |
| @@ -690,9 +712,9 @@ | ||
| 690 | 712 | $embed = null; |
| 691 | 713 | if ( $show_embed ) { |
| 692 | 714 | // Use the theme's content width or a reasonable default to avoid narrow embeds. |
| 693 | 715 | $embed_width = ! empty( $GLOBALS['content_width'] ) ? $GLOBALS['content_width'] : 600; |
| 694 | - $embed = wp_oembed_get( $attrs['url'], array( 'width' => $embed_width ) ); | |
| 716 | + $embed = \wp_oembed_get( $attrs['url'], array( 'width' => $embed_width ) ); | |
| 695 | 717 | if ( $embed ) { |
| 696 | 718 | $html .= $embed; |
| 697 | 719 | \wp_enqueue_script( 'wp-embed' ); |
| 698 | 720 | } |
| @@ -699,14 +721,14 @@ | ||
| 699 | 721 | } |
| 700 | 722 | |
| 701 | 723 | // Show the link if embed is not requested or if embed failed. |
| 702 | 724 | if ( ! $show_embed || ! $embed ) { |
| 703 | - $html .= sprintf( | |
| 725 | + $html .= \sprintf( | |
| 704 | 726 | '<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>', |
| 705 | - esc_url( $attrs['url'] ), | |
| 706 | - esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ), | |
| 727 | + \esc_url( $attrs['url'] ), | |
| 728 | + \esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ), | |
| 707 | 729 | // translators: %s is the URL of the post being replied to. |
| 708 | - sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) ) | |
| 730 | + \sprintf( \__( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', \esc_url( $attrs['url'] ) ) ) | |
| 709 | 731 | ); |
| 710 | 732 | } |
| 711 | 733 | |
| 712 | 734 | $html .= '</div>'; |
| @@ -816,9 +838,9 @@ | ||
| 816 | 838 | 'show_avatars' => true, |
| 817 | 839 | 'show_pagination' => true, |
| 818 | 840 | 'total' => 0, |
| 819 | 841 | 'per_page' => 10, |
| 820 | - 'nav_label' => __( 'Actor navigation', 'activitypub' ), | |
| 842 | + 'nav_label' => \__( 'Actor navigation', 'activitypub' ), | |
| 821 | 843 | ); |
| 822 | 844 | |
| 823 | 845 | $args = \wp_parse_args( $args, $defaults ); |
| 824 | 846 | |
| @@ -869,9 +891,9 @@ | ||
| 869 | 891 | <a |
| 870 | 892 | href="#" |
| 871 | 893 | role="button" |
| 872 | 894 | class="pagination-previous" |
| 873 | - data-wp-on-async--click="actions.previousPage" | |
| 895 | + data-wp-on--click="actions.previousPage" | |
| 874 | 896 | data-wp-bind--aria-disabled="state.disablePreviousLink" |
| 875 | 897 | aria-label="<?php \esc_attr_e( 'Previous page', 'activitypub' ); ?>" |
| 876 | 898 | > |
| 877 | 899 | <?php \esc_html_e( 'Previous', 'activitypub' ); ?> |
| @@ -882,9 +904,9 @@ | ||
| 882 | 904 | <a |
| 883 | 905 | href="#" |
| 884 | 906 | role="button" |
| 885 | 907 | class="pagination-next" |
| 886 | - data-wp-on-async--click="actions.nextPage" | |
| 908 | + data-wp-on--click="actions.nextPage" | |
| 887 | 909 | data-wp-bind--aria-disabled="state.disableNextLink" |
| 888 | 910 | aria-label="<?php \esc_attr_e( 'Next page', 'activitypub' ); ?>" |
| 889 | 911 | > |
| 890 | 912 | <?php \esc_html_e( 'Next', 'activitypub' ); ?> |
| @@ -1039,9 +1061,9 @@ | ||
| 1039 | 1061 | return \sprintf( |
| 1040 | 1062 | '<p class="ap-reply-mention"><a rel="mention ugc" href="%1$s" title="%2$s">%3$s</a></p>', |
| 1041 | 1063 | \esc_url( $url ), |
| 1042 | 1064 | \esc_attr( $webfinger ), |
| 1043 | - \esc_html( '@' . strtok( $webfinger, '@' ) ) | |
| 1065 | + \esc_html( '@' . \strtok( $webfinger, '@' ) ) | |
| 1044 | 1066 | ); |
| 1045 | 1067 | } |
| 1046 | 1068 | |
| 1047 | 1069 | /** |
| @@ -1145,9 +1167,13 @@ | ||
| 1145 | 1167 | public static function revert_embed_links( $block_content, $block ) { |
| 1146 | 1168 | if ( ! isset( $block['attrs']['url'] ) ) { |
| 1147 | 1169 | return $block_content; |
| 1148 | 1170 | } |
| 1149 | - return '<p><a href="' . esc_url( $block['attrs']['url'] ) . '">' . $block['attrs']['url'] . '</a></p>'; | |
| 1171 | + | |
| 1172 | + // Escape once and reuse: the URL is also the visible link text, so it must be safe there too. | |
| 1173 | + $url = \esc_url( $block['attrs']['url'] ); | |
| 1174 | + | |
| 1175 | + return '<p><a href="' . $url . '">' . $url . '</a></p>'; | |
| 1150 | 1176 | } |
| 1151 | 1177 | |
| 1152 | 1178 | /** |
| 1153 | 1179 | * Convert HTML content to blocks. |
| @@ -1288,9 +1314,9 @@ | ||
| 1288 | 1314 | // non-ActivityPub post types to avoid a full table scan. |
| 1289 | 1315 | $query_post_type = $query->get( 'post_type' ); |
| 1290 | 1316 | if ( ! empty( $query_post_type ) && 'any' !== $query_post_type ) { |
| 1291 | 1317 | $query_post_types = (array) $query_post_type; |
| 1292 | - if ( ! array_intersect( $query_post_types, \get_post_types_by_support( 'activitypub' ) ) ) { | |
| 1318 | + if ( ! \array_intersect( $query_post_types, \get_post_types_by_support( 'activitypub' ) ) ) { | |
| 1293 | 1319 | return; |
| 1294 | 1320 | } |
| 1295 | 1321 | } |
| 1296 | 1322 | |