| @@ -6,15 +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\Scheduler; | |
| 13 | -use Activitypub\Webfinger; | |
| 14 | 14 | |
| 15 | 15 | use function Activitypub\add_to_outbox; |
| 16 | -use function Activitypub\object_to_uri; | |
| 17 | 16 | |
| 18 | 17 | /** |
| 19 | 18 | * ActivityPub Outbox Collection |
| 20 | 19 | * |
| @@ -20,48 +19,11 @@ | ||
| 20 | 19 | * |
| 21 | 20 | * @link https://www.w3.org/TR/activitypub/#outbox |
| 22 | 21 | */ |
| 23 | 22 | class Outbox { |
| 24 | - /** | |
| 25 | - * The post type for the objects. | |
| 26 | - * | |
| 27 | - * @var string | |
| 28 | - */ | |
| 29 | 23 | const POST_TYPE = 'ap_outbox'; |
| 30 | 24 | |
| 31 | 25 | /** |
| 32 | - * Maximum number of outbox items to keep. | |
| 33 | - * | |
| 34 | - * When the total count exceeds this, the oldest items are purged | |
| 35 | - * regardless of their age. Acts as a safety net for runaway growth. | |
| 36 | - * | |
| 37 | - * @var int | |
| 38 | - */ | |
| 39 | - const MAX_ITEMS = 5000; | |
| 40 | - | |
| 41 | - /** | |
| 42 | - * Activity types included in the outbox collection listing. | |
| 43 | - * | |
| 44 | - * @var string[] | |
| 45 | - */ | |
| 46 | - const ACTIVITY_TYPES = array( 'Announce', 'Arrive', 'Create', 'Like', 'Update' ); | |
| 47 | - | |
| 48 | - | |
| 49 | - /** | |
| 50 | - * Number of items to process per batch during purge. | |
| 51 | - * | |
| 52 | - * @var int | |
| 53 | - */ | |
| 54 | - const PURGE_BATCH_SIZE = 100; | |
| 55 | - | |
| 56 | - /** | |
| 57 | - * Maximum seconds a purge run may take before yielding. | |
| 58 | - * | |
| 59 | - * @var int | |
| 60 | - */ | |
| 61 | - const PURGE_TIMEOUT = 30; | |
| 62 | - | |
| 63 | - /** | |
| 64 | 26 | * Add an Item to the outbox. |
| 65 | 27 | * |
| 66 | 28 | * @param Activity $activity Full Activity object that will be added to the outbox. |
| 67 | 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. |
| @@ -70,35 +32,15 @@ | ||
| 70 | 32 | * @return false|int|\WP_Error The added item or an error. |
| 71 | 33 | */ |
| 72 | 34 | public static function add( Activity $activity, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { |
| 73 | 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() ); | |
| 74 | 38 | |
| 75 | 39 | if ( ! $activity->get_actor() ) { |
| 76 | 40 | $activity->set_actor( Actors::get_by_id( $user_id )->get_id() ); |
| 77 | 41 | } |
| 78 | 42 | |
| 79 | - $object_id = object_to_uri( self::get_object_id( $activity ) ); | |
| 80 | - $title = self::get_object_title( $activity->get_object() ); | |
| 81 | - | |
| 82 | - if ( ! $object_id || ! \is_string( $object_id ) ) { | |
| 83 | - return new \WP_Error( | |
| 84 | - 'activitypub_outbox_invalid_object_id', | |
| 85 | - \__( 'Unable to determine an object ID for this activity.', 'activitypub' ), | |
| 86 | - array( 'status' => 400 ) | |
| 87 | - ); | |
| 88 | - } | |
| 89 | - | |
| 90 | - if ( ! \filter_var( $object_id, FILTER_VALIDATE_URL ) ) { | |
| 91 | - $object_id = Webfinger::resolve( $object_id ); | |
| 92 | - } | |
| 93 | - | |
| 94 | - if ( \is_wp_error( $object_id ) ) { | |
| 95 | - return $object_id; | |
| 96 | - } | |
| 97 | - | |
| 98 | - // Save activity in the context of an activitypub request. | |
| 99 | - \add_filter( 'activitypub_is_activitypub_request', '__return_true' ); | |
| 100 | - | |
| 101 | 43 | $outbox_item = array( |
| 102 | 44 | 'post_type' => self::POST_TYPE, |
| 103 | 45 | 'post_title' => sprintf( |
| 104 | 46 | /* translators: 1. Activity type, 2. Object Title or Excerpt */ |
| @@ -105,10 +47,9 @@ | ||
| 105 | 47 | __( '[%1$s] %2$s', 'activitypub' ), |
| 106 | 48 | $activity->get_type(), |
| 107 | 49 | \wp_trim_words( $title, 5 ) |
| 108 | 50 | ), |
| 109 | - // Persist the blind audience so later dispatch can compute recipients from `bto`/`bcc`. | |
| 110 | - 'post_content' => wp_slash( $activity->to_json( true, true ) ), | |
| 51 | + 'post_content' => wp_slash( $activity->to_json() ), | |
| 111 | 52 | // ensure that user ID is not below 0. |
| 112 | 53 | 'post_author' => \max( $user_id, 0 ), |
| 113 | 54 | 'post_status' => 'pending', |
| 114 | 55 | 'meta_input' => array( |
| @@ -118,10 +59,8 @@ | ||
| 118 | 59 | 'activitypub_content_visibility' => $visibility, |
| 119 | 60 | ), |
| 120 | 61 | ); |
| 121 | 62 | |
| 122 | - \remove_filter( 'activitypub_is_activitypub_request', '__return_true' ); | |
| 123 | - | |
| 124 | 63 | $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); |
| 125 | 64 | if ( $has_kses ) { |
| 126 | 65 | // Prevent KSES from corrupting JSON in post_content. |
| 127 | 66 | \kses_remove_filters(); |
| @@ -135,9 +74,9 @@ | ||
| 135 | 74 | |
| 136 | 75 | \wp_update_post( |
| 137 | 76 | array( |
| 138 | 77 | 'ID' => $id, |
| 139 | - 'post_content' => \wp_slash( $activity->to_json( true, true ) ), | |
| 78 | + 'post_content' => \wp_slash( $activity->to_json() ), | |
| 140 | 79 | ) |
| 141 | 80 | ); |
| 142 | 81 | } |
| 143 | 82 | |
| @@ -152,40 +91,26 @@ | ||
| 152 | 91 | if ( ! $id ) { |
| 153 | 92 | return false; |
| 154 | 93 | } |
| 155 | 94 | |
| 156 | - self::delete_superseded_items( $object_id, $activity->get_type(), $id ); | |
| 95 | + self::invalidate_existing_items( $object_id, $activity->get_type(), $id ); | |
| 157 | 96 | |
| 158 | 97 | return $id; |
| 159 | 98 | } |
| 160 | 99 | |
| 161 | 100 | /** |
| 162 | - * 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'. | |
| 163 | 103 | * |
| 164 | - * For most activity types, only items with the same type and object ID are | |
| 165 | - * deleted. Delete activities are a special case: they supersede all pending | |
| 166 | - * 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. | |
| 167 | 107 | * |
| 168 | - * Unschedules all federation events before deleting each item. | |
| 169 | - * Skips Follow, Announce, Accept, and Reject activities, as those are | |
| 170 | - * independent per-request responses that must not cancel each other. | |
| 171 | - * | |
| 172 | - * @param string $object_id The ActivityPub object ID (URL). | |
| 173 | - * @param string $activity_type The activity type (e.g. 'Create', 'Update', 'Delete'). | |
| 174 | - * @param int $exclude_id The ID of the newly added outbox item to keep. | |
| 175 | - * | |
| 176 | 108 | * @return void |
| 177 | 109 | */ |
| 178 | - private static function delete_superseded_items( $object_id, $activity_type, $exclude_id ) { | |
| 179 | - /* | |
| 180 | - * Do not delete items for Follow, Announce, Accept, or Reject activities. | |
| 181 | - * Follow activities from different users share the same object ID but are | |
| 182 | - * independent and must survive until their Accept is received. | |
| 183 | - * Accept/Reject are per-request responses (e.g. to individual incoming | |
| 184 | - * QuoteRequests) and must not cancel each other even when they share | |
| 185 | - * the same object ID. | |
| 186 | - */ | |
| 187 | - 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 ) { | |
| 188 | 113 | return; |
| 189 | 114 | } |
| 190 | 115 | |
| 191 | 116 | $meta_query = array( |
| @@ -194,10 +119,9 @@ | ||
| 194 | 119 | 'value' => $object_id, |
| 195 | 120 | ), |
| 196 | 121 | ); |
| 197 | 122 | |
| 198 | - // For non-Delete activities, only delete items of the same type. | |
| 199 | - // Delete activities supersede all pending items for the same object. | |
| 123 | + // For non-Delete activities, only invalidate items of the same type. | |
| 200 | 124 | if ( 'Delete' !== $activity_type ) { |
| 201 | 125 | $meta_query[] = array( |
| 202 | 126 | 'key' => '_activitypub_activity_type', |
| 203 | 127 | 'value' => $activity_type, |
| @@ -207,9 +131,9 @@ | ||
| 207 | 131 | $existing_items = get_posts( |
| 208 | 132 | array( |
| 209 | 133 | 'post_type' => self::POST_TYPE, |
| 210 | 134 | 'post_status' => 'pending', |
| 211 | - 'exclude' => array( $exclude_id ), | |
| 135 | + 'exclude' => array( $current_id ), | |
| 212 | 136 | // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query |
| 213 | 137 | 'meta_query' => $meta_query, |
| 214 | 138 | 'fields' => 'ids', |
| 215 | 139 | ) |
| @@ -215,10 +139,23 @@ | ||
| 215 | 139 | ) |
| 216 | 140 | ); |
| 217 | 141 | |
| 218 | 142 | foreach ( $existing_items as $existing_item_id ) { |
| 219 | - Scheduler::unschedule_events_for_item( $existing_item_id ); | |
| 220 | - \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' ); | |
| 221 | 158 | } |
| 222 | 159 | } |
| 223 | 160 | |
| 224 | 161 | /** |
| @@ -225,18 +162,14 @@ | ||
| 225 | 162 | * Creates an Undo activity. |
| 226 | 163 | * |
| 227 | 164 | * @param int|\WP_Post $outbox_item The Outbox post or post ID. |
| 228 | 165 | * |
| 229 | - * @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. | |
| 230 | 167 | */ |
| 231 | 168 | public static function undo( $outbox_item ) { |
| 232 | - $outbox_item = \get_post( $outbox_item ); | |
| 169 | + $outbox_item = get_post( $outbox_item ); | |
| 233 | 170 | $activity = self::get_activity( $outbox_item ); |
| 234 | 171 | |
| 235 | - if ( \is_wp_error( $activity ) ) { | |
| 236 | - return $activity; | |
| 237 | - } | |
| 238 | - | |
| 239 | 172 | $type = 'Undo'; |
| 240 | 173 | if ( 'Create' === $activity->get_type() ) { |
| 241 | 174 | $type = 'Delete'; |
| 242 | 175 | } elseif ( 'Add' === $activity->get_type() ) { |
| @@ -242,74 +175,12 @@ | ||
| 242 | 175 | } elseif ( 'Add' === $activity->get_type() ) { |
| 243 | 176 | $type = 'Remove'; |
| 244 | 177 | } |
| 245 | 178 | |
| 246 | - $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true ); | |
| 247 | - | |
| 248 | - return add_to_outbox( $activity, $type, $outbox_item->post_author, $visibility ); | |
| 179 | + return add_to_outbox( $activity, $type, $outbox_item->post_author ); | |
| 249 | 180 | } |
| 250 | 181 | |
| 251 | 182 | /** |
| 252 | - * Get an outbox item by object ID and activity type. | |
| 253 | - * | |
| 254 | - * @param string $object_id The ActivityPub object ID. | |
| 255 | - * @param string $activity_type The activity type (Create, Update, etc.). | |
| 256 | - * | |
| 257 | - * @return \WP_Post|null The outbox item or null if not found. | |
| 258 | - */ | |
| 259 | - public static function get_by_object_id( $object_id, $activity_type ) { | |
| 260 | - $outbox_items = \get_posts( | |
| 261 | - array( | |
| 262 | - 'post_type' => self::POST_TYPE, | |
| 263 | - 'post_status' => 'any', | |
| 264 | - 'posts_per_page' => 1, | |
| 265 | - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query | |
| 266 | - 'meta_query' => array( | |
| 267 | - array( | |
| 268 | - 'key' => '_activitypub_object_id', | |
| 269 | - 'value' => $object_id, | |
| 270 | - ), | |
| 271 | - array( | |
| 272 | - 'key' => '_activitypub_activity_type', | |
| 273 | - 'value' => $activity_type, | |
| 274 | - ), | |
| 275 | - ), | |
| 276 | - ) | |
| 277 | - ); | |
| 278 | - | |
| 279 | - return ! empty( $outbox_items ) ? $outbox_items[0] : null; | |
| 280 | - } | |
| 281 | - | |
| 282 | - /** | |
| 283 | - * Get an outbox item by its GUID. | |
| 284 | - * | |
| 285 | - * @param string $guid The GUID of the outbox item. | |
| 286 | - * | |
| 287 | - * @return \WP_Post|\WP_Error The outbox item or WP_Error. | |
| 288 | - */ | |
| 289 | - public static function get_by_guid( $guid ) { | |
| 290 | - global $wpdb; | |
| 291 | - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching | |
| 292 | - $post_id = $wpdb->get_var( | |
| 293 | - $wpdb->prepare( | |
| 294 | - "SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s", | |
| 295 | - \esc_url( $guid ), | |
| 296 | - self::POST_TYPE | |
| 297 | - ) | |
| 298 | - ); | |
| 299 | - | |
| 300 | - if ( ! $post_id ) { | |
| 301 | - return new \WP_Error( | |
| 302 | - 'activitypub_outbox_item_not_found', | |
| 303 | - \__( 'Outbox item not found', 'activitypub' ), | |
| 304 | - array( 'status' => 404 ) | |
| 305 | - ); | |
| 306 | - } | |
| 307 | - | |
| 308 | - return \get_post( $post_id ); | |
| 309 | - } | |
| 310 | - | |
| 311 | - /** | |
| 312 | 183 | * Reschedule an activity. |
| 313 | 184 | * |
| 314 | 185 | * @param int|\WP_Post $outbox_item The Outbox post or post ID. |
| 315 | 186 | * |
| @@ -334,16 +205,12 @@ | ||
| 334 | 205 | * @param int|\WP_Post $outbox_item The Outbox post or post ID. |
| 335 | 206 | * @return Activity|\WP_Error The Activity object or WP_Error. |
| 336 | 207 | */ |
| 337 | 208 | public static function get_activity( $outbox_item ) { |
| 338 | - $outbox_item = \get_post( $outbox_item ); | |
| 339 | - | |
| 340 | - if ( ! $outbox_item ) { | |
| 341 | - return new \WP_Error( | |
| 342 | - 'activitypub_outbox_item_not_found', | |
| 343 | - \__( 'Outbox item not found.', 'activitypub' ), | |
| 344 | - array( 'status' => 404 ) | |
| 345 | - ); | |
| 209 | + $outbox_item = get_post( $outbox_item ); | |
| 210 | + $actor = self::get_actor( $outbox_item ); | |
| 211 | + if ( is_wp_error( $actor ) ) { | |
| 212 | + return $actor; | |
| 346 | 213 | } |
| 347 | 214 | |
| 348 | 215 | $activity_object = \json_decode( $outbox_item->post_content, true ); |
| 349 | 216 | $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); |
| @@ -350,20 +217,11 @@ | ||
| 350 | 217 | |
| 351 | 218 | if ( $activity_object['type'] === $type ) { |
| 352 | 219 | $activity = Activity::init_from_array( $activity_object ); |
| 353 | 220 | if ( ! $activity->get_actor() ) { |
| 354 | - $actor = self::get_actor( $outbox_item ); | |
| 355 | - if ( \is_wp_error( $actor ) ) { | |
| 356 | - return $actor; | |
| 357 | - } | |
| 358 | 221 | $activity->set_actor( $actor->get_id() ); |
| 359 | 222 | } |
| 360 | 223 | } else { |
| 361 | - $actor = self::get_actor( $outbox_item ); | |
| 362 | - if ( \is_wp_error( $actor ) ) { | |
| 363 | - return $actor; | |
| 364 | - } | |
| 365 | - | |
| 366 | 224 | $activity = new Activity(); |
| 367 | 225 | $activity->set_type( $type ); |
| 368 | 226 | $activity->set_id( $outbox_item->guid ); |
| 369 | 227 | $activity->set_actor( $actor->get_id() ); |
| @@ -425,18 +283,8 @@ | ||
| 425 | 283 | if ( 'ap_outbox' !== $outbox_item->post_type ) { |
| 426 | 284 | return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' ); |
| 427 | 285 | } |
| 428 | 286 | |
| 429 | - // Authenticate via Bearer token for non-REST requests (e.g. permalink access). | |
| 430 | - if ( \get_option( 'activitypub_api', false ) && ! \is_user_logged_in() && ! \wp_is_serving_rest_request() ) { | |
| 431 | - \Activitypub\OAuth\Server::authenticate_oauth( null ); | |
| 432 | - } | |
| 433 | - | |
| 434 | - // Allow the author to view their own outbox items regardless of visibility. | |
| 435 | - if ( \get_current_user_id() === (int) $outbox_item->post_author ) { | |
| 436 | - return self::get_activity( $outbox_item ); | |
| 437 | - } | |
| 438 | - | |
| 439 | 287 | // Check if Outbox Activity is public. |
| 440 | 288 | $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true ); |
| 441 | 289 | |
| 442 | 290 | if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) { |
| @@ -442,9 +290,9 @@ | ||
| 442 | 290 | if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) { |
| 443 | 291 | return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' ); |
| 444 | 292 | } |
| 445 | 293 | |
| 446 | - $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' ) ); | |
| 447 | 295 | $activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); |
| 448 | 296 | |
| 449 | 297 | if ( ! in_array( $activity_type, $activity_types, true ) ) { |
| 450 | 298 | return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' ); |
| @@ -457,9 +305,9 @@ | ||
| 457 | 305 | * Get the object ID of an activity. |
| 458 | 306 | * |
| 459 | 307 | * @param Activity|Base_Object|string $data The activity object. |
| 460 | 308 | * |
| 461 | - * @return string|null The object ID. | |
| 309 | + * @return string The object ID. | |
| 462 | 310 | */ |
| 463 | 311 | private static function get_object_id( $data ) { |
| 464 | 312 | $object = $data->get_object(); |
| 465 | 313 | |
| @@ -470,19 +318,15 @@ | ||
| 470 | 318 | if ( is_string( $object ) ) { |
| 471 | 319 | return $object; |
| 472 | 320 | } |
| 473 | 321 | |
| 474 | - if ( $data->get_id() ) { | |
| 475 | - return $data->get_id(); | |
| 476 | - } | |
| 477 | - | |
| 478 | - return object_to_uri( $data->get_actor() ); | |
| 322 | + return $data->get_id() ?? $data->get_actor(); | |
| 479 | 323 | } |
| 480 | 324 | |
| 481 | 325 | /** |
| 482 | 326 | * Get the title of an activity recursively. |
| 483 | 327 | * |
| 484 | - * @param Activity|Base_Object $activity_object The activity object. | |
| 328 | + * @param Base_Object $activity_object The activity object. | |
| 485 | 329 | * |
| 486 | 330 | * @return string The title. |
| 487 | 331 | */ |
| 488 | 332 | private static function get_object_title( $activity_object ) { |
| @@ -495,90 +339,13 @@ | ||
| 495 | 339 | |
| 496 | 340 | return $post_id ? get_the_title( $post_id ) : ''; |
| 497 | 341 | } |
| 498 | 342 | |
| 499 | - $title = $activity_object->get_name() ?: $activity_object->get_content(); | |
| 343 | + $title = $activity_object->get_name() ?? $activity_object->get_content(); | |
| 500 | 344 | |
| 501 | 345 | if ( ! $title && $activity_object->get_object() instanceof Base_Object ) { |
| 502 | - $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(); | |
| 503 | 347 | } |
| 504 | 348 | |
| 505 | 349 | return $title; |
| 506 | - } | |
| 507 | - | |
| 508 | - /** | |
| 509 | - * Purge old outbox items. | |
| 510 | - * | |
| 511 | - * Deletes outbox items older than the specified number of days, | |
| 512 | - * except for Follow activities which are always preserved. | |
| 513 | - * Also enforces a hard cap on total items via MAX_ITEMS. | |
| 514 | - * | |
| 515 | - * @param int $days Number of days to keep items. Items older than this will be deleted. | |
| 516 | - * | |
| 517 | - * @return int The number of items deleted. | |
| 518 | - */ | |
| 519 | - public static function purge( $days ) { | |
| 520 | - if ( $days <= 0 ) { | |
| 521 | - return 0; | |
| 522 | - } | |
| 523 | - | |
| 524 | - $counts = \wp_count_posts( self::POST_TYPE ); | |
| 525 | - $total = 0; | |
| 526 | - foreach ( $counts as $count ) { | |
| 527 | - $total += (int) $count; | |
| 528 | - } | |
| 529 | - | |
| 530 | - if ( $total <= 20 ) { | |
| 531 | - return 0; | |
| 532 | - } | |
| 533 | - | |
| 534 | - $deleted = 0; | |
| 535 | - $cutoff = \gmdate( 'Y-m-d', \time() - ( $days * DAY_IN_SECONDS ) ); | |
| 536 | - $start_time = \time(); | |
| 537 | - | |
| 538 | - // If total exceeds the hard cap, drop the date filter to purge oldest items first. | |
| 539 | - $overflow = $total > self::MAX_ITEMS; | |
| 540 | - $date_query = array( | |
| 541 | - array( | |
| 542 | - 'before' => $cutoff, | |
| 543 | - ), | |
| 544 | - ); | |
| 545 | - | |
| 546 | - $query_args = array( | |
| 547 | - 'post_type' => self::POST_TYPE, | |
| 548 | - 'post_status' => 'any', | |
| 549 | - 'fields' => 'ids', | |
| 550 | - 'numberposts' => self::PURGE_BATCH_SIZE, | |
| 551 | - 'orderby' => 'date', | |
| 552 | - 'order' => 'ASC', | |
| 553 | - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query | |
| 554 | - 'meta_query' => array( | |
| 555 | - array( | |
| 556 | - 'key' => '_activitypub_activity_type', | |
| 557 | - 'value' => 'Follow', | |
| 558 | - 'compare' => '!=', | |
| 559 | - ), | |
| 560 | - ), | |
| 561 | - ); | |
| 562 | - | |
| 563 | - if ( ! $overflow ) { | |
| 564 | - $query_args['date_query'] = $date_query; | |
| 565 | - } | |
| 566 | - | |
| 567 | - do { | |
| 568 | - $post_ids = \get_posts( $query_args ); | |
| 569 | - | |
| 570 | - foreach ( $post_ids as $post_id ) { | |
| 571 | - \wp_delete_post( $post_id, true ); | |
| 572 | - ++$deleted; | |
| 573 | - } | |
| 574 | - | |
| 575 | - // Once we're back under the cap, re-apply the date filter. | |
| 576 | - if ( $overflow && ( $total - $deleted ) <= self::MAX_ITEMS ) { | |
| 577 | - $overflow = false; | |
| 578 | - $query_args['date_query'] = $date_query; | |
| 579 | - } | |
| 580 | - } while ( ! empty( $post_ids ) && ( \time() - $start_time ) < self::PURGE_TIMEOUT ); | |
| 581 | - | |
| 582 | - return $deleted; | |
| 583 | 350 | } |
| 584 | 351 | } |