PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.2
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/collection/class-interactions.php +61 -139 9.2.08.0.2 View file →
@@ -9,13 +9,13 @@
9 9
10 10 use Activitypub\Comment;
11 11 use Activitypub\Emoji;
12 12 use Activitypub\Webfinger;
13 +use WP_Comment_Query;
13 14
14 15 use function Activitypub\get_remote_metadata_by_actor;
15 16 use function Activitypub\is_ap_post;
16 17 use function Activitypub\is_post_disabled;
17 -use function Activitypub\is_same_host;
18 18 use function Activitypub\object_id_to_comment;
19 19 use function Activitypub\object_to_uri;
20 20 use function Activitypub\url_to_commentid;
21 21
@@ -28,30 +28,15 @@
28 28
29 29 /**
30 30 * Add a comment to a post.
31 31 *
32 - * When $user_id is provided, comment author data is built from the
33 - * local WordPress user instead of fetching remote actor metadata.
32 + * @param array $activity The activity-object.
34 33 *
35 - * @param array $activity The activity-object.
36 - * @param int|null $user_id Optional. Local user ID for outbox replies.
37 - *
38 34 * @return int|false|\WP_Error The comment ID or false or WP_Error on failure.
39 35 */
40 - public static function add_comment( $activity, $user_id = null ) {
41 - /*
42 - * A remote comment is stored under its object id (source_id); that id must be on
43 - * the signature-verified actor's host. Otherwise a remote server could file a
44 - * comment whose recorded id points at a different host, mis-recording its
45 - * provenance and taking over that id (the update owner-check would then reject the
46 - * genuine author). Local outbox replies ($user_id set) are trusted.
47 - */
48 - if ( null === $user_id && ! is_same_host( $activity['actor'] ?? '', $activity['object'] ?? '' ) ) {
49 - return false;
50 - }
36 + public static function add_comment( $activity ) {
37 + $comment_data = self::activity_to_comment( $activity );
51 38
52 - $comment_data = self::activity_to_comment( $activity, $user_id );
53 -
54 39 if ( ! $comment_data ) {
55 40 return false;
56 41 }
57 42
@@ -85,9 +70,9 @@
85 70 $comment_post_id = \url_to_postid( $target_url );
86 71
87 72 if ( ! $comment_post_id ) {
88 73 // Check for `ap_post`.
89 - $comment_post = Remote_Posts::get_by_guid( $target_url );
74 + $comment_post = Posts::get_by_guid( $target_url );
90 75 if ( $comment_post instanceof \WP_Post ) {
91 76 $comment_post_id = $comment_post->ID;
92 77 }
93 78 }
@@ -118,12 +103,8 @@
118 103 */
119 104 public static function update_comment( $activity ) {
120 105 $meta = get_remote_metadata_by_actor( $activity['actor'] );
121 106
122 - if ( \is_wp_error( $meta ) || ! \is_array( $meta ) ) {
123 - return $meta;
124 - }
125 -
126 107 // Determine comment_ID.
127 108 $comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
128 109 $comment_data = \get_comment( $comment, ARRAY_A );
129 110
@@ -130,38 +111,13 @@
130 111 if ( ! $comment_data ) {
131 112 return false;
132 113 }
133 114
134 - /*
135 - * Only the comment's author may update it. The comment maps to the remote actor that
136 - * created it via _activitypub_remote_actor_id; that actor post's guid is the
137 - * (signature-bound) actor URI. The Update's actor must match it, otherwise a remote
138 - * server could rewrite another actor's comment by sending an Update whose object.id
139 - * points at it.
140 - *
141 - * Comments created before this mapping existed have no owner recorded; those are let
142 - * through for backward compatibility (matching the Undo path) rather than becoming
143 - * permanently un-editable. On mismatch, return a WP_Error rather than false: false would
144 - * make the Update handler fall back to Create (which re-dispatches to Update for an
145 - * existing comment and recurses), while the unchanged comment array would be read as a
146 - * successful update and relayed onward. A WP_Error is handled but unsuccessful: no Create
147 - * fallback, and the handled-update success flag stays false.
148 - */
149 - $owner = \get_post( (int) \get_comment_meta( $comment_data['comment_ID'], '_activitypub_remote_actor_id', true ) );
150 - if ( $owner instanceof \WP_Post && object_to_uri( $activity['actor'] ) !== $owner->guid ) {
151 - return new \WP_Error(
152 - 'activitypub_update_forbidden',
153 - \__( 'The Update actor does not own the target comment.', 'activitypub' )
154 - );
155 - }
156 -
157 115 // Found a local comment id.
158 - $comment_data['comment_author'] = \sanitize_text_field( empty( $meta['name'] ) ? $meta['preferredUsername'] : $meta['name'] );
116 + $comment_data['comment_author'] = \esc_attr( empty( $meta['name'] ) ? $meta['preferredUsername'] : $meta['name'] );
159 117
160 - /*
161 - * Wrap emoji in content with blocks for runtime replacement.
162 - * Note: Remote images in comments are stripped for security (only emoji allowed).
163 - */
118 + // Wrap emoji in content with blocks for runtime replacement.
119 + // Note: Remote images in comments are stripped for security (only emoji allowed).
164 120 $content = Emoji::wrap_in_content( $activity['object']['content'], $activity['object'] );
165 121 $comment_data['comment_content'] = \addslashes( $content );
166 122
167 123 return self::persist( $comment_data, self::UPDATE );
@@ -174,17 +130,8 @@
174 130 *
175 131 * @return array|string|int|\WP_Error|false Comment data or `false` on failure.
176 132 */
177 133 public static function add_reaction( $activity ) {
178 - /*
179 - * The reaction is stored under its own id (source_id); that id must be on the
180 - * signature-verified actor's host, so a remote server cannot file a reaction
181 - * whose recorded id points at a different host and take over that id.
182 - */
183 - if ( ! is_same_host( $activity['actor'] ?? '', $activity['id'] ?? '' ) ) {
184 - return false;
185 - }
186 -
187 134 $url = object_to_uri( $activity['object'] );
188 135 $comment_post_id = \url_to_postid( $url );
189 136 $parent_comment_id = url_to_commentid( $url );
190 137
@@ -189,9 +136,9 @@
189 136 $parent_comment_id = url_to_commentid( $url );
190 137
191 138 if ( ! $comment_post_id ) {
192 139 // Check for `ap_post`.
193 - $comment_post = Remote_Posts::get_by_guid( $url );
140 + $comment_post = Posts::get_by_guid( $url );
194 141 if ( $comment_post instanceof \WP_Post ) {
195 142 $comment_post_id = $comment_post->ID;
196 143 }
197 144 }
@@ -256,9 +203,9 @@
256 203 ),
257 204 ),
258 205 );
259 206
260 - $query = new \WP_Comment_Query( $args );
207 + $query = new WP_Comment_Query( $args );
261 208 return $query->comments;
262 209 }
263 210
264 211 /**
@@ -286,9 +233,9 @@
286 233 public static function get_by_actor( $actor ) {
287 234 $meta = get_remote_metadata_by_actor( $actor );
288 235
289 236 // Get URL, because $actor seems to be the ID.
290 - if ( $meta && ! \is_wp_error( $meta ) && isset( $meta['url'] ) ) {
237 + if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
291 238 $actor = object_to_uri( $meta['url'] );
292 239 }
293 240
294 241 $args = array(
@@ -365,19 +312,19 @@
365 312 return $allowed_tags;
366 313 }
367 314
368 315 // Add `p` and `br` to the list of allowed tags.
369 - if ( ! \array_key_exists( 'br', $allowed_tags ) ) {
316 + if ( ! array_key_exists( 'br', $allowed_tags ) ) {
370 317 $allowed_tags['br'] = array();
371 318 }
372 319
373 - if ( ! \array_key_exists( 'p', $allowed_tags ) ) {
320 + if ( ! array_key_exists( 'p', $allowed_tags ) ) {
374 321 $allowed_tags['p'] = array();
375 322 }
376 323
377 324 // Add `img` for custom emoji support with strict validation.
378 325 $emoji_html = Emoji::get_kses_allowed_html();
379 - if ( ! \array_key_exists( 'img', $allowed_tags ) ) {
326 + if ( ! array_key_exists( 'img', $allowed_tags ) ) {
380 327 $allowed_tags['img'] = $emoji_html['img'];
381 328 }
382 329
383 330 return $allowed_tags;
@@ -383,71 +330,50 @@
383 330 return $allowed_tags;
384 331 }
385 332
386 333 /**
387 - * Convert an Activity to a WP_Comment.
334 + * Convert an Activity to a WP_Comment
388 335 *
389 - * When $user_id is provided, comment author data is built from the
390 - * local WordPress user instead of fetching remote actor metadata.
336 + * @param array $activity The Activity array.
391 337 *
392 - * @param array $activity The Activity array.
393 - * @param int|null $user_id Optional. Local user ID for outbox comments.
394 - *
395 338 * @return array|false The comment data or false on failure.
396 339 */
397 - public static function activity_to_comment( $activity, $user_id = null ) {
340 + public static function activity_to_comment( $activity ) {
398 341 $comment_content = null;
342 + $actor = object_to_uri( $activity['actor'] ?? null );
343 + $actor = get_remote_metadata_by_actor( $actor );
399 344
400 - if ( $user_id ) {
401 - // Outbox: resolve author from the local WordPress user.
402 - $user = \get_userdata( $user_id );
345 + // Check Actor-Meta.
346 + if ( ! $actor || is_wp_error( $actor ) ) {
347 + return false;
348 + }
403 349
404 - if ( ! $user ) {
405 - return false;
406 - }
350 + // Check Actor-Name.
351 + $comment_author = null;
352 + if ( ! empty( $actor['name'] ) ) {
353 + $comment_author = $actor['name'];
354 + } elseif ( ! empty( $actor['preferredUsername'] ) ) {
355 + $comment_author = $actor['preferredUsername'];
356 + }
407 357
408 - $comment_author = $user->display_name;
409 - $comment_author_url = $user->user_url;
410 - $comment_author_email = $user->user_email;
411 - $comment_content = \wp_kses_post( $activity['object']['content'] ?? '' );
412 - } else {
413 - // S2S: resolve author from remote actor metadata.
414 - $actor = object_to_uri( $activity['actor'] ?? null );
415 - $actor = get_remote_metadata_by_actor( $actor );
358 + if ( empty( $comment_author ) && \get_option( 'require_name_email' ) ) {
359 + return false;
360 + }
416 361
417 - if ( ! $actor || \is_wp_error( $actor ) ) {
418 - return false;
419 - }
362 + $url = object_to_uri( $actor['url'] ?? $actor['id'] );
420 363
421 - $comment_author = null;
422 - if ( ! empty( $actor['name'] ) ) {
423 - $comment_author = $actor['name'];
424 - } elseif ( ! empty( $actor['preferredUsername'] ) ) {
425 - $comment_author = $actor['preferredUsername'];
426 - }
364 + if ( isset( $activity['object']['content'] ) ) {
365 + // Wrap emoji in content with blocks for runtime replacement.
366 + // Note: Remote images in comments are stripped for security (only emoji allowed).
367 + $content = Emoji::wrap_in_content( $activity['object']['content'], $activity['object'] );
368 + $comment_content = \addslashes( $content );
369 + }
427 370
428 - if ( empty( $comment_author ) && \get_option( 'require_name_email' ) ) {
429 - return false;
430 - }
431 -
432 - $comment_author = $comment_author ?? \__( 'Anonymous', 'activitypub' );
433 - $comment_author_url = \esc_url_raw( object_to_uri( $actor['url'] ?? $actor['id'] ) );
434 -
435 - $webfinger = Webfinger::uri_to_acct( $comment_author_url );
436 - if ( \is_wp_error( $webfinger ) ) {
437 - $comment_author_email = '';
438 - } else {
439 - $comment_author_email = \str_replace( 'acct:', '', $webfinger );
440 - }
441 -
442 - if ( isset( $activity['object']['content'] ) ) {
443 - /*
444 - * Wrap emoji in content with blocks for runtime replacement.
445 - * Note: Remote images in comments are stripped for security (only emoji allowed).
446 - */
447 - $content = Emoji::wrap_in_content( $activity['object']['content'], $activity['object'] );
448 - $comment_content = \addslashes( $content );
449 - }
371 + $webfinger = Webfinger::uri_to_acct( $url );
372 + if ( is_wp_error( $webfinger ) ) {
373 + $webfinger = '';
374 + } else {
375 + $webfinger = str_replace( 'acct:', '', $webfinger );
450 376 }
451 377
452 378 $published = $activity['object']['published'] ?? $activity['published'] ?? 'now';
453 379 $gm_date = \gmdate( 'Y-m-d H:i:s', \strtotime( $published ) );
@@ -452,36 +378,32 @@
452 378 $published = $activity['object']['published'] ?? $activity['published'] ?? 'now';
453 379 $gm_date = \gmdate( 'Y-m-d H:i:s', \strtotime( $published ) );
454 380
455 381 $comment_data = array(
456 - 'comment_author' => $comment_author,
457 - 'comment_author_url' => $comment_author_url,
382 + 'comment_author' => $comment_author ?? __( 'Anonymous', 'activitypub' ),
383 + 'comment_author_url' => \esc_url_raw( $url ),
458 384 'comment_content' => $comment_content,
459 385 'comment_type' => 'comment',
460 - 'comment_author_email' => $comment_author_email,
386 + 'comment_author_email' => $webfinger,
461 387 'comment_date' => \get_date_from_gmt( $gm_date ),
462 388 'comment_date_gmt' => $gm_date,
463 - 'comment_meta' => array(),
389 + 'comment_meta' => array(
390 + 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ),
391 + 'protocol' => 'activitypub',
392 + ),
464 393 );
465 394
466 - if ( $user_id ) {
467 - $comment_data['user_id'] = $user_id;
468 - } else {
469 - $comment_data['comment_meta']['protocol'] = 'activitypub';
470 - $comment_data['comment_meta']['source_id'] = \esc_url_raw( object_to_uri( $activity['object'] ) );
471 -
472 - // Store reference to remote actor post.
473 - $actor_uri = object_to_uri( $activity['actor'] ?? null );
474 - if ( $actor_uri ) {
475 - $remote_actor = Remote_Actors::get_by_uri( $actor_uri );
476 - if ( ! \is_wp_error( $remote_actor ) ) {
477 - $comment_data['comment_meta']['_activitypub_remote_actor_id'] = $remote_actor->ID;
478 - }
395 + // Store reference to remote actor post.
396 + $actor_uri = object_to_uri( $activity['actor'] ?? null );
397 + if ( $actor_uri ) {
398 + $remote_actor = Remote_Actors::get_by_uri( $actor_uri );
399 + if ( ! \is_wp_error( $remote_actor ) ) {
400 + $comment_data['comment_meta']['_activitypub_remote_actor_id'] = $remote_actor->ID;
479 401 }
402 + }
480 403
481 - if ( isset( $activity['object']['url'] ) ) {
482 - $comment_data['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) );
483 - }
404 + if ( isset( $activity['object']['url'] ) ) {
405 + $comment_data['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) );
484 406 }
485 407
486 408 return $comment_data;
487 409 }