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
activitypub / includes / class-migration.php

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

927 lines 26.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\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
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 */
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
217 self::unlock();
218 }
219
220 /**
221 * Asynchronously migrates the database structure.
222 *
223 * @param string $version_from_db The version from which to migrate.
224 */
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 }
230
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;
244 }
245
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 }
261 }
262
263 /**
264 * Updates the custom template to use shortcodes instead of the deprecated templates.
265 */
266 private static function migrate_from_0_16() {
267 // Get the custom template.
268 $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
269
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 */
274 $need_update = false;
275
276 // If the old contents is blank, use the defaults.
277 if ( '' === $old_content ) {
278 $old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
279 $need_update = true;
280 }
281
282 // Set the new content to be the old content.
283 $content = $old_content;
284
285 // Convert old templates to shortcodes.
286 $content = \str_replace( '%title%', '[ap_title]', $content );
287 $content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
288 $content = \str_replace( '%content%', '[ap_content]', $content );
289 $content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
290 $content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
291 $content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
292 $content = \str_replace( '%tags%', '[ap_hashtags]', $content );
293
294 // Store the new template if required.
295 if ( $content !== $old_content || $need_update ) {
296 \update_option( 'activitypub_custom_post_content', $content );
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 );
925 }
926 }
927