PluginProbe
ActivityPub / 3.2.1
ActivityPub v3.2.1
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
activitypub / includes / class-comment.php

class-comment.php in ActivityPub 3.2.1, at includes/class-comment.php

627 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Activitypub;
4
5 use Activitypub\Collection\Users;
6 use WP_Comment_Query;
7
8 use function Activitypub\is_user_disabled;
9 use function Activitypub\is_single_user;
10
11 /**
12 * ActivityPub Comment Class
13 *
14 * This class is a helper/utils class that provides a collection of static
15 * methods that are used to handle comments.
16 */
17 class Comment {
18 /**
19 * Initialize the class, registering WordPress hooks
20 */
21 public static function init() {
22 self::register_comment_types();
23
24 \add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 );
25 \add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
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 );
31 }
32
33 /**
34 * Filter the comment reply link.
35 *
36 * We don't want to show the comment reply link for federated comments
37 * if the user is disabled for federation.
38 *
39 * @param string $link The HTML markup for the comment reply link.
40 * @param array $args An array of arguments overriding the defaults.
41 * @param WP_Comment $comment The object of the comment being replied.
42 *
43 * @return string The filtered HTML markup for the comment reply link.
44 */
45 public static function comment_reply_link( $link, $args, $comment ) {
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
52 return $link;
53 }
54
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 );
66 }
67
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 /**
90 * Check if it is allowed to comment to a comment.
91 *
92 * Checks if the comment is local only or if the user can comment federated comments.
93 *
94 * @param mixed $comment Comment object or ID.
95 *
96 * @return boolean True if the user can comment, false otherwise.
97 */
98 public static function are_comments_allowed( $comment ) {
99 $comment = \get_comment( $comment );
100
101 if ( ! self::was_received( $comment ) ) {
102 return true;
103 }
104
105 $current_user = get_current_user_id();
106
107 if ( ! $current_user ) {
108 return false;
109 }
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
116 $is_user_disabled = is_user_disabled( $current_user );
117
118 if ( $is_user_disabled ) {
119 return false;
120 }
121
122 return true;
123 }
124
125 /**
126 * Check if a comment is federated.
127 *
128 * We consider a comment federated if comment was received via ActivityPub.
129 *
130 * Use this function to check if it is comment that was received via ActivityPub.
131 *
132 * @param mixed $comment Comment object or ID.
133 *
134 * @return boolean True if the comment is federated, false otherwise.
135 */
136 public static function was_received( $comment ) {
137 $comment = \get_comment( $comment );
138
139 if ( ! $comment ) {
140 return false;
141 }
142
143 $protocol = \get_comment_meta( $comment->comment_ID, 'protocol', true );
144
145 if ( 'activitypub' === $protocol ) {
146 return true;
147 }
148
149 return false;
150 }
151
152 /**
153 * Check if a comment was federated.
154 *
155 * This function checks if a comment was federated via ActivityPub.
156 *
157 * @param mixed $comment Comment object or ID.
158 *
159 * @return boolean True if the comment was federated, false otherwise.
160 */
161 public static function was_sent( $comment ) {
162 $comment = \get_comment( $comment );
163
164 if ( ! $comment ) {
165 return false;
166 }
167
168 $status = \get_comment_meta( $comment->comment_ID, 'activitypub_status', true );
169
170 if ( $status ) {
171 return true;
172 }
173
174 return false;
175 }
176
177 /**
178 * Check if a comment is local only.
179 *
180 * This function checks if a comment is local only and was not sent or received via ActivityPub.
181 *
182 * @param mixed $comment Comment object or ID.
183 *
184 * @return boolean True if the comment is local only, false otherwise.
185 */
186 public static function is_local( $comment ) {
187 if ( self::was_sent( $comment ) || self::was_received( $comment ) ) {
188 return false;
189 }
190
191 return true;
192 }
193
194 /**
195 * Check if a comment should be federated.
196 *
197 * We consider a comment should be federated if it is authored by a user that is
198 * not disabled for federation and if it is a reply directly to the post or to a
199 * federated comment.
200 *
201 * Use this function to check if a comment should be federated.
202 *
203 * @param mixed $comment Comment object or ID.
204 *
205 * @return boolean True if the comment should be federated, false otherwise.
206 */
207 public static function should_be_federated( $comment ) {
208 // we should not federate federated comments
209 if ( self::was_received( $comment ) ) {
210 return false;
211 }
212
213 $comment = \get_comment( $comment );
214 $user_id = $comment->user_id;
215
216 // comments without user can't be federated
217 if ( ! $user_id ) {
218 return false;
219 }
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
226 $is_user_disabled = is_user_disabled( $user_id );
227
228 // user is disabled for federation
229 if ( $is_user_disabled ) {
230 return false;
231 }
232
233 // it is a comment to the post and can be federated
234 if ( empty( $comment->comment_parent ) ) {
235 return true;
236 }
237
238 // check if parent comment is federated
239 $parent_comment = \get_comment( $comment->comment_parent );
240
241 return ! self::is_local( $parent_comment );
242 }
243
244 /**
245 * Examine a comment ID and look up an existing comment it represents.
246 *
247 * @param string $id ActivityPub object ID (usually a URL) to check.
248 *
249 * @return \WP_Comment|false Comment object, or false on failure.
250 */
251 public static function object_id_to_comment( $id ) {
252 $comment_query = new WP_Comment_Query(
253 array(
254 'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
255 'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
256 )
257 );
258
259 if ( ! $comment_query->comments ) {
260 return false;
261 }
262
263 if ( count( $comment_query->comments ) > 1 ) {
264 return false;
265 }
266
267 return $comment_query->comments[0];
268 }
269
270 /**
271 * Verify if URL is a local comment, or if it is a previously received
272 * remote comment (For threading comments locally)
273 *
274 * @param string $url The URL to check.
275 *
276 * @return int comment_ID or null if not found
277 */
278 public static function url_to_commentid( $url ) {
279 if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) {
280 return null;
281 }
282
283 // check for local comment
284 if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
285 $query = \wp_parse_url( $url, \PHP_URL_QUERY );
286
287 if ( $query ) {
288 parse_str( $query, $params );
289
290 if ( ! empty( $params['c'] ) ) {
291 $comment = \get_comment( $params['c'] );
292
293 if ( $comment ) {
294 return $comment->comment_ID;
295 }
296 }
297 }
298 }
299
300 $args = array(
301 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
302 'meta_query' => array(
303 'relation' => 'OR',
304 array(
305 'key' => 'source_url',
306 'value' => $url,
307 ),
308 array(
309 'key' => 'source_id',
310 'value' => $url,
311 ),
312 ),
313 );
314
315 $query = new WP_Comment_Query();
316 $comments = $query->query( $args );
317
318 if ( $comments && is_array( $comments ) ) {
319 return $comments[0]->comment_ID;
320 }
321
322 return null;
323 }
324
325 /**
326 * Filters the CSS classes to add an ActivityPub class.
327 *
328 * @param string[] $classes An array of comment classes.
329 * @param string[] $css_class An array of additional classes added to the list.
330 * @param string $comment_id The comment ID as a numeric string.
331 *
332 * @return string[] An array of classes.
333 */
334 public static function comment_class( $classes, $css_class, $comment_id ) {
335 // check if ActivityPub comment
336 if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
337 $classes[] = 'activitypub-comment';
338 }
339
340 return $classes;
341 }
342
343 /**
344 * Link remote comments to source url.
345 *
346 * @param string $comment_link
347 * @param object|WP_Comment $comment
348 *
349 * @return string $url
350 */
351 public static function remote_comment_link( $comment_link, $comment ) {
352 if ( ! $comment || is_admin() ) {
353 return $comment_link;
354 }
355
356 $comment_meta = \get_comment_meta( $comment->comment_ID );
357
358 if ( ! empty( $comment_meta['source_url'][0] ) ) {
359 return $comment_meta['source_url'][0];
360 } elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
361 return $comment_meta['source_id'][0];
362 }
363
364 return $comment_link;
365 }
366
367
368 /**
369 * Generates an ActivityPub URI for a comment
370 *
371 * @param WP_Comment|int $comment A comment object or comment ID
372 *
373 * @return string ActivityPub URI for comment
374 */
375 public static function generate_id( $comment ) {
376 $comment = \get_comment( $comment );
377 $comment_meta = \get_comment_meta( $comment->comment_ID );
378
379 // show external comment ID if it exists
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];
384 }
385
386 // generate URI based on comment ID
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(
399 array(
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 )
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();
625 }
626 }
627