PluginProbe
ActivityPub / 7.0.0
ActivityPub v7.0.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 / class-migration.php

class-migration.php in ActivityPub 7.0.0, at includes/class-migration.php

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