| @@ -6,17 +6,14 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace Activitypub\Collection; |
| 9 | 9 | |
| 10 | +use Activitypub\Dispatcher; | |
| 11 | +use Activitypub\Scheduler; | |
| 10 | 12 | use Activitypub\Activity\Activity; |
| 11 | 13 | use Activitypub\Activity\Base_Object; |
| 12 | -use Activitypub\OAuth\Server; | |
| 13 | -use Activitypub\Scheduler; | |
| 14 | -use Activitypub\Webfinger; | |
| 15 | 14 | |
| 16 | 15 | use function Activitypub\add_to_outbox; |
| 17 | -use function Activitypub\object_to_uri; | |
| 18 | -use function Activitypub\user_can_act_as_blog; | |
| 19 | 16 | |
| 20 | 17 | /** |
| 21 | 18 | * ActivityPub Outbox Collection |
| 22 | 19 | * |
| @@ -22,48 +19,11 @@ | ||
| 22 | 19 | * |
| 23 | 20 | * @link https://www.w3.org/TR/activitypub/#outbox |
| 24 | 21 | */ |
| 25 | 22 | class Outbox { |
| 26 | - /** | |
| 27 | - * The post type for the objects. | |
| 28 | - * | |
| 29 | - * @var string | |
| 30 | - */ | |
| 31 | 23 | const POST_TYPE = 'ap_outbox'; |
| 32 | 24 | |
| 33 | 25 | /** |
| 34 | - * Maximum number of outbox items to keep. | |
| 35 | - * | |
| 36 | - * When the total count exceeds this, the oldest items are purged | |
| 37 | - * regardless of their age. Acts as a safety net for runaway growth. | |
| 38 | - * | |
| 39 | - * @var int | |
| 40 | - */ | |
| 41 | - const MAX_ITEMS = 5000; | |
| 42 | - | |
| 43 | - /** | |
| 44 | - * Activity types included in the outbox collection listing. | |
| 45 | - * | |
| 46 | - * @var string[] | |
| 47 | - */ | |
| 48 | - const ACTIVITY_TYPES = array( 'Announce', 'Arrive', 'Create', 'Like', 'Update' ); | |
| 49 | - | |
| 50 | - | |
| 51 | - /** | |
| 52 | - * Number of items to process per batch during purge. | |
| 53 | - * | |
| 54 | - * @var int | |
| 55 | - */ | |
| 56 | - const PURGE_BATCH_SIZE = 100; | |
| 57 | - | |
| 58 | - /** | |
| 59 | - * Maximum seconds a purge run may take before yielding. | |
| 60 | - * | |
| 61 | - * @var int | |
| 62 | - */ | |
| 63 | - const PURGE_TIMEOUT = 30; | |
| 64 | - | |
| 65 | - /** | |
| 66 | 26 | * Add an Item to the outbox. |
| 67 | 27 | * |
| 68 | 28 | * @param Activity $activity Full Activity object that will be added to the outbox. |
| 69 | 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. |
| @@ -72,45 +32,24 @@ | ||
| 72 | 32 | * @return false|int|\WP_Error The added item or an error. |
| 73 | 33 | */ |
| 74 | 34 | public static function add( Activity $activity, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { |
| 75 | 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() ); | |
| 76 | 38 | |
| 77 | 39 | if ( ! $activity->get_actor() ) { |
| 78 | 40 | $activity->set_actor( Actors::get_by_id( $user_id )->get_id() ); |
| 79 | 41 | } |
| 80 | 42 | |
| 81 | - $object_id = object_to_uri( self::get_object_id( $activity ) ); | |
| 82 | - $title = self::get_object_title( $activity->get_object() ); | |
| 83 | - | |
| 84 | - if ( ! $object_id || ! \is_string( $object_id ) ) { | |
| 85 | - return new \WP_Error( | |
| 86 | - 'activitypub_outbox_invalid_object_id', | |
| 87 | - \__( 'Unable to determine an object ID for this activity.', 'activitypub' ), | |
| 88 | - array( 'status' => 400 ) | |
| 89 | - ); | |
| 90 | - } | |
| 91 | - | |
| 92 | - if ( ! \filter_var( $object_id, FILTER_VALIDATE_URL ) ) { | |
| 93 | - $object_id = Webfinger::resolve( $object_id ); | |
| 94 | - } | |
| 95 | - | |
| 96 | - if ( \is_wp_error( $object_id ) ) { | |
| 97 | - return $object_id; | |
| 98 | - } | |
| 99 | - | |
| 100 | - // Save activity in the context of an activitypub request. | |
| 101 | - \add_filter( 'activitypub_is_activitypub_request', '__return_true' ); | |
| 102 | - | |
| 103 | 43 | $outbox_item = array( |
| 104 | 44 | 'post_type' => self::POST_TYPE, |
| 105 | - 'post_title' => \sprintf( | |
| 45 | + 'post_title' => sprintf( | |
| 106 | 46 | /* translators: 1. Activity type, 2. Object Title or Excerpt */ |
| 107 | - \__( '[%1$s] %2$s', 'activitypub' ), | |
| 47 | + __( '[%1$s] %2$s', 'activitypub' ), | |
| 108 | 48 | $activity->get_type(), |
| 109 | 49 | \wp_trim_words( $title, 5 ) |
| 110 | 50 | ), |
| 111 | - // Persist the blind audience so later dispatch can compute recipients from `bto`/`bcc`. | |
| 112 | - 'post_content' => \wp_slash( $activity->to_json( true, true ) ), | |
| 51 | + 'post_content' => wp_slash( $activity->to_json() ), | |
| 113 | 52 | // ensure that user ID is not below 0. |
| 114 | 53 | 'post_author' => \max( $user_id, 0 ), |
| 115 | 54 | 'post_status' => 'pending', |
| 116 | 55 | 'meta_input' => array( |
| @@ -120,10 +59,8 @@ | ||
| 120 | 59 | 'activitypub_content_visibility' => $visibility, |
| 121 | 60 | ), |
| 122 | 61 | ); |
| 123 | 62 | |
| 124 | - \remove_filter( 'activitypub_is_activitypub_request', '__return_true' ); | |
| 125 | - | |
| 126 | 63 | $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); |
| 127 | 64 | if ( $has_kses ) { |
| 128 | 65 | // Prevent KSES from corrupting JSON in post_content. |
| 129 | 66 | \kses_remove_filters(); |
| @@ -137,9 +74,9 @@ | ||
| 137 | 74 | |
| 138 | 75 | \wp_update_post( |
| 139 | 76 | array( |
| 140 | 77 | 'ID' => $id, |
| 141 | - 'post_content' => \wp_slash( $activity->to_json( true, true ) ), | |
| 78 | + 'post_content' => \wp_slash( $activity->to_json() ), | |
| 142 | 79 | ) |
| 143 | 80 | ); |
| 144 | 81 | } |
| 145 | 82 | |
| @@ -154,40 +91,26 @@ | ||
| 154 | 91 | if ( ! $id ) { |
| 155 | 92 | return false; |
| 156 | 93 | } |
| 157 | 94 | |
| 158 | - self::delete_superseded_items( $object_id, $activity->get_type(), $id ); | |
| 95 | + self::invalidate_existing_items( $object_id, $activity->get_type(), $id ); | |
| 159 | 96 | |
| 160 | 97 | return $id; |
| 161 | 98 | } |
| 162 | 99 | |
| 163 | 100 | /** |
| 164 | - * Delete pending outbox items that have been superseded by a newer item. | |
| 101 | + * Invalidate existing outbox items with the same activity type and object ID | |
| 102 | + * by setting their status to 'publish'. | |
| 165 | 103 | * |
| 166 | - * For most activity types, only items with the same type and object ID are | |
| 167 | - * deleted. Delete activities are a special case: they supersede all pending | |
| 168 | - * items for the same object regardless of type. | |
| 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. | |
| 169 | 107 | * |
| 170 | - * Unschedules all federation events before deleting each item. | |
| 171 | - * Skips Follow, Announce, Accept, and Reject activities, as those are | |
| 172 | - * independent per-request responses that must not cancel each other. | |
| 173 | - * | |
| 174 | - * @param string $object_id The ActivityPub object ID (URL). | |
| 175 | - * @param string $activity_type The activity type (e.g. 'Create', 'Update', 'Delete'). | |
| 176 | - * @param int $exclude_id The ID of the newly added outbox item to keep. | |
| 177 | - * | |
| 178 | 108 | * @return void |
| 179 | 109 | */ |
| 180 | - private static function delete_superseded_items( $object_id, $activity_type, $exclude_id ) { | |
| 181 | - /* | |
| 182 | - * Do not delete items for Follow, Announce, Accept, or Reject activities. | |
| 183 | - * Follow activities from different users share the same object ID but are | |
| 184 | - * independent and must survive until their Accept is received. | |
| 185 | - * Accept/Reject are per-request responses (e.g. to individual incoming | |
| 186 | - * QuoteRequests) and must not cancel each other even when they share | |
| 187 | - * the same object ID. | |
| 188 | - */ | |
| 189 | - if ( \in_array( $activity_type, array( 'Follow', 'Announce', 'Accept', 'Reject' ), true ) ) { | |
| 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 ) { | |
| 190 | 113 | return; |
| 191 | 114 | } |
| 192 | 115 | |
| 193 | 116 | $meta_query = array( |
| @@ -196,45 +119,21 @@ | ||
| 196 | 119 | 'value' => $object_id, |
| 197 | 120 | ), |
| 198 | 121 | ); |
| 199 | 122 | |
| 200 | - /* | |
| 201 | - * Same-type pending items are always superseded. A confirmed | |
| 202 | - * re-publish (Create) additionally invalidates a pending Delete so | |
| 203 | - * we do not send both Delete and Create for the same object. | |
| 204 | - * | |
| 205 | - * Update is intentionally NOT in this list: it must not cancel a | |
| 206 | - * pending Delete, or an unrelated edit could flip a hidden object back | |
| 207 | - * to federated. An Update for an already-deleted object is rejected | |
| 208 | - * upstream in `add_to_outbox()`, and the scheduler re-publish path emits | |
| 209 | - * a Create (not an Update), so that legitimate path still cancels Delete. | |
| 210 | - */ | |
| 123 | + // For non-Delete activities, only invalidate items of the same type. | |
| 211 | 124 | if ( 'Delete' !== $activity_type ) { |
| 212 | - $types = 'Create' === $activity_type | |
| 213 | - ? array( 'Create', 'Delete' ) | |
| 214 | - : array( $activity_type ); | |
| 215 | - | |
| 216 | 125 | $meta_query[] = array( |
| 217 | - 'key' => '_activitypub_activity_type', | |
| 218 | - 'value' => $types, | |
| 219 | - 'compare' => 'IN', | |
| 126 | + 'key' => '_activitypub_activity_type', | |
| 127 | + 'value' => $activity_type, | |
| 220 | 128 | ); |
| 221 | 129 | } |
| 222 | 130 | |
| 223 | - /* | |
| 224 | - * Delete wipes the entire outbox history for the object — any | |
| 225 | - * already-sent Create/Update/etc. is now stale and a redelivery | |
| 226 | - * retry would resurrect content we are tearing down. Other | |
| 227 | - * activity types only invalidate pending peers. | |
| 228 | - */ | |
| 229 | - $status_filter = 'Delete' === $activity_type ? 'any' : 'pending'; | |
| 230 | - | |
| 231 | - $existing_items = \get_posts( | |
| 131 | + $existing_items = get_posts( | |
| 232 | 132 | array( |
| 233 | 133 | 'post_type' => self::POST_TYPE, |
| 234 | - 'post_status' => $status_filter, | |
| 235 | - 'exclude' => array( $exclude_id ), | |
| 236 | - 'numberposts' => -1, | |
| 134 | + 'post_status' => 'pending', | |
| 135 | + 'exclude' => array( $current_id ), | |
| 237 | 136 | // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query |
| 238 | 137 | 'meta_query' => $meta_query, |
| 239 | 138 | 'fields' => 'ids', |
| 240 | 139 | ) |
| @@ -240,10 +139,23 @@ | ||
| 240 | 139 | ) |
| 241 | 140 | ); |
| 242 | 141 | |
| 243 | 142 | foreach ( $existing_items as $existing_item_id ) { |
| 244 | - Scheduler::unschedule_events_for_item( $existing_item_id ); | |
| 245 | - \wp_delete_post( $existing_item_id, true ); | |
| 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' ); | |
| 246 | 158 | } |
| 247 | 159 | } |
| 248 | 160 | |
| 249 | 161 | /** |
| @@ -250,18 +162,14 @@ | ||
| 250 | 162 | * Creates an Undo activity. |
| 251 | 163 | * |
| 252 | 164 | * @param int|\WP_Post $outbox_item The Outbox post or post ID. |
| 253 | 165 | * |
| 254 | - * @return int|bool|\WP_Error The ID of the outbox item or false on failure. | |
| 166 | + * @return int|bool The ID of the outbox item or false on failure. | |
| 255 | 167 | */ |
| 256 | 168 | public static function undo( $outbox_item ) { |
| 257 | - $outbox_item = \get_post( $outbox_item ); | |
| 169 | + $outbox_item = get_post( $outbox_item ); | |
| 258 | 170 | $activity = self::get_activity( $outbox_item ); |
| 259 | 171 | |
| 260 | - if ( \is_wp_error( $activity ) ) { | |
| 261 | - return $activity; | |
| 262 | - } | |
| 263 | - | |
| 264 | 172 | $type = 'Undo'; |
| 265 | 173 | if ( 'Create' === $activity->get_type() ) { |
| 266 | 174 | $type = 'Delete'; |
| 267 | 175 | } elseif ( 'Add' === $activity->get_type() ) { |
| @@ -267,74 +175,12 @@ | ||
| 267 | 175 | } elseif ( 'Add' === $activity->get_type() ) { |
| 268 | 176 | $type = 'Remove'; |
| 269 | 177 | } |
| 270 | 178 | |
| 271 | - $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true ); | |
| 272 | - | |
| 273 | - return add_to_outbox( $activity, $type, $outbox_item->post_author, $visibility ); | |
| 179 | + return add_to_outbox( $activity, $type, $outbox_item->post_author ); | |
| 274 | 180 | } |
| 275 | 181 | |
| 276 | 182 | /** |
| 277 | - * Get an outbox item by object ID and activity type. | |
| 278 | - * | |
| 279 | - * @param string $object_id The ActivityPub object ID. | |
| 280 | - * @param string $activity_type The activity type (Create, Update, etc.). | |
| 281 | - * | |
| 282 | - * @return \WP_Post|null The outbox item or null if not found. | |
| 283 | - */ | |
| 284 | - public static function get_by_object_id( $object_id, $activity_type ) { | |
| 285 | - $outbox_items = \get_posts( | |
| 286 | - array( | |
| 287 | - 'post_type' => self::POST_TYPE, | |
| 288 | - 'post_status' => 'any', | |
| 289 | - 'posts_per_page' => 1, | |
| 290 | - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query | |
| 291 | - 'meta_query' => array( | |
| 292 | - array( | |
| 293 | - 'key' => '_activitypub_object_id', | |
| 294 | - 'value' => $object_id, | |
| 295 | - ), | |
| 296 | - array( | |
| 297 | - 'key' => '_activitypub_activity_type', | |
| 298 | - 'value' => $activity_type, | |
| 299 | - ), | |
| 300 | - ), | |
| 301 | - ) | |
| 302 | - ); | |
| 303 | - | |
| 304 | - return ! empty( $outbox_items ) ? $outbox_items[0] : null; | |
| 305 | - } | |
| 306 | - | |
| 307 | - /** | |
| 308 | - * Get an outbox item by its GUID. | |
| 309 | - * | |
| 310 | - * @param string $guid The GUID of the outbox item. | |
| 311 | - * | |
| 312 | - * @return \WP_Post|\WP_Error The outbox item or WP_Error. | |
| 313 | - */ | |
| 314 | - public static function get_by_guid( $guid ) { | |
| 315 | - global $wpdb; | |
| 316 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching | |
| 317 | - $post_id = $wpdb->get_var( | |
| 318 | - $wpdb->prepare( | |
| 319 | - "SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s", | |
| 320 | - \esc_url( $guid ), | |
| 321 | - self::POST_TYPE | |
| 322 | - ) | |
| 323 | - ); | |
| 324 | - | |
| 325 | - if ( ! $post_id ) { | |
| 326 | - return new \WP_Error( | |
| 327 | - 'activitypub_outbox_item_not_found', | |
| 328 | - \__( 'Outbox item not found', 'activitypub' ), | |
| 329 | - array( 'status' => 404 ) | |
| 330 | - ); | |
| 331 | - } | |
| 332 | - | |
| 333 | - return \get_post( $post_id ); | |
| 334 | - } | |
| 335 | - | |
| 336 | - /** | |
| 337 | 183 | * Reschedule an activity. |
| 338 | 184 | * |
| 339 | 185 | * @param int|\WP_Post $outbox_item The Outbox post or post ID. |
| 340 | 186 | * |
| @@ -340,14 +186,14 @@ | ||
| 340 | 186 | * |
| 341 | 187 | * @return bool True if the activity was rescheduled, false otherwise. |
| 342 | 188 | */ |
| 343 | 189 | public static function reschedule( $outbox_item ) { |
| 344 | - $outbox_item = \get_post( $outbox_item ); | |
| 190 | + $outbox_item = get_post( $outbox_item ); | |
| 345 | 191 | |
| 346 | 192 | $outbox_item->post_status = 'pending'; |
| 347 | - $outbox_item->post_date = \current_time( 'mysql' ); | |
| 193 | + $outbox_item->post_date = current_time( 'mysql' ); | |
| 348 | 194 | |
| 349 | - \wp_update_post( $outbox_item ); | |
| 195 | + wp_update_post( $outbox_item ); | |
| 350 | 196 | |
| 351 | 197 | Scheduler::schedule_outbox_activity_for_federation( $outbox_item->ID ); |
| 352 | 198 | |
| 353 | 199 | return true; |
| @@ -359,16 +205,12 @@ | ||
| 359 | 205 | * @param int|\WP_Post $outbox_item The Outbox post or post ID. |
| 360 | 206 | * @return Activity|\WP_Error The Activity object or WP_Error. |
| 361 | 207 | */ |
| 362 | 208 | public static function get_activity( $outbox_item ) { |
| 363 | - $outbox_item = \get_post( $outbox_item ); | |
| 364 | - | |
| 365 | - if ( ! $outbox_item ) { | |
| 366 | - return new \WP_Error( | |
| 367 | - 'activitypub_outbox_item_not_found', | |
| 368 | - \__( 'Outbox item not found.', 'activitypub' ), | |
| 369 | - array( 'status' => 404 ) | |
| 370 | - ); | |
| 209 | + $outbox_item = get_post( $outbox_item ); | |
| 210 | + $actor = self::get_actor( $outbox_item ); | |
| 211 | + if ( is_wp_error( $actor ) ) { | |
| 212 | + return $actor; | |
| 371 | 213 | } |
| 372 | 214 | |
| 373 | 215 | $activity_object = \json_decode( $outbox_item->post_content, true ); |
| 374 | 216 | $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); |
| @@ -375,20 +217,11 @@ | ||
| 375 | 217 | |
| 376 | 218 | if ( $activity_object['type'] === $type ) { |
| 377 | 219 | $activity = Activity::init_from_array( $activity_object ); |
| 378 | 220 | if ( ! $activity->get_actor() ) { |
| 379 | - $actor = self::get_actor( $outbox_item ); | |
| 380 | - if ( \is_wp_error( $actor ) ) { | |
| 381 | - return $actor; | |
| 382 | - } | |
| 383 | 221 | $activity->set_actor( $actor->get_id() ); |
| 384 | 222 | } |
| 385 | 223 | } else { |
| 386 | - $actor = self::get_actor( $outbox_item ); | |
| 387 | - if ( \is_wp_error( $actor ) ) { | |
| 388 | - return $actor; | |
| 389 | - } | |
| 390 | - | |
| 391 | 224 | $activity = new Activity(); |
| 392 | 225 | $activity->set_type( $type ); |
| 393 | 226 | $activity->set_id( $outbox_item->guid ); |
| 394 | 227 | $activity->set_actor( $actor->get_id() ); |
| @@ -396,9 +229,9 @@ | ||
| 396 | 229 | $activity->set_object( $activity_object ); |
| 397 | 230 | } |
| 398 | 231 | |
| 399 | 232 | if ( 'Update' === $type ) { |
| 400 | - $activity->set_updated( \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( $outbox_item->post_modified ) ) ); | |
| 233 | + $activity->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, strtotime( $outbox_item->post_modified ) ) ); | |
| 401 | 234 | } |
| 402 | 235 | |
| 403 | 236 | /** |
| 404 | 237 | * Filters the Activity object before it is returned. |
| @@ -405,9 +238,9 @@ | ||
| 405 | 238 | * |
| 406 | 239 | * @param Activity $activity The Activity object. |
| 407 | 240 | * @param \WP_Post $outbox_item The outbox item post object. |
| 408 | 241 | */ |
| 409 | - return \apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item ); | |
| 242 | + return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item ); | |
| 410 | 243 | } |
| 411 | 244 | |
| 412 | 245 | /** |
| 413 | 246 | * Get the Actor object from the Outbox item. |
| @@ -422,8 +255,11 @@ | ||
| 422 | 255 | switch ( $actor_type ) { |
| 423 | 256 | case 'blog': |
| 424 | 257 | $actor_id = Actors::BLOG_USER_ID; |
| 425 | 258 | break; |
| 259 | + case 'application': | |
| 260 | + $actor_id = Actors::APPLICATION_USER_ID; | |
| 261 | + break; | |
| 426 | 262 | case 'user': |
| 427 | 263 | default: |
| 428 | 264 | $actor_id = $outbox_item->post_author; |
| 429 | 265 | break; |
| @@ -447,45 +283,19 @@ | ||
| 447 | 283 | if ( 'ap_outbox' !== $outbox_item->post_type ) { |
| 448 | 284 | return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' ); |
| 449 | 285 | } |
| 450 | 286 | |
| 451 | - // Authenticate via Bearer token for non-REST requests (e.g. permalink access). | |
| 452 | - if ( \get_option( 'activitypub_api', false ) && ! \is_user_logged_in() && ! \wp_is_serving_rest_request() ) { | |
| 453 | - Server::authenticate_oauth( null ); | |
| 454 | - } | |
| 455 | - | |
| 456 | - /* | |
| 457 | - * Allow the author to view their own outbox items regardless of visibility. | |
| 458 | - * The `is_user_logged_in()` guard prevents anonymous visitors from matching | |
| 459 | - * the blog actor's items (where both `get_current_user_id()` and `post_author` | |
| 460 | - * are `0`), which would otherwise expose private activities at their permalink. | |
| 461 | - * | |
| 462 | - * Users authorized to act as the blog actor are treated as the author of | |
| 463 | - * blog-actor items so they can read the same private outbox they can post to. | |
| 464 | - */ | |
| 465 | - if ( \is_user_logged_in() ) { | |
| 466 | - $author = (int) $outbox_item->post_author; | |
| 467 | - | |
| 468 | - if ( \get_current_user_id() === $author ) { | |
| 469 | - return self::get_activity( $outbox_item ); | |
| 470 | - } | |
| 471 | - | |
| 472 | - if ( Actors::BLOG_USER_ID === $author && user_can_act_as_blog() ) { | |
| 473 | - return self::get_activity( $outbox_item ); | |
| 474 | - } | |
| 475 | - } | |
| 476 | - | |
| 477 | 287 | // Check if Outbox Activity is public. |
| 478 | 288 | $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true ); |
| 479 | 289 | |
| 480 | - if ( ! \in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) { | |
| 290 | + if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) { | |
| 481 | 291 | return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' ); |
| 482 | 292 | } |
| 483 | 293 | |
| 484 | - $activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', self::ACTIVITY_TYPES ); | |
| 294 | + $activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) ); | |
| 485 | 295 | $activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); |
| 486 | 296 | |
| 487 | - if ( ! \in_array( $activity_type, $activity_types, true ) ) { | |
| 297 | + if ( ! in_array( $activity_type, $activity_types, true ) ) { | |
| 488 | 298 | return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' ); |
| 489 | 299 | } |
| 490 | 300 | |
| 491 | 301 | return self::get_activity( $outbox_item ); |
| @@ -495,32 +305,28 @@ | ||
| 495 | 305 | * Get the object ID of an activity. |
| 496 | 306 | * |
| 497 | 307 | * @param Activity|Base_Object|string $data The activity object. |
| 498 | 308 | * |
| 499 | - * @return string|null The object ID. | |
| 309 | + * @return string The object ID. | |
| 500 | 310 | */ |
| 501 | 311 | private static function get_object_id( $data ) { |
| 502 | 312 | $object = $data->get_object(); |
| 503 | 313 | |
| 504 | - if ( \is_object( $object ) ) { | |
| 314 | + if ( is_object( $object ) ) { | |
| 505 | 315 | return self::get_object_id( $object ); |
| 506 | 316 | } |
| 507 | 317 | |
| 508 | - if ( \is_string( $object ) ) { | |
| 318 | + if ( is_string( $object ) ) { | |
| 509 | 319 | return $object; |
| 510 | 320 | } |
| 511 | 321 | |
| 512 | - if ( $data->get_id() ) { | |
| 513 | - return $data->get_id(); | |
| 514 | - } | |
| 515 | - | |
| 516 | - return object_to_uri( $data->get_actor() ); | |
| 322 | + return $data->get_id() ?? $data->get_actor(); | |
| 517 | 323 | } |
| 518 | 324 | |
| 519 | 325 | /** |
| 520 | 326 | * Get the title of an activity recursively. |
| 521 | 327 | * |
| 522 | - * @param Activity|Base_Object $activity_object The activity object. | |
| 328 | + * @param Base_Object $activity_object The activity object. | |
| 523 | 329 | * |
| 524 | 330 | * @return string The title. |
| 525 | 331 | */ |
| 526 | 332 | private static function get_object_title( $activity_object ) { |
| @@ -527,96 +333,19 @@ | ||
| 527 | 333 | if ( ! $activity_object ) { |
| 528 | 334 | return ''; |
| 529 | 335 | } |
| 530 | 336 | |
| 531 | - if ( \is_string( $activity_object ) ) { | |
| 532 | - $post_id = \url_to_postid( $activity_object ); | |
| 337 | + if ( is_string( $activity_object ) ) { | |
| 338 | + $post_id = url_to_postid( $activity_object ); | |
| 533 | 339 | |
| 534 | - return $post_id ? \get_the_title( $post_id ) : ''; | |
| 340 | + return $post_id ? get_the_title( $post_id ) : ''; | |
| 535 | 341 | } |
| 536 | 342 | |
| 537 | - $title = $activity_object->get_name() ?: $activity_object->get_content(); | |
| 343 | + $title = $activity_object->get_name() ?? $activity_object->get_content(); | |
| 538 | 344 | |
| 539 | 345 | if ( ! $title && $activity_object->get_object() instanceof Base_Object ) { |
| 540 | - $title = $activity_object->get_object()->get_name() ?: $activity_object->get_object()->get_content(); | |
| 346 | + $title = $activity_object->get_object()->get_name() ?? $activity_object->get_object()->get_content(); | |
| 541 | 347 | } |
| 542 | 348 | |
| 543 | 349 | return $title; |
| 544 | - } | |
| 545 | - | |
| 546 | - /** | |
| 547 | - * Purge old outbox items. | |
| 548 | - * | |
| 549 | - * Deletes outbox items older than the specified number of days, | |
| 550 | - * except for Follow activities which are always preserved. | |
| 551 | - * Also enforces a hard cap on total items via MAX_ITEMS. | |
| 552 | - * | |
| 553 | - * @param int $days Number of days to keep items. Items older than this will be deleted. | |
| 554 | - * | |
| 555 | - * @return int The number of items deleted. | |
| 556 | - */ | |
| 557 | - public static function purge( $days ) { | |
| 558 | - if ( $days <= 0 ) { | |
| 559 | - return 0; | |
| 560 | - } | |
| 561 | - | |
| 562 | - $counts = \wp_count_posts( self::POST_TYPE ); | |
| 563 | - $total = 0; | |
| 564 | - foreach ( $counts as $count ) { | |
| 565 | - $total += (int) $count; | |
| 566 | - } | |
| 567 | - | |
| 568 | - if ( $total <= 20 ) { | |
| 569 | - return 0; | |
| 570 | - } | |
| 571 | - | |
| 572 | - $deleted = 0; | |
| 573 | - $cutoff = \gmdate( 'Y-m-d', \time() - ( $days * DAY_IN_SECONDS ) ); | |
| 574 | - $start_time = \time(); | |
| 575 | - | |
| 576 | - // If total exceeds the hard cap, drop the date filter to purge oldest items first. | |
| 577 | - $overflow = $total > self::MAX_ITEMS; | |
| 578 | - $date_query = array( | |
| 579 | - array( | |
| 580 | - 'before' => $cutoff, | |
| 581 | - ), | |
| 582 | - ); | |
| 583 | - | |
| 584 | - $query_args = array( | |
| 585 | - 'post_type' => self::POST_TYPE, | |
| 586 | - 'post_status' => 'any', | |
| 587 | - 'fields' => 'ids', | |
| 588 | - 'numberposts' => self::PURGE_BATCH_SIZE, | |
| 589 | - 'orderby' => 'date', | |
| 590 | - 'order' => 'ASC', | |
| 591 | - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query | |
| 592 | - 'meta_query' => array( | |
| 593 | - array( | |
| 594 | - 'key' => '_activitypub_activity_type', | |
| 595 | - 'value' => 'Follow', | |
| 596 | - 'compare' => '!=', | |
| 597 | - ), | |
| 598 | - ), | |
| 599 | - ); | |
| 600 | - | |
| 601 | - if ( ! $overflow ) { | |
| 602 | - $query_args['date_query'] = $date_query; | |
| 603 | - } | |
| 604 | - | |
| 605 | - do { | |
| 606 | - $post_ids = \get_posts( $query_args ); | |
| 607 | - | |
| 608 | - foreach ( $post_ids as $post_id ) { | |
| 609 | - \wp_delete_post( $post_id, true ); | |
| 610 | - ++$deleted; | |
| 611 | - } | |
| 612 | - | |
| 613 | - // Once we're back under the cap, re-apply the date filter. | |
| 614 | - if ( $overflow && ( $total - $deleted ) <= self::MAX_ITEMS ) { | |
| 615 | - $overflow = false; | |
| 616 | - $query_args['date_query'] = $date_query; | |
| 617 | - } | |
| 618 | - } while ( ! empty( $post_ids ) && ( \time() - $start_time ) < self::PURGE_TIMEOUT ); | |
| 619 | - | |
| 620 | - return $deleted; | |
| 621 | 350 | } |
| 622 | 351 | } |