PluginProbe
ActivityPub / 9.2.1
ActivityPub v9.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
← All changes | includes/handler/class-delete.php +274 -85 2.0.09.2.1 View file →
@@ -1,62 +1,66 @@
1 1 <?php
2 +/**
3 + * Delete handler file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub\Handler;
3 9
4 -use WP_Error;
5 -use WP_REST_Request;
6 -use Activitypub\Http;
7 -use Activitypub\Collection\Followers;
8 10 use Activitypub\Collection\Interactions;
11 +use Activitypub\Collection\Remote_Actors;
12 +use Activitypub\Collection\Remote_Posts;
13 +use Activitypub\Tombstone;
9 14
15 +use function Activitypub\object_to_uri;
16 +
10 17 /**
11 18 * Handles Delete requests.
12 19 */
13 20 class Delete {
14 21 /**
15 - * Initialize the class, registering WordPress hooks
22 + * Initialize the class, registering WordPress hooks.
16 23 */
17 24 public static function init() {
18 - \add_action(
19 - 'activitypub_inbox_delete',
20 - array( self::class, 'handle_delete' )
21 - );
25 + \add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ), 10, 2 );
26 + \add_filter( 'activitypub_skip_inbox_storage', array( self::class, 'skip_inbox_storage' ), 10, 2 );
27 + \add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 3 );
28 + \add_action( 'activitypub_delete_remote_actor_interactions', array( self::class, 'delete_interactions' ) );
29 + \add_action( 'activitypub_delete_remote_actor_posts', array( self::class, 'delete_posts' ) );
22 30
23 - // defer signature verification for `Delete` requests.
24 - \add_filter(
25 - 'activitypub_defer_signature_verification',
26 - array( self::class, 'defer_signature_verification' ),
27 - 10,
28 - 2
29 - );
30 -
31 - // side effect
32 - \add_action(
33 - 'activitypub_delete_actor_interactions',
34 - array( self::class, 'delete_interactions' )
35 - );
31 + \add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
32 + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'maybe_bury' ), 10, 2 );
36 33 }
37 34
38 35 /**
39 36 * Handles "Delete" requests.
40 37 *
41 - * @param array $activity The delete activity.
42 - * @param int $user_id The ID of the user performing the delete activity.
38 + * @param array $activity The delete activity.
39 + * @param int|int[] $user_ids The local user ID(s).
43 40 */
44 - public static function handle_delete( $activity ) {
45 - $object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
41 + public static function handle_delete( $activity, $user_ids ) {
42 + $object_type = $activity['object']['type'] ?? '';
46 43
47 44 switch ( $object_type ) {
48 - // Actor Types
49 - // @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
45 + /*
46 + * Actor Types.
47 + *
48 + * @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
49 + */
50 50 case 'Person':
51 51 case 'Group':
52 52 case 'Organization':
53 53 case 'Service':
54 54 case 'Application':
55 - self::maybe_delete_follower( $activity );
55 + self::delete_remote_actor( $activity, $user_ids );
56 56 break;
57 - // Object and Link Types
58 - // @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
57 +
58 + /*
59 + * Object and Link Types.
60 + *
61 + * @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
62 + */
59 63 case 'Note':
60 64 case 'Article':
61 65 case 'Image':
62 66 case 'Audio':
@@ -62,112 +66,258 @@
62 66 case 'Audio':
63 67 case 'Video':
64 68 case 'Event':
65 69 case 'Document':
66 - self::maybe_delete_interaction( $activity );
70 + self::delete_object( $activity, $user_ids );
67 71 break;
68 - // Tombstone Type
69 - // @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
72 +
73 + /*
74 + * Tombstone Type.
75 + *
76 + * @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
77 + */
70 78 case 'Tombstone':
71 - self::maybe_delete_interaction( $activity );
79 + self::delete_object( $activity, $user_ids );
72 80 break;
73 - // Minimal Activity
74 - // @see https://www.w3.org/TR/activitystreams-core/#example-1
81 +
82 + /*
83 + * Minimal Activity.
84 + *
85 + * @see https://www.w3.org/TR/activitystreams-core/#example-1
86 + */
75 87 default:
76 - // ignore non Minimal Activities.
77 - if ( ! is_string( $activity['object'] ) ) {
78 - return;
88 + // Check if Object is an Actor.
89 + if ( object_to_uri( $activity['object'] ) === $activity['actor'] ) {
90 + self::delete_remote_actor( $activity, $user_ids );
91 + } else { // Assume an object otherwise.
92 + self::delete_object( $activity, $user_ids );
79 93 }
94 + // Maybe handle Delete Activity for other Object Types.
95 + break;
96 + }
97 + }
80 98
81 - // check if Object is an Actor.
82 - if ( $activity['actor'] === $activity['object'] ) {
83 - self::maybe_delete_follower( $activity );
84 - self::maybe_delete_interactions( $activity );
85 - } else { // assume a interaction otherwise.
86 - self::maybe_delete_interaction( $activity );
87 - }
88 - // maybe handle Delete Activity for other Object Types.
89 - break;
99 + /**
100 + * Delete an Object.
101 + *
102 + * @param array $activity The Activity object.
103 + * @param int|int[] $user_ids The user ID(s).
104 + */
105 + public static function delete_object( $activity, $user_ids ) {
106 + $result = self::maybe_delete_interaction( $activity );
107 +
108 + if ( ! $result ) {
109 + $result = self::maybe_delete_post( $activity );
90 110 }
111 +
112 + $success = ( $result && ! \is_wp_error( $result ) );
113 +
114 + /**
115 + * Fires after an ActivityPub Delete activity has been handled.
116 + *
117 + * @param array $activity The ActivityPub activity data.
118 + * @param int[] $user_ids The local user IDs.
119 + * @param bool $success True on success, false otherwise.
120 + * @param mixed|null $result The result of the delete operation.
121 + */
122 + \do_action( 'activitypub_handled_delete', $activity, (array) $user_ids, $success, $result );
91 123 }
92 124
93 125 /**
126 + * Delete an Actor.
127 + *
128 + * @param array $activity The Activity object.
129 + * @param int|int[] $user_ids The user ID(s).
130 + */
131 + public static function delete_remote_actor( $activity, $user_ids ) {
132 + $result = self::maybe_delete_follower( $activity );
133 + $success = ( $result && ! \is_wp_error( $result ) );
134 +
135 + /**
136 + * Fires after an ActivityPub Delete activity has been handled.
137 + *
138 + * @param array $activity The ActivityPub activity data.
139 + * @param int[] $user_ids The local user IDs.
140 + * @param bool $success True on success, false otherwise.
141 + * @param mixed|null $result The result of the delete operation.
142 + */
143 + \do_action( 'activitypub_handled_delete', $activity, (array) $user_ids, $success, $result );
144 +
145 + return $result;
146 + }
147 +
148 + /**
94 149 * Delete a Follower if Actor-URL is a Tombstone.
95 150 *
96 151 * @param array $activity The delete activity.
152 + *
153 + * @return bool True on success, false otherwise.
97 154 */
98 155 public static function maybe_delete_follower( $activity ) {
99 - $follower = Followers::get_follower_by_actor( $activity['actor'] );
156 + $follower = Remote_Actors::get_by_uri( $activity['actor'] );
100 157
101 - // verify if Actor is deleted.
102 - if ( $follower && Http::is_tombstone( $activity['actor'] ) ) {
103 - $follower->delete();
158 + // Verify that Actor is deleted.
159 + if ( ! \is_wp_error( $follower ) && Tombstone::exists( $activity['actor'] ) ) {
160 + self::maybe_delete_interactions( $follower->ID );
161 + self::maybe_delete_posts( $follower->ID );
162 + $state = Remote_Actors::delete( $follower->ID );
104 163 }
164 +
165 + return $state ?? false;
105 166 }
106 167
107 168 /**
108 - * Delete Reactions if Actor-URL is a Tombstone.
169 + * Schedule Deletion of Interactions of a Remote Actor.
109 170 *
110 - * @param array $activity The delete activity.
171 + * @param int $id The remote actor ID.
111 172 */
112 - public static function maybe_delete_interactions( $activity ) {
113 - // verify if Actor is deleted.
114 - if ( Http::is_tombstone( $activity['actor'] ) ) {
115 - \wp_schedule_single_event(
116 - \time(),
117 - 'activitypub_delete_actor_interactions',
118 - array( $activity['actor'] )
119 - );
173 + public static function maybe_delete_interactions( $id ) {
174 + \wp_schedule_single_event(
175 + \time(),
176 + 'activitypub_delete_remote_actor_interactions',
177 + array( $id )
178 + );
179 + }
180 +
181 + /**
182 + * Schedule Deletion of Reader Items of a Remote Actor.
183 + *
184 + * @param int $id The remote actor ID.
185 + */
186 + public static function maybe_delete_posts( $id ) {
187 + \wp_schedule_single_event(
188 + \time(),
189 + 'activitypub_delete_remote_actor_posts',
190 + array( $id )
191 + );
192 + }
193 +
194 + /**
195 + * Delete Interactions from a Remote Actor.
196 + *
197 + * @param int $id The ID of the actor whose comments to delete.
198 + *
199 + * @return bool True on success, false otherwise.
200 + */
201 + public static function delete_interactions( $id ) {
202 + $comments = Interactions::get_by_remote_actor_id( $id );
203 +
204 + foreach ( $comments as $comment ) {
205 + \wp_delete_comment( $comment, true );
120 206 }
207 +
208 + if ( $comments ) {
209 + return true;
210 + } else {
211 + return false;
212 + }
121 213 }
122 214
123 215 /**
124 - * Delete comments from an Actor.
216 + * Delete Reader Items from an Actor.
125 217 *
126 - * @param array $comments The comments to delete.
218 + * @param int $id The ID of the actor whose comments to delete.
219 + *
220 + * @return bool True on success, false otherwise.
127 221 */
128 - public static function delete_interactions( $actor ) {
129 - $comments = Interactions::get_interactions_by_actor( $actor );
222 + public static function delete_posts( $id ) {
223 + $posts = Remote_Posts::get_by_remote_actor_id( $id );
130 224
131 - if ( is_array( $comments ) ) {
225 + foreach ( $posts as $post ) {
226 + Remote_Posts::delete( $post->ID );
227 + }
228 +
229 + if ( $posts ) {
230 + return true;
231 + } else {
232 + return false;
233 + }
234 + }
235 +
236 + /**
237 + * Delete a Reaction if URL is a Tombstone.
238 + *
239 + * Note: When comments are deleted, WordPress automatically deletes all associated
240 + * comment meta including _activitypub_remote_actor_id. The remote actor post itself
241 + * is not deleted, as it may be referenced by other comments or may be needed for
242 + * future interactions.
243 + *
244 + * @param array $activity The delete activity.
245 + *
246 + * @return bool True on success, false otherwise.
247 + */
248 + public static function maybe_delete_interaction( $activity ) {
249 + $id = object_to_uri( $activity['object'] );
250 + $comments = Interactions::get_by_id( $id );
251 +
252 + if ( $comments && Tombstone::exists( $id ) ) {
132 253 foreach ( $comments as $comment ) {
133 - wp_delete_comment( $comment->comment_ID );
254 + // WordPress will automatically delete all comment meta including _activitypub_remote_actor_id.
255 + \wp_delete_comment( $comment->comment_ID, true );
134 256 }
257 +
258 + return true;
135 259 }
260 +
261 + return false;
136 262 }
137 263
138 264 /**
139 - * Delete a Reaction if URL is a Tombstone.
265 + * Delete a post from the Posts collection.
140 266 *
141 267 * @param array $activity The delete activity.
142 268 *
143 - * @return void
269 + * @return bool|\WP_Error True on success, false or WP_Error on failure.
144 270 */
145 - public static function maybe_delete_interaction( $activity ) {
146 - if ( is_array( $activity['object'] ) ) {
147 - $id = $activity['object']['id'];
148 - } else {
149 - $id = $activity['object'];
271 + public static function maybe_delete_post( $activity ) {
272 + $id = object_to_uri( $activity['object'] );
273 +
274 + // Check if the object exists and is a tombstone.
275 + if ( Tombstone::exists( $id ) ) {
276 + return Remote_Posts::delete_by_guid( $id );
150 277 }
151 278
152 - $comments = Interactions::get_interaction_by_id( $id );
279 + return false;
280 + }
153 281
154 - if ( $comments && Http::is_tombstone( $id ) ) {
155 - foreach ( $comments as $comment ) {
156 - wp_delete_comment( $comment->comment_ID, true );
157 - }
282 + /**
283 + * Skip inbox storage for `Delete` requests.
284 + *
285 + * @param bool $skip Whether to skip inbox storage.
286 + * @param array $data The activity data array.
287 + *
288 + * @return bool Whether to skip inbox storage.
289 + */
290 + public static function skip_inbox_storage( $skip, $data ) {
291 + if ( isset( $data['type'] ) && 'Delete' === $data['type'] ) {
292 + return true;
158 293 }
294 +
295 + return $skip;
159 296 }
160 297
161 298 /**
162 299 * Defer signature verification for `Delete` requests.
163 300 *
164 - * @param bool $defer Whether to defer signature verification.
165 - * @param WP_REST_Request $request The request object.
301 + * Endpoints that opt in to mandatory signing by calling
302 + * `verify_signature( $request, true )` must not be overridden — the
303 + * Delete carve-out is only for the default inbox path where the
304 + * remote actor's keys may legitimately be gone before the Delete
305 + * arrives.
166 306 *
307 + * @since 8.2.0 The `$force_signature` parameter is now respected.
308 + *
309 + * @param bool $defer Whether to defer signature verification.
310 + * @param \WP_REST_Request $request The request object.
311 + * @param bool $force_signature Whether the caller has forced signature verification.
312 + *
167 313 * @return bool Whether to defer signature verification.
168 314 */
169 - public static function defer_signature_verification( $defer, $request ) {
315 + public static function defer_signature_verification( $defer, $request, $force_signature = false ) {
316 + if ( $force_signature ) {
317 + return $defer;
318 + }
319 +
170 320 $json = $request->get_json_params();
171 321
172 322 if ( isset( $json['type'] ) && 'Delete' === $json['type'] ) {
173 323 return true;
@@ -172,7 +322,46 @@
172 322 if ( isset( $json['type'] ) && 'Delete' === $json['type'] ) {
173 323 return true;
174 324 }
175 325
176 - return false;
326 + return $defer;
327 + }
328 +
329 + /**
330 + * Set the object to the object ID.
331 + *
332 + * @param \Activitypub\Activity\Activity $activity The Activity object.
333 + *
334 + * @return \Activitypub\Activity\Activity The filtered Activity object.
335 + */
336 + public static function outbox_activity( $activity ) {
337 + if ( 'Delete' === $activity->get_type() ) {
338 + $activity->set_object( object_to_uri( $activity->get_object() ) );
339 + }
340 +
341 + return $activity;
342 + }
343 +
344 + /**
345 + * Add a URL to the tombstone registry when a Delete activity is sent.
346 + *
347 + * @param int $outbox_id The ID of the outbox activity.
348 + * @param \Activitypub\Activity\Activity $activity The Activity object.
349 + */
350 + public static function maybe_bury( $outbox_id, $activity ) {
351 + if ( 'Delete' !== $activity->get_type() ) {
352 + return;
353 + }
354 +
355 + $object = $activity->get_object();
356 +
357 + if ( ! $object ) {
358 + return;
359 + }
360 +
361 + Tombstone::bury( object_to_uri( $object ) );
362 +
363 + if ( \is_object( $object ) ) {
364 + Tombstone::bury( $object->get_id(), $object->get_url() );
365 + }
177 366 }
178 367 }