PluginProbe
ActivityPub / 3.2.5
ActivityPub v3.2.5
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
← All changes | includes/class-comment.php +299 -11 2.1.13.2.5 View file →
@@ -1,11 +1,13 @@
1 1 <?php
2 2
3 3 namespace Activitypub;
4 4
5 +use Activitypub\Collection\Users;
5 6 use WP_Comment_Query;
6 7
7 8 use function Activitypub\is_user_disabled;
9 +use function Activitypub\is_single_user;
8 10
9 11 /**
10 12 * ActivityPub Comment Class
11 13 *
@@ -16,11 +18,17 @@
16 18 /**
17 19 * Initialize the class, registering WordPress hooks
18 20 */
19 21 public static function init() {
22 + self::register_comment_types();
23 +
20 24 \add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 );
21 25 \add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
22 26 \add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
27 + \add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
28 + \add_action( 'pre_get_comments', array( static::class, 'comment_query' ) );
29 +
30 + \add_filter( 'get_avatar_comment_types', array( static::class, 'get_avatar_comment_types' ), 99 );
23 31 }
24 32
25 33 /**
26 34 * Filter the comment reply link.
@@ -35,15 +43,51 @@
35 43 * @return string The filtered HTML markup for the comment reply link.
36 44 */
37 45 public static function comment_reply_link( $link, $args, $comment ) {
38 46 if ( self::are_comments_allowed( $comment ) ) {
47 + $user_id = get_current_user_id();
48 + if ( $user_id && self::was_received( $comment ) && \user_can( $user_id, 'activitypub' ) ) {
49 + return self::create_fediverse_reply_link( $link, $args );
50 + }
51 +
39 52 return $link;
40 53 }
41 54
42 - return apply_filters( 'activitypub_comment_reply_link', '' );
55 + $attrs = array(
56 + 'selectedComment' => self::generate_id( $comment ),
57 + 'commentId' => $comment->comment_ID,
58 + );
59 +
60 + $div = sprintf(
61 + '<div class="activitypub-remote-reply" data-attrs="%s"></div>',
62 + esc_attr( wp_json_encode( $attrs ) )
63 + );
64 +
65 + return apply_filters( 'activitypub_comment_reply_link', $div );
43 66 }
44 67
45 68 /**
69 + * Create a link to reply to a federated comment.
70 + * This function adds a title attribute to the reply link to inform the user
71 + * that the comment was received from the fediverse and the reply will be sent
72 + * to the original author.
73 + *
74 + * @param string $link The HTML markup for the comment reply link.
75 + * @param array $args The args provided by the `comment_reply_link` filter.
76 + *
77 + * @return string The modified HTML markup for the comment reply link.
78 + */
79 + private static function create_fediverse_reply_link( $link, $args ) {
80 + $str_to_replace = sprintf( '>%s<', $args['reply_text'] );
81 + $replace_with = sprintf(
82 + ' title="%s">%s<',
83 + esc_attr__( 'This comment was received from the fediverse and your reply will be sent to the original author', 'activitypub' ),
84 + esc_html__( 'Reply with federation', 'activitypub' )
85 + );
86 + return str_replace( $str_to_replace, $replace_with, $link );
87 + }
88 +
89 + /**
46 90 * Check if it is allowed to comment to a comment.
47 91 *
48 92 * Checks if the comment is local only or if the user can comment federated comments.
49 93 *
@@ -63,8 +107,13 @@
63 107 if ( ! $current_user ) {
64 108 return false;
65 109 }
66 110
111 + if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) {
112 + // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
113 + $current_user = Users::BLOG_USER_ID;
114 + }
115 +
67 116 $is_user_disabled = is_user_disabled( $current_user );
68 117
69 118 if ( $is_user_disabled ) {
70 119 return false;
@@ -168,8 +217,13 @@
168 217 if ( ! $user_id ) {
169 218 return false;
170 219 }
171 220
221 + if ( is_single_user() && \user_can( $user_id, 'publish_posts' ) ) {
222 + // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
223 + $user_id = Users::BLOG_USER_ID;
224 + }
225 +
172 226 $is_user_disabled = is_user_disabled( $user_id );
173 227
174 228 // user is disabled for federation
175 229 if ( $is_user_disabled ) {
@@ -191,9 +245,9 @@
191 245 * Examine a comment ID and look up an existing comment it represents.
192 246 *
193 247 * @param string $id ActivityPub object ID (usually a URL) to check.
194 248 *
195 - * @return int|boolean Comment ID, or false on failure.
249 + * @return \WP_Comment|false Comment object, or false on failure.
196 250 */
197 251 public static function object_id_to_comment( $id ) {
198 252 $comment_query = new WP_Comment_Query(
199 253 array(
@@ -226,9 +280,9 @@
226 280 return null;
227 281 }
228 282
229 283 // check for local comment
230 - if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
284 + if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
231 285 $query = \wp_parse_url( $url, \PHP_URL_QUERY );
232 286
233 287 if ( $query ) {
234 288 parse_str( $query, $params );
@@ -318,21 +372,255 @@
318 372 *
319 373 * @return string ActivityPub URI for comment
320 374 */
321 375 public static function generate_id( $comment ) {
322 - $comment = get_comment( $comment );
376 + $comment = \get_comment( $comment );
377 + $comment_meta = \get_comment_meta( $comment->comment_ID );
323 378
324 379 // show external comment ID if it exists
325 - $source_id = get_comment_meta( $comment->comment_ID, 'source_id', true );
326 - if ( ! empty( $source_id ) ) {
327 - return $source_id;
380 + if ( ! empty( $comment_meta['source_id'][0] ) ) {
381 + return $comment_meta['source_id'][0];
382 + } elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
383 + return $comment_meta['source_url'][0];
328 384 }
329 385
330 386 // generate URI based on comment ID
331 - return \add_query_arg(
387 + return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) );
388 + }
389 +
390 + /**
391 + * Check if a post has remote comments
392 + *
393 + * @param int $post_id The post ID.
394 + *
395 + * @return bool True if the post has remote comments, false otherwise.
396 + */
397 + private static function post_has_remote_comments( $post_id ) {
398 + $comments = \get_comments(
332 399 array(
333 - 'c' => $comment->comment_ID,
334 - ),
335 - \trailingslashit( site_url() )
400 + 'post_id' => $post_id,
401 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
402 + 'meta_query' => array(
403 + 'relation' => 'AND',
404 + array(
405 + 'key' => 'protocol',
406 + 'value' => 'activitypub',
407 + 'compare' => '=',
408 + ),
409 + array(
410 + 'key' => 'source_id',
411 + 'compare' => 'EXISTS',
412 + ),
413 + ),
414 + )
336 415 );
416 +
417 + return ! empty( $comments );
418 + }
419 +
420 + /**
421 + * Enqueue scripts for remote comments
422 + */
423 + public static function enqueue_scripts() {
424 + if ( ! \is_singular() || \is_user_logged_in() ) {
425 + // only on single pages, only for logged out users
426 + return;
427 + }
428 +
429 + if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) {
430 + // post type does not support ActivityPub
431 + return;
432 + }
433 +
434 + if ( ! \comments_open() || ! \get_comments_number() ) {
435 + // no comments, no need to load the script
436 + return;
437 + }
438 +
439 + if ( ! self::post_has_remote_comments( \get_the_ID() ) ) {
440 + // no remote comments, no need to load the script
441 + return;
442 + }
443 +
444 + $handle = 'activitypub-remote-reply';
445 + $data = array(
446 + 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
447 + );
448 + $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
449 + $asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php';
450 +
451 + if ( \file_exists( $asset_file ) ) {
452 + $assets = require_once $asset_file;
453 +
454 + \wp_enqueue_script(
455 + $handle,
456 + \plugins_url( 'build/remote-reply/index.js', __DIR__ ),
457 + $assets['dependencies'],
458 + $assets['version'],
459 + true
460 + );
461 + \wp_add_inline_script( $handle, $js, 'before' );
462 +
463 + \wp_enqueue_style(
464 + $handle,
465 + \plugins_url( 'build/remote-reply/style-index.css', __DIR__ ),
466 + [ 'wp-components' ],
467 + $assets['version']
468 + );
469 + }
470 + }
471 +
472 + /**
473 + * Return the registered custom comment types.
474 + *
475 + * @return array The registered custom comment types
476 + */
477 + public static function get_comment_types() {
478 + global $activitypub_comment_types;
479 +
480 + return $activitypub_comment_types;
481 + }
482 +
483 + /**
484 + * Is this a registered comment type
485 + *
486 + * @param string $slug The name of the type
487 + * @return boolean True if registered.
488 + */
489 + public static function is_registered_comment_type( $slug ) {
490 + $slug = strtolower( $slug );
491 + $slug = sanitize_key( $slug );
492 +
493 + return in_array( $slug, array_keys( self::get_comment_types() ), true );
494 + }
495 +
496 + /**
497 + * Return the registered custom comment types names.
498 + *
499 + * @return array The registered custom comment type names
500 + */
501 + public static function get_comment_type_names() {
502 + return array_values( wp_list_pluck( self::get_comment_types(), 'type' ) );
503 + }
504 +
505 + /**
506 + * Get a comment type
507 + *
508 + * @param string $type The comment type
509 + *
510 + * @return array The comment type
511 + */
512 + public static function get_comment_type( $type ) {
513 + $type = strtolower( $type );
514 + $type = sanitize_key( $type );
515 + $types = self::get_comment_types();
516 +
517 + if ( in_array( $type, array_keys( $types ), true ) ) {
518 + $type_array = $types[ $type ];
519 + } else {
520 + $type_array = array();
521 + }
522 +
523 + return apply_filters( "activitypub_comment_type_{$type}", $type_array );
524 + }
525 +
526 + /**
527 + * Get a comment type attribute
528 + *
529 + * @param string $type The comment type
530 + * @param string $attr The attribute to get
531 + *
532 + * @return mixed The value of the attribute
533 + */
534 + public static function get_comment_type_attr( $type, $attr ) {
535 + $type_array = self::get_comment_type( $type );
536 +
537 + if ( $type_array && isset( $type_array[ $attr ] ) ) {
538 + $value = $type_array[ $attr ];
539 + } else {
540 + $value = '';
541 + }
542 +
543 + return apply_filters( "activitypub_comment_type_{$attr}", $value, $type );
544 + }
545 +
546 +
547 +
548 + /**
549 + * Register the comment types used by the ActivityPub plugin
550 + *
551 + * @return void
552 + */
553 + public static function register_comment_types() {
554 + register_comment_type(
555 + 'announce',
556 + array(
557 + 'label' => __( 'Reposts', 'activitypub' ),
558 + 'singular' => __( 'Repost', 'activitypub' ),
559 + 'description' => __( 'A repost on the indieweb is a post that is purely a 100% re-publication of another (typically someone else\'s) post.', 'activitypub' ),
560 + 'icon' => '♻️',
561 + 'class' => 'p-repost',
562 + 'type' => 'repost',
563 + // translators: %1$s username, %2$s opject format (post, audio, ...), %3$s URL, %4$s domain
564 + 'excerpt' => __( '&hellip; reposted this!', 'activitypub' ),
565 + )
566 + );
567 +
568 + register_comment_type(
569 + 'like',
570 + array(
571 + 'label' => __( 'Likes', 'activitypub' ),
572 + 'singular' => __( 'Like', 'activitypub' ),
573 + 'description' => __( 'A like is a popular webaction button and in some cases post type on various silos such as Facebook and Instagram.', 'activitypub' ),
574 + 'icon' => '👍',
575 + 'class' => 'p-like',
576 + 'type' => 'like',
577 + // translators: %1$s username, %2$s opject format (post, audio, ...), %3$s URL, %4$s domain
578 + 'excerpt' => __( '&hellip; liked this!', 'activitypub' ),
579 + )
580 + );
581 + }
582 +
583 + /**
584 + * Show avatars on Activities if set
585 + *
586 + * @param array $types list of avatar enabled comment types
587 + *
588 + * @return array show avatars on Activities
589 + */
590 + public static function get_avatar_comment_types( $types ) {
591 + $comment_types = self::get_comment_type_names();
592 + $types = array_merge( $types, $comment_types );
593 +
594 + return array_unique( $types );
595 + }
596 +
597 + /**
598 + * Excludes likes and reposts from comment queries.
599 + *
600 + * @author Jan Boddez
601 + *
602 + * @see https://github.com/janboddez/indieblocks/blob/a2d59de358031056a649ee47a1332ce9e39d4ce2/includes/functions.php#L423-L432
603 + *
604 + * @param WP_Comment_Query $query Comment count.
605 + */
606 + public static function comment_query( $query ) {
607 + if ( ! $query instanceof WP_Comment_Query ) {
608 + return;
609 + }
610 +
611 + if ( is_admin() || ! is_singular() ) {
612 + return;
613 + }
614 +
615 + if ( ! empty( $query->query_vars['type__in'] ) ) {
616 + return;
617 + }
618 +
619 + if ( isset( $query->query_vars['count'] ) && true === $query->query_vars['count'] ) {
620 + return;
621 + }
622 +
623 + // Exclude likes and reposts by the Webmention plugin.
624 + $query->query_vars['type__not_in'] = self::get_comment_type_names();
337 625 }
338 626 }