PluginProbe
ActivityPub / 5.7.0
ActivityPub v5.7.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 / collection / class-outbox.php

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

352 lines 10.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\Dispatcher;
11 use Activitypub\Scheduler;
12 use Activitypub\Activity\Activity;
13 use Activitypub\Activity\Base_Object;
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 const POST_TYPE = 'ap_outbox';
24
25 /**
26 * Add an Item to the outbox.
27 *
28 * @param Activity $activity Full Activity object that will be added to the outbox.
29 * @param int $user_id The real or imaginary user ID of the actor that published the activity that will be added to the outbox.
30 * @param string $visibility Optional. The visibility of the content. Default: `ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC`. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
31 *
32 * @return false|int|\WP_Error The added item or an error.
33 */
34 public static function add( Activity $activity, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
35 $actor_type = Actors::get_type_by_id( $user_id );
36 $object_id = self::get_object_id( $activity );
37 $title = self::get_object_title( $activity->get_object() );
38
39 if ( ! $activity->get_actor() ) {
40 $activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
41 }
42
43 $outbox_item = array(
44 'post_type' => self::POST_TYPE,
45 'post_title' => sprintf(
46 /* translators: 1. Activity type, 2. Object Title or Excerpt */
47 __( '[%1$s] %2$s', 'activitypub' ),
48 $activity->get_type(),
49 \wp_trim_words( $title, 5 )
50 ),
51 'post_content' => wp_slash( $activity->to_json() ),
52 // ensure that user ID is not below 0.
53 'post_author' => \max( $user_id, 0 ),
54 'post_status' => 'pending',
55 'meta_input' => array(
56 '_activitypub_object_id' => $object_id,
57 '_activitypub_activity_type' => $activity->get_type(),
58 '_activitypub_activity_actor' => $actor_type,
59 'activitypub_content_visibility' => $visibility,
60 ),
61 );
62
63 $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
64 if ( $has_kses ) {
65 // Prevent KSES from corrupting JSON in post_content.
66 \kses_remove_filters();
67 }
68
69 $id = \wp_insert_post( $outbox_item, true );
70
71 // Update the activity ID if the post was inserted successfully.
72 if ( $id && ! \is_wp_error( $id ) ) {
73 $activity->set_id( \get_the_guid( $id ) );
74
75 \wp_update_post(
76 array(
77 'ID' => $id,
78 'post_content' => \wp_slash( $activity->to_json() ),
79 )
80 );
81 }
82
83 if ( $has_kses ) {
84 \kses_init_filters();
85 }
86
87 if ( \is_wp_error( $id ) ) {
88 return $id;
89 }
90
91 if ( ! $id ) {
92 return false;
93 }
94
95 self::invalidate_existing_items( $object_id, $activity->get_type(), $id );
96
97 return $id;
98 }
99
100 /**
101 * Invalidate existing outbox items with the same activity type and object ID
102 * by setting their status to 'publish'.
103 *
104 * @param string $object_id The ID of the activity object.
105 * @param string $activity_type The type of the activity.
106 * @param int $current_id The ID of the current outbox item to exclude.
107 *
108 * @return void
109 */
110 private static function invalidate_existing_items( $object_id, $activity_type, $current_id ) {
111 // Do not invalidate items for Announce activities.
112 if ( 'Announce' === $activity_type ) {
113 return;
114 }
115
116 $meta_query = array(
117 array(
118 'key' => '_activitypub_object_id',
119 'value' => $object_id,
120 ),
121 );
122
123 // For non-Delete activities, only invalidate items of the same type.
124 if ( 'Delete' !== $activity_type ) {
125 $meta_query[] = array(
126 'key' => '_activitypub_activity_type',
127 'value' => $activity_type,
128 );
129 }
130
131 $existing_items = get_posts(
132 array(
133 'post_type' => self::POST_TYPE,
134 'post_status' => 'pending',
135 'exclude' => array( $current_id ),
136 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
137 'meta_query' => $meta_query,
138 'fields' => 'ids',
139 )
140 );
141
142 foreach ( $existing_items as $existing_item_id ) {
143 $event_args = array(
144 Dispatcher::$callback,
145 $existing_item_id,
146 Dispatcher::$batch_size,
147 \get_post_meta( $existing_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore
148 );
149
150 $timestamp = \wp_next_scheduled( 'activitypub_async_batch', $event_args );
151 \wp_unschedule_event( $timestamp, 'activitypub_async_batch', $event_args );
152
153 $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $existing_item_id ) );
154 \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $existing_item_id ) );
155
156 \wp_publish_post( $existing_item_id );
157 \delete_post_meta( $existing_item_id, '_activitypub_outbox_offset' );
158 }
159 }
160
161 /**
162 * Creates an Undo activity.
163 *
164 * @param int|\WP_Post $outbox_item The Outbox post or post ID.
165 *
166 * @return int|bool The ID of the outbox item or false on failure.
167 */
168 public static function undo( $outbox_item ) {
169 $outbox_item = get_post( $outbox_item );
170 $activity = self::get_activity( $outbox_item );
171
172 $type = 'Undo';
173 if ( 'Create' === $activity->get_type() ) {
174 $type = 'Delete';
175 } elseif ( 'Add' === $activity->get_type() ) {
176 $type = 'Remove';
177 }
178
179 return add_to_outbox( $activity, $type, $outbox_item->post_author );
180 }
181
182 /**
183 * Reschedule an activity.
184 *
185 * @param int|\WP_Post $outbox_item The Outbox post or post ID.
186 *
187 * @return bool True if the activity was rescheduled, false otherwise.
188 */
189 public static function reschedule( $outbox_item ) {
190 $outbox_item = get_post( $outbox_item );
191
192 $outbox_item->post_status = 'pending';
193 $outbox_item->post_date = current_time( 'mysql' );
194
195 wp_update_post( $outbox_item );
196
197 Scheduler::schedule_outbox_activity_for_federation( $outbox_item->ID );
198
199 return true;
200 }
201
202 /**
203 * Get the Activity object from the Outbox item.
204 *
205 * @param int|\WP_Post $outbox_item The Outbox post or post ID.
206 * @return Activity|\WP_Error The Activity object or WP_Error.
207 */
208 public static function get_activity( $outbox_item ) {
209 $outbox_item = get_post( $outbox_item );
210 $actor = self::get_actor( $outbox_item );
211 if ( is_wp_error( $actor ) ) {
212 return $actor;
213 }
214
215 $activity_object = \json_decode( $outbox_item->post_content, true );
216 $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
217
218 if ( $activity_object['type'] === $type ) {
219 $activity = Activity::init_from_array( $activity_object );
220 if ( ! $activity->get_actor() ) {
221 $activity->set_actor( $actor->get_id() );
222 }
223 } else {
224 $activity = new Activity();
225 $activity->set_type( $type );
226 $activity->set_id( $outbox_item->guid );
227 $activity->set_actor( $actor->get_id() );
228 // Pre-fill the Activity with data (for example cc and to).
229 $activity->set_object( $activity_object );
230 }
231
232 if ( 'Update' === $type ) {
233 $activity->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, strtotime( $outbox_item->post_modified ) ) );
234 }
235
236 /**
237 * Filters the Activity object before it is returned.
238 *
239 * @param Activity $activity The Activity object.
240 * @param \WP_Post $outbox_item The outbox item post object.
241 */
242 return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item );
243 }
244
245 /**
246 * Get the Actor object from the Outbox item.
247 *
248 * @param \WP_Post $outbox_item The Outbox post.
249 *
250 * @return \Activitypub\Model\User|\Activitypub\Model\Blog|\WP_Error The Actor object or WP_Error.
251 */
252 public static function get_actor( $outbox_item ) {
253 $actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true );
254
255 switch ( $actor_type ) {
256 case 'blog':
257 $actor_id = Actors::BLOG_USER_ID;
258 break;
259 case 'application':
260 $actor_id = Actors::APPLICATION_USER_ID;
261 break;
262 case 'user':
263 default:
264 $actor_id = $outbox_item->post_author;
265 break;
266 }
267
268 return Actors::get_by_id( $actor_id );
269 }
270
271 /**
272 * Get the Activity object from the Outbox item.
273 *
274 * @param \WP_Post $outbox_item The Outbox post.
275 *
276 * @return Activity|\WP_Error The Activity object or WP_Error.
277 */
278 public static function maybe_get_activity( $outbox_item ) {
279 if ( ! $outbox_item instanceof \WP_Post ) {
280 return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
281 }
282
283 if ( 'ap_outbox' !== $outbox_item->post_type ) {
284 return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
285 }
286
287 // Check if Outbox Activity is public.
288 $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true );
289
290 if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) {
291 return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' );
292 }
293
294 $activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) );
295 $activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
296
297 if ( ! in_array( $activity_type, $activity_types, true ) ) {
298 return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' );
299 }
300
301 return self::get_activity( $outbox_item );
302 }
303
304 /**
305 * Get the object ID of an activity.
306 *
307 * @param Activity|Base_Object|string $data The activity object.
308 *
309 * @return string The object ID.
310 */
311 private static function get_object_id( $data ) {
312 $object = $data->get_object();
313
314 if ( is_object( $object ) ) {
315 return self::get_object_id( $object );
316 }
317
318 if ( is_string( $object ) ) {
319 return $object;
320 }
321
322 return $data->get_id() ?? $data->get_actor();
323 }
324
325 /**
326 * Get the title of an activity recursively.
327 *
328 * @param Base_Object $activity_object The activity object.
329 *
330 * @return string The title.
331 */
332 private static function get_object_title( $activity_object ) {
333 if ( ! $activity_object ) {
334 return '';
335 }
336
337 if ( is_string( $activity_object ) ) {
338 $post_id = url_to_postid( $activity_object );
339
340 return $post_id ? get_the_title( $post_id ) : '';
341 }
342
343 $title = $activity_object->get_name() ?? $activity_object->get_content();
344
345 if ( ! $title && $activity_object->get_object() instanceof Base_Object ) {
346 $title = $activity_object->get_object()->get_name() ?? $activity_object->get_object()->get_content();
347 }
348
349 return $title;
350 }
351 }
352