PluginProbe
ActivityPub / 5.9.0
ActivityPub v5.9.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 5.9.0, at includes/class-migration.php

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