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
← All changes | includes/class-migration.php +778 -31 1.2.05.7.0 View file →
@@ -1,10 +1,18 @@
1 1 <?php
2 +/**
3 + * Migration class file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub;
3 9
4 -use Activitypub\Activitypub;
5 -use Activitypub\Model\Blog_User;
10 +use Activitypub\Collection\Actors;
11 +use Activitypub\Collection\Extra_Fields;
6 12 use Activitypub\Collection\Followers;
13 +use Activitypub\Collection\Outbox;
14 +use Activitypub\Transformer\Factory;
7 15
8 16 /**
9 17 * ActivityPub Migration Class
10 18 *
@@ -11,12 +19,16 @@
11 19 * @author Matthias Pfefferle
12 20 */
13 21 class Migration {
14 22 /**
15 - * Initialize the class, registering WordPress hooks
23 + * Initialize the class, registering WordPress hooks.
16 24 */
17 25 public static function init() {
18 - \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) );
26 + \add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
27 + \add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 );
28 + \add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
29 +
30 + self::maybe_migrate();
19 31 }
20 32
21 33 /**
22 34 * Get the target version.
@@ -23,12 +35,16 @@
23 35 *
24 36 * This is the version that the database structure will be updated to.
25 37 * It is the same as the plugin version.
26 38 *
39 + * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
40 + *
27 41 * @return string The target version.
28 42 */
29 43 public static function get_target_version() {
30 - return get_plugin_version();
44 + _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
45 +
46 + return ACTIVITYPUB_PLUGIN_VERSION;
31 47 }
32 48
33 49 /**
34 50 * The current version of the database structure.
@@ -41,18 +57,25 @@
41 57
42 58 /**
43 59 * Locks the database migration process to prevent simultaneous migrations.
44 60 *
45 - * @return void
61 + * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
46 62 */
47 63 public static function lock() {
48 - \update_option( 'activitypub_migration_lock', \time() );
64 + global $wpdb;
65 +
66 + // Try to lock.
67 + $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_migration_lock', \time() ) ); // phpcs:ignore WordPress.DB
68 +
69 + if ( ! $lock_result ) {
70 + $lock_result = \get_option( 'activitypub_migration_lock' );
71 + }
72 +
73 + return $lock_result;
49 74 }
50 75
51 76 /**
52 77 * Unlocks the database migration process.
53 - *
54 - * @return void
55 78 */
56 79 public static function unlock() {
57 80 \delete_option( 'activitypub_migration_lock' );
58 81 }
@@ -84,11 +107,11 @@
84 107 *
85 108 * @return bool True if the database structure is up to date, false otherwise.
86 109 */
87 110 public static function is_latest_version() {
88 - return (bool) version_compare(
111 + return (bool) \version_compare(
89 112 self::get_version(),
90 - self::get_target_version(),
113 + ACTIVITYPUB_PLUGIN_VERSION,
91 114 '=='
92 115 );
93 116 }
94 117
@@ -107,51 +130,148 @@
107 130 self::lock();
108 131
109 132 $version_from_db = self::get_version();
110 133
111 - if ( version_compare( $version_from_db, '0.17.0', '<' ) ) {
134 + // Check for initial migration.
135 + if ( ! $version_from_db ) {
136 + self::add_default_settings();
137 + $version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
138 + }
139 +
140 + // Schedule the async migration.
141 + if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
142 + \wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
143 + }
144 + if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
112 145 self::migrate_from_0_16();
113 146 }
114 - if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
115 - self::migrate_from_0_17();
147 + if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
148 + self::migrate_from_1_2_0();
116 149 }
150 + if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
151 + self::migrate_from_2_0_0();
152 + }
153 + if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) {
154 + self::migrate_from_2_2_0();
155 + }
156 + if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) {
157 + self::migrate_from_2_6_0();
158 + }
159 + if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) {
160 + self::migrate_to_4_0_0();
161 + }
162 + if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
163 + self::migrate_to_4_1_0();
164 + }
165 + if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
166 + \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
167 + }
168 + if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) {
169 + self::migrate_to_4_7_1();
170 + }
171 + if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
172 + self::migrate_to_4_7_2();
173 + }
174 + if ( \version_compare( $version_from_db, '4.7.3', '<' ) ) {
175 + add_action( 'init', 'flush_rewrite_rules', 20 );
176 + }
177 + if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
178 + Scheduler::register_schedules();
179 + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) );
180 + \wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) );
181 + add_action( 'init', 'flush_rewrite_rules', 20 );
182 + }
183 + if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
184 + Scheduler::register_schedules();
185 + }
186 + if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
187 + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
188 + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
189 + \add_action( 'init', 'flush_rewrite_rules', 20 );
190 + }
191 + if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
192 + self::delete_mastodon_api_orphaned_extra_fields();
193 + }
117 194
118 - update_option( 'activitypub_db_version', self::get_target_version() );
195 + /*
196 + * Add new update routines above this comment. ^
197 + *
198 + * Use 'unreleased' as the version number for new migrations and add tests for the callback directly.
199 + * The release script will automatically replace it with the actual version number.
200 + * Example:
201 + *
202 + * if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
203 + * // Update routine.
204 + * }
205 + */
119 206
207 + /**
208 + * Fires when the system has to be migrated.
209 + *
210 + * @param string $version_from_db The version from which to migrate.
211 + * @param string $target_version The target version to migrate to.
212 + */
213 + \do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION );
214 +
215 + \update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION );
216 +
120 217 self::unlock();
121 218 }
122 219
123 220 /**
124 - * Updates the DB-schema of the followers-list
221 + * Asynchronously migrates the database structure.
125 222 *
126 - * @return void
223 + * @param string $version_from_db The version from which to migrate.
127 224 */
128 - private static function migrate_from_0_17() {
129 - // migrate followers
130 - foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
131 - $followers = get_user_meta( $user_id, 'activitypub_followers', true );
225 + public static function async_migration( $version_from_db ) {
226 + if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
227 + self::migrate_from_0_17();
228 + }
229 + }
132 230
133 - if ( $followers ) {
134 - foreach ( $followers as $actor ) {
135 - Followers::add_follower( $user_id, $actor );
136 - }
137 - }
231 + /**
232 + * Asynchronously runs upgrade routines.
233 + *
234 + * @param callable $callback Callable upgrade routine. Must be a method of this class.
235 + * @params mixed ...$args Optional. Parameters that get passed to the callback.
236 + */
237 + public static function async_upgrade( $callback ) {
238 + $args = \func_get_args();
239 +
240 + // Bail if the existing lock is still valid.
241 + if ( self::is_locked() ) {
242 + \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args );
243 + return;
138 244 }
139 245
140 - Activitypub::flush_rewrite_rules();
246 + self::lock();
247 +
248 + $callback = array_shift( $args ); // Remove $callback from arguments.
249 + $next = \call_user_func_array( array( self::class, $callback ), $args );
250 +
251 + self::unlock();
252 +
253 + if ( ! empty( $next ) ) {
254 + // Schedule the next run, adding the result to the arguments.
255 + \wp_schedule_single_event(
256 + \time() + 30,
257 + 'activitypub_upgrade',
258 + \array_merge( array( $callback ), \array_values( $next ) )
259 + );
260 + }
141 261 }
142 262
143 263 /**
144 264 * Updates the custom template to use shortcodes instead of the deprecated templates.
145 - *
146 - * @return void
147 265 */
148 266 private static function migrate_from_0_16() {
149 267 // Get the custom template.
150 268 $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
151 269
152 - // If the old content exists but is a blank string, we're going to need a flag to updated it even
153 - // after setting it to the default contents.
270 + /*
271 + * If the old content exists but is a blank string, we're going to need a flag to updated it even
272 + * after setting it to the default contents.
273 + */
154 274 $need_update = false;
155 275
156 276 // If the old contents is blank, use the defaults.
157 277 if ( '' === $old_content ) {
@@ -174,6 +294,633 @@
174 294 // Store the new template if required.
175 295 if ( $content !== $old_content || $need_update ) {
176 296 \update_option( 'activitypub_custom_post_content', $content );
177 297 }
298 + }
299 +
300 + /**
301 + * Updates the DB-schema of the followers-list.
302 + */
303 + public static function migrate_from_0_17() {
304 + // Migrate followers.
305 + foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
306 + $followers = get_user_meta( $user_id, 'activitypub_followers', true );
307 +
308 + if ( $followers ) {
309 + foreach ( $followers as $actor ) {
310 + Followers::add_follower( $user_id, $actor );
311 + }
312 + }
313 + }
314 +
315 + Activitypub::flush_rewrite_rules();
316 + }
317 +
318 + /**
319 + * Clear the cache after updating to 1.3.0.
320 + */
321 + private static function migrate_from_1_2_0() {
322 + $user_ids = \get_users(
323 + array(
324 + 'fields' => 'ID',
325 + 'capability__in' => array( 'publish_posts' ),
326 + )
327 + );
328 +
329 + foreach ( $user_ids as $user_id ) {
330 + wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
331 + }
332 + }
333 +
334 + /**
335 + * Unschedule Hooks after updating to 2.0.0.
336 + */
337 + private static function migrate_from_2_0_0() {
338 + wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
339 + wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
340 + wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
341 +
342 + wp_unschedule_hook( 'activitypub_send_post_activity' );
343 + wp_unschedule_hook( 'activitypub_send_update_activity' );
344 + wp_unschedule_hook( 'activitypub_send_delete_activity' );
345 +
346 + $object_type = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
347 + if ( 'article' === $object_type ) {
348 + \update_option( 'activitypub_object_type', 'wordpress-post-format' );
349 + }
350 + }
351 +
352 + /**
353 + * Add the ActivityPub capability to all users that can publish posts
354 + * Delete old meta to store followers.
355 + */
356 + private static function migrate_from_2_2_0() {
357 + // Add the ActivityPub capability to all users that can publish posts.
358 + self::add_activitypub_capability();
359 + }
360 +
361 + /**
362 + * Rename DB fields.
363 + */
364 + private static function migrate_from_2_6_0() {
365 + wp_cache_flush();
366 +
367 + self::update_usermeta_key( 'activitypub_user_description', 'activitypub_description' );
368 +
369 + self::update_options_key( 'activitypub_blog_user_description', 'activitypub_blog_description' );
370 + self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
371 + }
372 +
373 + /**
374 + * * Update actor-mode settings.
375 + * * Get the ID of the latest blog post and save it to the options table.
376 + */
377 + private static function migrate_to_4_0_0() {
378 + $latest_post_id = 0;
379 +
380 + // Get the ID of the latest blog post and save it to the options table.
381 + $latest_post = get_posts(
382 + array(
383 + 'numberposts' => 1,
384 + 'orderby' => 'ID',
385 + 'order' => 'DESC',
386 + 'post_type' => 'any',
387 + 'post_status' => 'publish',
388 + )
389 + );
390 +
391 + if ( $latest_post ) {
392 + $latest_post_id = $latest_post[0]->ID;
393 + }
394 +
395 + \update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
396 +
397 + $users = \get_users(
398 + array(
399 + 'capability__in' => array( 'activitypub' ),
400 + )
401 + );
402 +
403 + foreach ( $users as $user ) {
404 + $followers = Followers::get_followers( $user->ID );
405 +
406 + if ( $followers ) {
407 + \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
408 + }
409 + }
410 +
411 + $followers = Followers::get_followers( Actors::BLOG_USER_ID );
412 +
413 + if ( $followers ) {
414 + \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
415 + }
416 +
417 + self::migrate_actor_mode();
418 + }
419 +
420 + /**
421 + * Upate to 4.1.0
422 + *
423 + * * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
424 + */
425 + public static function migrate_to_4_1_0() {
426 + $content_type = \get_option( 'activitypub_post_content_type' );
427 +
428 + switch ( $content_type ) {
429 + case 'excerpt':
430 + $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
431 + break;
432 + case 'title':
433 + $template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]";
434 + break;
435 + case 'content':
436 + $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
437 + break;
438 + case 'custom':
439 + $template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
440 + break;
441 + default:
442 + $template = ACTIVITYPUB_CUSTOM_POST_CONTENT;
443 + break;
444 + }
445 +
446 + \update_option( 'activitypub_custom_post_content', $template );
447 +
448 + \delete_option( 'activitypub_post_content_type' );
449 +
450 + $object_type = \get_option( 'activitypub_object_type', false );
451 + if ( ! $object_type ) {
452 + \update_option( 'activitypub_object_type', 'note' );
453 + }
454 +
455 + // Clean up empty visibility meta.
456 + global $wpdb;
457 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
458 + "DELETE FROM $wpdb->postmeta
459 + WHERE meta_key = 'activitypub_content_visibility'
460 + AND (meta_value IS NULL OR meta_value = '')"
461 + );
462 + }
463 +
464 + /**
465 + * Updates post meta keys to be prefixed with an underscore.
466 + */
467 + public static function migrate_to_4_7_1() {
468 + global $wpdb;
469 +
470 + $meta_keys = array(
471 + 'activitypub_actor_json',
472 + 'activitypub_canonical_url',
473 + 'activitypub_errors',
474 + 'activitypub_inbox',
475 + 'activitypub_user_id',
476 + );
477 +
478 + foreach ( $meta_keys as $meta_key ) {
479 + // phpcs:ignore WordPress.DB
480 + $wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) );
481 + }
482 + }
483 +
484 + /**
485 + * Clears the post cache for Followers, we should have done this in 4.7.1 when we renamed those keys.
486 + */
487 + public static function migrate_to_4_7_2() {
488 + global $wpdb;
489 + // phpcs:ignore WordPress.DB
490 + $followers = $wpdb->get_col(
491 + $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE )
492 + );
493 + foreach ( $followers as $id ) {
494 + clean_post_cache( $id );
495 + }
496 + }
497 +
498 + /**
499 + * Update comment counts for posts in batches.
500 + *
501 + * @see Comment::pre_wp_update_comment_count_now()
502 + * @param int $batch_size Optional. Number of posts to process per batch. Default 100.
503 + * @param int $offset Optional. Number of posts to skip. Default 0.
504 + */
505 + public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
506 + global $wpdb;
507 +
508 + // Bail if the existing lock is still valid.
509 + if ( self::is_locked() ) {
510 + \wp_schedule_single_event(
511 + time() + ( 5 * MINUTE_IN_SECONDS ),
512 + 'activitypub_update_comment_counts',
513 + array(
514 + 'batch_size' => $batch_size,
515 + 'offset' => $offset,
516 + )
517 + );
518 + return;
519 + }
520 +
521 + self::lock();
522 +
523 + Comment::register_comment_types();
524 + $comment_types = Comment::get_comment_type_slugs();
525 + $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
526 +
527 + // Get and process this batch.
528 + $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
529 + $wpdb->prepare(
530 + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
531 + "SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
532 + $batch_size,
533 + $offset
534 + )
535 + );
536 +
537 + foreach ( $post_ids as $post_id ) {
538 + \wp_update_comment_count_now( $post_id );
539 + }
540 +
541 + if ( count( $post_ids ) === $batch_size ) {
542 + // Schedule next batch.
543 + \wp_schedule_single_event(
544 + time() + MINUTE_IN_SECONDS,
545 + 'activitypub_update_comment_counts',
546 + array(
547 + 'batch_size' => $batch_size,
548 + 'offset' => $offset + $batch_size,
549 + )
550 + );
551 + }
552 +
553 + self::unlock();
554 + }
555 +
556 + /**
557 + * Create outbox items for posts in batches.
558 + *
559 + * @param int $batch_size Optional. Number of posts to process per batch. Default 50.
560 + * @param int $offset Optional. Number of posts to skip. Default 0.
561 + * @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
562 + */
563 + public static function create_post_outbox_items( $batch_size = 50, $offset = 0 ) {
564 + $posts = \get_posts(
565 + array(
566 + // our own `ap_outbox` will be excluded from `any` by virtue of its `exclude_from_search` arg.
567 + 'post_type' => 'any',
568 + 'posts_per_page' => $batch_size,
569 + 'offset' => $offset,
570 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
571 + 'meta_query' => array(
572 + array(
573 + 'key' => 'activitypub_status',
574 + 'value' => 'federated',
575 + ),
576 + ),
577 + )
578 + );
579 +
580 + // Avoid multiple queries for post meta.
581 + \update_postmeta_cache( \wp_list_pluck( $posts, 'ID' ) );
582 +
583 + foreach ( $posts as $post ) {
584 + $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
585 +
586 + self::add_to_outbox( $post, 'Create', $post->post_author, $visibility );
587 +
588 + // Add Update activity when the post has been modified.
589 + if ( $post->post_modified !== $post->post_date ) {
590 + self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
591 + }
592 + }
593 +
594 + if ( count( $posts ) === $batch_size ) {
595 + return array(
596 + 'batch_size' => $batch_size,
597 + 'offset' => $offset + $batch_size,
598 + );
599 + }
600 +
601 + return null;
602 + }
603 +
604 + /**
605 + * Create outbox items for comments in batches.
606 + *
607 + * @param int $batch_size Optional. Number of posts to process per batch. Default 50.
608 + * @param int $offset Optional. Number of posts to skip. Default 0.
609 + * @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
610 + */
611 + public static function create_comment_outbox_items( $batch_size = 50, $offset = 0 ) {
612 + $comments = \get_comments(
613 + array(
614 + 'author__not_in' => array( 0 ), // Limit to comments by registered users.
615 + 'number' => $batch_size,
616 + 'offset' => $offset,
617 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
618 + 'meta_query' => array(
619 + array(
620 + 'key' => 'activitypub_status',
621 + 'value' => 'federated',
622 + ),
623 + ),
624 + )
625 + );
626 +
627 + foreach ( $comments as $comment ) {
628 + self::add_to_outbox( $comment, 'Create', $comment->user_id );
629 + }
630 +
631 + if ( count( $comments ) === $batch_size ) {
632 + return array(
633 + 'batch_size' => $batch_size,
634 + 'offset' => $offset + $batch_size,
635 + );
636 + }
637 +
638 + return null;
639 + }
640 +
641 + /**
642 + * Update _activitypub_actor_json meta values to ensure they are properly slashed.
643 + *
644 + * @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
645 + * @param int $offset Optional. Number of meta values to skip. Default 0.
646 + * @return array|null Array with batch size and offset if there are more meta values to process, null otherwise.
647 + */
648 + public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) {
649 + global $wpdb;
650 +
651 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery
652 + $meta_values = $wpdb->get_results(
653 + $wpdb->prepare(
654 + "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d",
655 + $batch_size,
656 + $offset
657 + )
658 + );
659 +
660 + foreach ( $meta_values as $meta ) {
661 + $json = \json_decode( $meta->meta_value, true );
662 +
663 + // If json_decode fails, try adding slashes.
664 + if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) {
665 + $escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value );
666 + $json = \json_decode( $escaped_value, true );
667 +
668 + // Update the meta if json_decode succeeds with slashes.
669 + if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) {
670 + \update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) );
671 + }
672 + }
673 + }
674 +
675 + if ( \count( $meta_values ) === $batch_size ) {
676 + return array(
677 + 'batch_size' => $batch_size,
678 + 'offset' => $offset + $batch_size,
679 + );
680 + }
681 +
682 + return null;
683 + }
684 +
685 + /**
686 + * Update comment author emails with webfinger addresses for ActivityPub comments.
687 + *
688 + * @param int $batch_size Optional. Number of comments to process per batch. Default 50.
689 + * @param int $offset Optional. Number of comments to skip. Default 0.
690 + * @return array|null Array with batch size and offset if there are more comments to process, null otherwise.
691 + */
692 + public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) {
693 + $comments = \get_comments(
694 + array(
695 + 'number' => $batch_size,
696 + 'offset' => $offset,
697 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
698 + 'meta_query' => array(
699 + array(
700 + 'key' => 'protocol',
701 + 'value' => 'activitypub',
702 + ),
703 + ),
704 + )
705 + );
706 +
707 + foreach ( $comments as $comment ) {
708 + $comment_author_url = $comment->comment_author_url;
709 + if ( empty( $comment_author_url ) ) {
710 + continue;
711 + }
712 +
713 + $webfinger = Webfinger::uri_to_acct( $comment_author_url );
714 + if ( \is_wp_error( $webfinger ) ) {
715 + continue;
716 + }
717 +
718 + \wp_update_comment(
719 + array(
720 + 'comment_ID' => $comment->comment_ID,
721 + 'comment_author_email' => \str_replace( 'acct:', '', $webfinger ),
722 + )
723 + );
724 + }
725 +
726 + if ( count( $comments ) === $batch_size ) {
727 + return array(
728 + 'batch_size' => $batch_size,
729 + 'offset' => $offset + $batch_size,
730 + );
731 + }
732 +
733 + return null;
734 + }
735 +
736 + /**
737 + * Set the defaults needed for the plugin to work.
738 + *
739 + * Add the ActivityPub capability to all users that can publish posts.
740 + */
741 + public static function add_default_settings() {
742 + self::add_activitypub_capability();
743 + self::add_notification_defaults();
744 + self::add_default_extra_field();
745 + }
746 +
747 + /**
748 + * Add an activity to the outbox without federating it.
749 + *
750 + * @param \WP_Post|\WP_Comment $comment The comment or post object.
751 + * @param string $activity_type The type of activity.
752 + * @param int $user_id The user ID.
753 + * @param string $visibility Optional. The visibility of the content. Default 'public'.
754 + */
755 + private static function add_to_outbox( $comment, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
756 + $transformer = Factory::get_transformer( $comment );
757 + if ( ! $transformer || \is_wp_error( $transformer ) ) {
758 + return;
759 + }
760 +
761 + $activity = $transformer->to_activity( $activity_type );
762 + if ( ! $activity || \is_wp_error( $activity ) ) {
763 + return;
764 + }
765 +
766 + // If the user is disabled, fall back to the blog user when available.
767 + if ( ! user_can_activitypub( $user_id ) ) {
768 + if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
769 + $user_id = Actors::BLOG_USER_ID;
770 + } else {
771 + return;
772 + }
773 + }
774 +
775 + $post_id = Outbox::add( $activity, $user_id, $visibility );
776 +
777 + // Immediately set to publish, no federation needed.
778 + \wp_publish_post( $post_id );
779 + }
780 +
781 + /**
782 + * Add the ActivityPub capability to all users that can publish posts.
783 + */
784 + private static function add_activitypub_capability() {
785 + // Get all WP_User objects that can publish posts.
786 + $users = \get_users(
787 + array(
788 + 'capability__in' => array( 'publish_posts' ),
789 + )
790 + );
791 +
792 + // Add ActivityPub capability to all users that can publish posts.
793 + foreach ( $users as $user ) {
794 + $user->add_cap( 'activitypub' );
795 + }
796 + }
797 +
798 + /**
799 + * Add default notification settings.
800 + */
801 + private static function add_notification_defaults() {
802 + \add_option( 'activitypub_mailer_new_follower', '1' );
803 + \add_option( 'activitypub_mailer_new_dm', '1' );
804 + }
805 +
806 + /**
807 + * Add a default extra field for the user.
808 + */
809 + private static function add_default_extra_field() {
810 + $users = \get_users(
811 + array(
812 + 'capability__in' => array( 'activitypub' ),
813 + )
814 + );
815 +
816 + $title = \__( 'Powered by', 'activitypub' );
817 + $content = 'WordPress';
818 +
819 + // Add a default extra field for each user.
820 + foreach ( $users as $user ) {
821 + \wp_insert_post(
822 + array(
823 + 'post_type' => Extra_Fields::USER_POST_TYPE,
824 + 'post_author' => $user->ID,
825 + 'post_status' => 'publish',
826 + 'post_title' => $title,
827 + 'post_content' => $content,
828 + )
829 + );
830 + }
831 +
832 + \wp_insert_post(
833 + array(
834 + 'post_type' => Extra_Fields::BLOG_POST_TYPE,
835 + 'post_author' => 0,
836 + 'post_status' => 'publish',
837 + 'post_title' => $title,
838 + 'post_content' => $content,
839 + )
840 + );
841 + }
842 +
843 + /**
844 + * Rename meta keys.
845 + *
846 + * @param string $old_key The old comment meta key.
847 + * @param string $new_key The new comment meta key.
848 + */
849 + private static function update_usermeta_key( $old_key, $new_key ) {
850 + global $wpdb;
851 +
852 + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
853 + $wpdb->usermeta,
854 + array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
855 + array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
856 + array( '%s' ),
857 + array( '%s' )
858 + );
859 + }
860 +
861 + /**
862 + * Rename option keys.
863 + *
864 + * @param string $old_key The old option key.
865 + * @param string $new_key The new option key.
866 + */
867 + private static function update_options_key( $old_key, $new_key ) {
868 + global $wpdb;
869 +
870 + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
871 + $wpdb->options,
872 + array( 'option_name' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
873 + array( 'option_name' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
874 + array( '%s' ),
875 + array( '%s' )
876 + );
877 + }
878 +
879 + /**
880 + * Migrate the actor mode settings.
881 + */
882 + public static function migrate_actor_mode() {
883 + $blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
884 + $author_profiles = \get_option( 'activitypub_enable_users', '1' );
885 +
886 + if (
887 + '1' === $blog_profile &&
888 + '1' === $author_profiles
889 + ) {
890 + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
891 + } elseif (
892 + '1' === $blog_profile &&
893 + '1' !== $author_profiles
894 + ) {
895 + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
896 + } elseif (
897 + '1' !== $blog_profile &&
898 + '1' === $author_profiles
899 + ) {
900 + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
901 + }
902 + }
903 +
904 + /**
905 + * Deletes user extra fields where the author is the blog user.
906 + *
907 + * These extra fields were created when the Enable Mastodon Apps integration passed
908 + * an author_url instead of a user_id to the mastodon_api_account filter. This caused
909 + * Extra_Fields::default_actor_extra_fields() to run but fail to cache the fact it ran
910 + * for non-existent users. The result is a number of user extra fields with no author.
911 + *
912 + * @ticket https://github.com/Automattic/wordpress-activitypub/pull/1554
913 + */
914 + public static function delete_mastodon_api_orphaned_extra_fields() {
915 + global $wpdb;
916 +
917 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery
918 + $wpdb->delete(
919 + $wpdb->posts,
920 + array(
921 + 'post_type' => Extra_Fields::USER_POST_TYPE,
922 + 'post_author' => Actors::BLOG_USER_ID,
923 + )
924 + );
178 925 }
179 926 }