PluginProbe
ActivityPub / 2.4.0
ActivityPub v2.4.0
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 2.4.0, at includes/class-comment.php

466 lines 12.2 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 \add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 );
23 \add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
24 \add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
25 \add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
26 }
27
28 /**
29 * Filter the comment reply link.
30 *
31 * We don't want to show the comment reply link for federated comments
32 * if the user is disabled for federation.
33 *
34 * @param string $link The HTML markup for the comment reply link.
35 * @param array $args An array of arguments overriding the defaults.
36 * @param WP_Comment $comment The object of the comment being replied.
37 *
38 * @return string The filtered HTML markup for the comment reply link.
39 */
40 public static function comment_reply_link( $link, $args, $comment ) {
41 if ( self::are_comments_allowed( $comment ) ) {
42 $user_id = get_current_user_id();
43 if ( $user_id && self::was_received( $comment ) && \user_can( $user_id, 'activitypub' ) ) {
44 return self::create_fediverse_reply_link( $link, $args );
45 }
46
47 return $link;
48 }
49
50 $attrs = array(
51 'selectedComment' => self::generate_id( $comment ),
52 'commentId' => $comment->comment_ID,
53 );
54
55 $div = sprintf(
56 '<div class="activitypub-remote-reply" data-attrs="%s"></div>',
57 esc_attr( wp_json_encode( $attrs ) )
58 );
59
60 return apply_filters( 'activitypub_comment_reply_link', $div );
61 }
62
63 /**
64 * Create a link to reply to a federated comment.
65 * This function adds a title attribute to the reply link to inform the user
66 * that the comment was received from the fediverse and the reply will be sent
67 * to the original author.
68 *
69 * @param string $link The HTML markup for the comment reply link.
70 * @param array $args The args provided by the `comment_reply_link` filter.
71 *
72 * @return string The modified HTML markup for the comment reply link.
73 */
74 private static function create_fediverse_reply_link( $link, $args ) {
75 $str_to_replace = sprintf( '>%s<', $args['reply_text'] );
76 $replace_with = sprintf(
77 ' title="%s">%s<',
78 esc_attr__( 'This comment was received from the fediverse and your reply will be sent to the original author', 'activitypub' ),
79 esc_html__( 'Reply with federation', 'activitypub' )
80 );
81 return str_replace( $str_to_replace, $replace_with, $link );
82 }
83
84 /**
85 * Check if it is allowed to comment to a comment.
86 *
87 * Checks if the comment is local only or if the user can comment federated comments.
88 *
89 * @param mixed $comment Comment object or ID.
90 *
91 * @return boolean True if the user can comment, false otherwise.
92 */
93 public static function are_comments_allowed( $comment ) {
94 $comment = \get_comment( $comment );
95
96 if ( ! self::was_received( $comment ) ) {
97 return true;
98 }
99
100 $current_user = get_current_user_id();
101
102 if ( ! $current_user ) {
103 return false;
104 }
105
106 if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) {
107 // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
108 $current_user = Users::BLOG_USER_ID;
109 }
110
111 $is_user_disabled = is_user_disabled( $current_user );
112
113 if ( $is_user_disabled ) {
114 return false;
115 }
116
117 return true;
118 }
119
120 /**
121 * Check if a comment is federated.
122 *
123 * We consider a comment federated if comment was received via ActivityPub.
124 *
125 * Use this function to check if it is comment that was received via ActivityPub.
126 *
127 * @param mixed $comment Comment object or ID.
128 *
129 * @return boolean True if the comment is federated, false otherwise.
130 */
131 public static function was_received( $comment ) {
132 $comment = \get_comment( $comment );
133
134 if ( ! $comment ) {
135 return false;
136 }
137
138 $protocol = \get_comment_meta( $comment->comment_ID, 'protocol', true );
139
140 if ( 'activitypub' === $protocol ) {
141 return true;
142 }
143
144 return false;
145 }
146
147 /**
148 * Check if a comment was federated.
149 *
150 * This function checks if a comment was federated via ActivityPub.
151 *
152 * @param mixed $comment Comment object or ID.
153 *
154 * @return boolean True if the comment was federated, false otherwise.
155 */
156 public static function was_sent( $comment ) {
157 $comment = \get_comment( $comment );
158
159 if ( ! $comment ) {
160 return false;
161 }
162
163 $status = \get_comment_meta( $comment->comment_ID, 'activitypub_status', true );
164
165 if ( $status ) {
166 return true;
167 }
168
169 return false;
170 }
171
172 /**
173 * Check if a comment is local only.
174 *
175 * This function checks if a comment is local only and was not sent or received via ActivityPub.
176 *
177 * @param mixed $comment Comment object or ID.
178 *
179 * @return boolean True if the comment is local only, false otherwise.
180 */
181 public static function is_local( $comment ) {
182 if ( self::was_sent( $comment ) || self::was_received( $comment ) ) {
183 return false;
184 }
185
186 return true;
187 }
188
189 /**
190 * Check if a comment should be federated.
191 *
192 * We consider a comment should be federated if it is authored by a user that is
193 * not disabled for federation and if it is a reply directly to the post or to a
194 * federated comment.
195 *
196 * Use this function to check if a comment should be federated.
197 *
198 * @param mixed $comment Comment object or ID.
199 *
200 * @return boolean True if the comment should be federated, false otherwise.
201 */
202 public static function should_be_federated( $comment ) {
203 // we should not federate federated comments
204 if ( self::was_received( $comment ) ) {
205 return false;
206 }
207
208 $comment = \get_comment( $comment );
209 $user_id = $comment->user_id;
210
211 // comments without user can't be federated
212 if ( ! $user_id ) {
213 return false;
214 }
215
216 if ( is_single_user() && \user_can( $user_id, 'publish_posts' ) ) {
217 // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
218 $user_id = Users::BLOG_USER_ID;
219 }
220
221 $is_user_disabled = is_user_disabled( $user_id );
222
223 // user is disabled for federation
224 if ( $is_user_disabled ) {
225 return false;
226 }
227
228 // it is a comment to the post and can be federated
229 if ( empty( $comment->comment_parent ) ) {
230 return true;
231 }
232
233 // check if parent comment is federated
234 $parent_comment = \get_comment( $comment->comment_parent );
235
236 return ! self::is_local( $parent_comment );
237 }
238
239 /**
240 * Examine a comment ID and look up an existing comment it represents.
241 *
242 * @param string $id ActivityPub object ID (usually a URL) to check.
243 *
244 * @return int|boolean Comment ID, or false on failure.
245 */
246 public static function object_id_to_comment( $id ) {
247 $comment_query = new WP_Comment_Query(
248 array(
249 'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
250 'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
251 )
252 );
253
254 if ( ! $comment_query->comments ) {
255 return false;
256 }
257
258 if ( count( $comment_query->comments ) > 1 ) {
259 return false;
260 }
261
262 return $comment_query->comments[0];
263 }
264
265 /**
266 * Verify if URL is a local comment, or if it is a previously received
267 * remote comment (For threading comments locally)
268 *
269 * @param string $url The URL to check.
270 *
271 * @return int comment_ID or null if not found
272 */
273 public static function url_to_commentid( $url ) {
274 if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) {
275 return null;
276 }
277
278 // check for local comment
279 if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
280 $query = \wp_parse_url( $url, \PHP_URL_QUERY );
281
282 if ( $query ) {
283 parse_str( $query, $params );
284
285 if ( ! empty( $params['c'] ) ) {
286 $comment = \get_comment( $params['c'] );
287
288 if ( $comment ) {
289 return $comment->comment_ID;
290 }
291 }
292 }
293 }
294
295 $args = array(
296 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
297 'meta_query' => array(
298 'relation' => 'OR',
299 array(
300 'key' => 'source_url',
301 'value' => $url,
302 ),
303 array(
304 'key' => 'source_id',
305 'value' => $url,
306 ),
307 ),
308 );
309
310 $query = new WP_Comment_Query();
311 $comments = $query->query( $args );
312
313 if ( $comments && is_array( $comments ) ) {
314 return $comments[0]->comment_ID;
315 }
316
317 return null;
318 }
319
320 /**
321 * Filters the CSS classes to add an ActivityPub class.
322 *
323 * @param string[] $classes An array of comment classes.
324 * @param string[] $css_class An array of additional classes added to the list.
325 * @param string $comment_id The comment ID as a numeric string.
326 *
327 * @return string[] An array of classes.
328 */
329 public static function comment_class( $classes, $css_class, $comment_id ) {
330 // check if ActivityPub comment
331 if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
332 $classes[] = 'activitypub-comment';
333 }
334
335 return $classes;
336 }
337
338 /**
339 * Link remote comments to source url.
340 *
341 * @param string $comment_link
342 * @param object|WP_Comment $comment
343 *
344 * @return string $url
345 */
346 public static function remote_comment_link( $comment_link, $comment ) {
347 if ( ! $comment || is_admin() ) {
348 return $comment_link;
349 }
350
351 $comment_meta = \get_comment_meta( $comment->comment_ID );
352
353 if ( ! empty( $comment_meta['source_url'][0] ) ) {
354 return $comment_meta['source_url'][0];
355 } elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
356 return $comment_meta['source_id'][0];
357 }
358
359 return $comment_link;
360 }
361
362
363 /**
364 * Generates an ActivityPub URI for a comment
365 *
366 * @param WP_Comment|int $comment A comment object or comment ID
367 *
368 * @return string ActivityPub URI for comment
369 */
370 public static function generate_id( $comment ) {
371 $comment = \get_comment( $comment );
372 $comment_meta = \get_comment_meta( $comment->comment_ID );
373
374 // show external comment ID if it exists
375 if ( ! empty( $comment_meta['source_id'][0] ) ) {
376 return $comment_meta['source_id'][0];
377 } elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
378 return $comment_meta['source_url'][0];
379 }
380
381 // generate URI based on comment ID
382 return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) );
383 }
384
385 /**
386 * Check if a post has remote comments
387 *
388 * @param int $post_id The post ID.
389 *
390 * @return bool True if the post has remote comments, false otherwise.
391 */
392 private static function post_has_remote_comments( $post_id ) {
393 $comments = \get_comments(
394 array(
395 'post_id' => $post_id,
396 'meta_query' => array(
397 'relation' => 'AND',
398 array(
399 'key' => 'protocol',
400 'value' => 'activitypub',
401 'compare' => '=',
402 ),
403 array(
404 'key' => 'source_id',
405 'compare' => 'EXISTS',
406 ),
407 ),
408 )
409 );
410
411 return ! empty( $comments );
412 }
413
414 /**
415 * Enqueue scripts for remote comments
416 */
417 public static function enqueue_scripts() {
418 if ( ! \is_singular() || \is_user_logged_in() ) {
419 // only on single pages, only for logged out users
420 return;
421 }
422
423 if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) {
424 // post type does not support ActivityPub
425 return;
426 }
427
428 if ( ! \comments_open() || ! \get_comments_number() ) {
429 // no comments, no need to load the script
430 return;
431 }
432
433 if ( ! self::post_has_remote_comments( \get_the_ID() ) ) {
434 // no remote comments, no need to load the script
435 return;
436 }
437
438 $handle = 'activitypub-remote-reply';
439 $data = array(
440 'namespace' => ACTIVITYPUB_REST_NAMESPACE,
441 );
442 $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
443 $asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php';
444
445 if ( \file_exists( $asset_file ) ) {
446 $assets = require_once $asset_file;
447
448 \wp_enqueue_script(
449 $handle,
450 \plugins_url( 'build/remote-reply/index.js', __DIR__ ),
451 $assets['dependencies'],
452 $assets['version'],
453 true
454 );
455 \wp_add_inline_script( $handle, $js, 'before' );
456
457 \wp_enqueue_style(
458 $handle,
459 \plugins_url( 'build/remote-reply/style-index.css', __DIR__ ),
460 [ 'wp-components' ],
461 $assets['version']
462 );
463 }
464 }
465 }
466