PluginProbe
ActivityPub / 7.8.4
ActivityPub v7.8.4
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 / collection / class-outbox.php

class-outbox.php in ActivityPub 7.8.4, at includes/collection/class-outbox.php

444 lines 12.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Outbox collection file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Collection;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Activity\Base_Object;
12 use Activitypub\Scheduler;
13 use Activitypub\Webfinger;
14
15 use function Activitypub\add_to_outbox;
16
17 /**
18 * ActivityPub Outbox Collection
19 *
20 * @link https://www.w3.org/TR/activitypub/#outbox
21 */
22 class Outbox {
23 /**
24 * The post type for the objects.
25 *
26 * @var string
27 */
28 const POST_TYPE = 'ap_outbox';
29
30 /**
31 * Add an Item to the outbox.
32 *
33 * @param Activity $activity Full Activity object that will be added to the outbox.
34 * @param int $user_id The real or imaginary user ID of the actor that published the activity that will be added to the outbox.
35 * @param string $visibility Optional. The visibility of the content. Default: `ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC`. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
36 *
37 * @return false|int|\WP_Error The added item or an error.
38 */
39 public static function add( Activity $activity, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
40 $actor_type = Actors::get_type_by_id( $user_id );
41 $object_id = self::get_object_id( $activity );
42 $title = self::get_object_title( $activity->get_object() );
43
44 if ( ! $activity->get_actor() ) {
45 $activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
46 }
47
48 if ( ! \filter_var( $object_id, FILTER_VALIDATE_URL ) ) {
49 $object_id = Webfinger::resolve( $object_id );
50 }
51
52 if ( \is_wp_error( $object_id ) ) {
53 return $object_id;
54 }
55
56 // Save activity in the context of an activitypub request.
57 \add_filter( 'activitypub_is_activitypub_request', '__return_true' );
58
59 $outbox_item = array(
60 'post_type' => self::POST_TYPE,
61 'post_title' => sprintf(
62 /* translators: 1. Activity type, 2. Object Title or Excerpt */
63 __( '[%1$s] %2$s', 'activitypub' ),
64 $activity->get_type(),
65 \wp_trim_words( $title, 5 )
66 ),
67 'post_content' => wp_slash( $activity->to_json() ),
68 // ensure that user ID is not below 0.
69 'post_author' => \max( $user_id, 0 ),
70 'post_status' => 'pending',
71 'meta_input' => array(
72 '_activitypub_object_id' => $object_id,
73 '_activitypub_activity_type' => $activity->get_type(),
74 '_activitypub_activity_actor' => $actor_type,
75 'activitypub_content_visibility' => $visibility,
76 ),
77 );
78
79 \remove_filter( 'activitypub_is_activitypub_request', '__return_true' );
80
81 $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
82 if ( $has_kses ) {
83 // Prevent KSES from corrupting JSON in post_content.
84 \kses_remove_filters();
85 }
86
87 $id = \wp_insert_post( $outbox_item, true );
88
89 // Update the activity ID if the post was inserted successfully.
90 if ( $id && ! \is_wp_error( $id ) ) {
91 $activity->set_id( \get_the_guid( $id ) );
92
93 \wp_update_post(
94 array(
95 'ID' => $id,
96 'post_content' => \wp_slash( $activity->to_json() ),
97 )
98 );
99 }
100
101 if ( $has_kses ) {
102 \kses_init_filters();
103 }
104
105 if ( \is_wp_error( $id ) ) {
106 return $id;
107 }
108
109 if ( ! $id ) {
110 return false;
111 }
112
113 self::invalidate_existing_items( $object_id, $activity->get_type(), $id );
114
115 return $id;
116 }
117
118 /**
119 * Invalidate existing outbox items with the same activity type and object ID
120 * by setting their status to 'publish'.
121 *
122 * @param string $object_id The ID of the activity object.
123 * @param string $activity_type The type of the activity.
124 * @param int $current_id The ID of the current outbox item to exclude.
125 *
126 * @return void
127 */
128 private static function invalidate_existing_items( $object_id, $activity_type, $current_id ) {
129 // Do not invalidate items for Announce activities.
130 if ( 'Announce' === $activity_type ) {
131 return;
132 }
133
134 $meta_query = array(
135 array(
136 'key' => '_activitypub_object_id',
137 'value' => $object_id,
138 ),
139 );
140
141 // For non-Delete activities, only invalidate items of the same type.
142 if ( 'Delete' !== $activity_type ) {
143 $meta_query[] = array(
144 'key' => '_activitypub_activity_type',
145 'value' => $activity_type,
146 );
147 }
148
149 $existing_items = get_posts(
150 array(
151 'post_type' => self::POST_TYPE,
152 'post_status' => 'pending',
153 'exclude' => array( $current_id ),
154 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
155 'meta_query' => $meta_query,
156 'fields' => 'ids',
157 )
158 );
159
160 foreach ( $existing_items as $existing_item_id ) {
161 Scheduler::unschedule_events_for_item( $existing_item_id );
162 \wp_publish_post( $existing_item_id );
163 }
164 }
165
166 /**
167 * Creates an Undo activity.
168 *
169 * @param int|\WP_Post $outbox_item The Outbox post or post ID.
170 *
171 * @return int|bool|\WP_Error The ID of the outbox item or false on failure.
172 */
173 public static function undo( $outbox_item ) {
174 $outbox_item = \get_post( $outbox_item );
175 $activity = self::get_activity( $outbox_item );
176
177 if ( \is_wp_error( $activity ) ) {
178 return $activity;
179 }
180
181 $type = 'Undo';
182 if ( 'Create' === $activity->get_type() ) {
183 $type = 'Delete';
184 } elseif ( 'Add' === $activity->get_type() ) {
185 $type = 'Remove';
186 }
187
188 $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true );
189
190 return add_to_outbox( $activity, $type, $outbox_item->post_author, $visibility );
191 }
192
193 /**
194 * Get an outbox item by its GUID.
195 *
196 * @param string $guid The GUID of the outbox item.
197 *
198 * @return \WP_Post|\WP_Error The outbox item or WP_Error.
199 */
200 public static function get_by_guid( $guid ) {
201 global $wpdb;
202 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
203 $post_id = $wpdb->get_var(
204 $wpdb->prepare(
205 "SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s",
206 \esc_url( $guid ),
207 self::POST_TYPE
208 )
209 );
210
211 if ( ! $post_id ) {
212 return new \WP_Error(
213 'activitypub_outbox_item_not_found',
214 \__( 'Outbox item not found', 'activitypub' ),
215 array( 'status' => 404 )
216 );
217 }
218
219 return \get_post( $post_id );
220 }
221
222 /**
223 * Reschedule an activity.
224 *
225 * @param int|\WP_Post $outbox_item The Outbox post or post ID.
226 *
227 * @return bool True if the activity was rescheduled, false otherwise.
228 */
229 public static function reschedule( $outbox_item ) {
230 $outbox_item = get_post( $outbox_item );
231
232 $outbox_item->post_status = 'pending';
233 $outbox_item->post_date = current_time( 'mysql' );
234
235 wp_update_post( $outbox_item );
236
237 Scheduler::schedule_outbox_activity_for_federation( $outbox_item->ID );
238
239 return true;
240 }
241
242 /**
243 * Get the Activity object from the Outbox item.
244 *
245 * @param int|\WP_Post $outbox_item The Outbox post or post ID.
246 * @return Activity|\WP_Error The Activity object or WP_Error.
247 */
248 public static function get_activity( $outbox_item ) {
249 $outbox_item = \get_post( $outbox_item );
250
251 $activity_object = \json_decode( $outbox_item->post_content, true );
252 $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
253
254 if ( $activity_object['type'] === $type ) {
255 $activity = Activity::init_from_array( $activity_object );
256 if ( ! $activity->get_actor() ) {
257 $actor = self::get_actor( $outbox_item );
258 if ( \is_wp_error( $actor ) ) {
259 return $actor;
260 }
261 $activity->set_actor( $actor->get_id() );
262 }
263 } else {
264 $actor = self::get_actor( $outbox_item );
265 if ( \is_wp_error( $actor ) ) {
266 return $actor;
267 }
268
269 $activity = new Activity();
270 $activity->set_type( $type );
271 $activity->set_id( $outbox_item->guid );
272 $activity->set_actor( $actor->get_id() );
273 // Pre-fill the Activity with data (for example cc and to).
274 $activity->set_object( $activity_object );
275 }
276
277 if ( 'Update' === $type ) {
278 $activity->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, strtotime( $outbox_item->post_modified ) ) );
279 }
280
281 /**
282 * Filters the Activity object before it is returned.
283 *
284 * @param Activity $activity The Activity object.
285 * @param \WP_Post $outbox_item The outbox item post object.
286 */
287 return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item );
288 }
289
290 /**
291 * Get the Actor object from the Outbox item.
292 *
293 * @param \WP_Post $outbox_item The Outbox post.
294 *
295 * @return \Activitypub\Model\User|\Activitypub\Model\Blog|\WP_Error The Actor object or WP_Error.
296 */
297 public static function get_actor( $outbox_item ) {
298 $actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true );
299
300 switch ( $actor_type ) {
301 case 'blog':
302 $actor_id = Actors::BLOG_USER_ID;
303 break;
304 case 'application':
305 $actor_id = Actors::APPLICATION_USER_ID;
306 break;
307 case 'user':
308 default:
309 $actor_id = $outbox_item->post_author;
310 break;
311 }
312
313 return Actors::get_by_id( $actor_id );
314 }
315
316 /**
317 * Get the Activity object from the Outbox item.
318 *
319 * @param \WP_Post $outbox_item The Outbox post.
320 *
321 * @return Activity|\WP_Error The Activity object or WP_Error.
322 */
323 public static function maybe_get_activity( $outbox_item ) {
324 if ( ! $outbox_item instanceof \WP_Post ) {
325 return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
326 }
327
328 if ( 'ap_outbox' !== $outbox_item->post_type ) {
329 return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
330 }
331
332 // Check if Outbox Activity is public.
333 $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true );
334
335 if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) {
336 return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' );
337 }
338
339 $activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) );
340 $activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
341
342 if ( ! in_array( $activity_type, $activity_types, true ) ) {
343 return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' );
344 }
345
346 return self::get_activity( $outbox_item );
347 }
348
349 /**
350 * Get the object ID of an activity.
351 *
352 * @param Activity|Base_Object|string $data The activity object.
353 *
354 * @return string The object ID.
355 */
356 private static function get_object_id( $data ) {
357 $object = $data->get_object();
358
359 if ( is_object( $object ) ) {
360 return self::get_object_id( $object );
361 }
362
363 if ( is_string( $object ) ) {
364 return $object;
365 }
366
367 return $data->get_id() ?? $data->get_actor();
368 }
369
370 /**
371 * Get the title of an activity recursively.
372 *
373 * @param Activity|Base_Object $activity_object The activity object.
374 *
375 * @return string The title.
376 */
377 private static function get_object_title( $activity_object ) {
378 if ( ! $activity_object ) {
379 return '';
380 }
381
382 if ( is_string( $activity_object ) ) {
383 $post_id = url_to_postid( $activity_object );
384
385 return $post_id ? get_the_title( $post_id ) : '';
386 }
387
388 $title = $activity_object->get_name() ?: $activity_object->get_content();
389
390 if ( ! $title && $activity_object->get_object() instanceof Base_Object ) {
391 $title = $activity_object->get_object()->get_name() ?: $activity_object->get_object()->get_content();
392 }
393
394 return $title;
395 }
396
397 /**
398 * Purge old outbox items.
399 *
400 * Deletes outbox items older than the specified number of days,
401 * except for Follow activities which are always preserved.
402 *
403 * @param int $days Number of days to keep items. Items older than this will be deleted.
404 *
405 * @return int The number of items deleted.
406 */
407 public static function purge( $days ) {
408 $total_posts = (int) \wp_count_posts( self::POST_TYPE )->publish;
409 if ( $total_posts <= 20 ) {
410 return 0;
411 }
412
413 $post_ids = \get_posts(
414 array(
415 'post_type' => self::POST_TYPE,
416 'post_status' => 'any',
417 'fields' => 'ids',
418 'numberposts' => -1,
419 'date_query' => array(
420 array(
421 'before' => \gmdate( 'Y-m-d', \time() - ( $days * DAY_IN_SECONDS ) ),
422 ),
423 ),
424 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
425 'meta_query' => array(
426 array(
427 'key' => '_activitypub_activity_type',
428 'value' => 'Follow',
429 'compare' => '!=',
430 ),
431 ),
432 )
433 );
434
435 $deleted = 0;
436 foreach ( $post_ids as $post_id ) {
437 \wp_delete_post( $post_id, true );
438 ++$deleted;
439 }
440
441 return $deleted;
442 }
443 }
444