PluginProbe
ActivityPub / 4.7.1
ActivityPub v4.7.1
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 4.7.1, at includes/class-migration.php

567 lines 15.3 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\Followers;
12
13 /**
14 * ActivityPub Migration Class
15 *
16 * @author Matthias Pfefferle
17 */
18 class Migration {
19 /**
20 * Initialize the class, registering WordPress hooks.
21 */
22 public static function init() {
23 \add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
24 \add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
25
26 self::maybe_migrate();
27 }
28
29 /**
30 * Get the target version.
31 *
32 * This is the version that the database structure will be updated to.
33 * It is the same as the plugin version.
34 *
35 * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
36 *
37 * @return string The target version.
38 */
39 public static function get_target_version() {
40 _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
41
42 return ACTIVITYPUB_PLUGIN_VERSION;
43 }
44
45 /**
46 * The current version of the database structure.
47 *
48 * @return string The current version.
49 */
50 public static function get_version() {
51 return get_option( 'activitypub_db_version', 0 );
52 }
53
54 /**
55 * Locks the database migration process to prevent simultaneous migrations.
56 *
57 * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
58 */
59 public static function lock() {
60 global $wpdb;
61
62 // Try to lock.
63 $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
64
65 if ( ! $lock_result ) {
66 $lock_result = \get_option( 'activitypub_migration_lock' );
67 }
68
69 return $lock_result;
70 }
71
72 /**
73 * Unlocks the database migration process.
74 */
75 public static function unlock() {
76 \delete_option( 'activitypub_migration_lock' );
77 }
78
79 /**
80 * Whether the database migration process is locked.
81 *
82 * @return boolean
83 */
84 public static function is_locked() {
85 $lock = \get_option( 'activitypub_migration_lock' );
86
87 if ( ! $lock ) {
88 return false;
89 }
90
91 $lock = (int) $lock;
92
93 if ( $lock < \time() - 1800 ) {
94 self::unlock();
95 return false;
96 }
97
98 return true;
99 }
100
101 /**
102 * Whether the database structure is up to date.
103 *
104 * @return bool True if the database structure is up to date, false otherwise.
105 */
106 public static function is_latest_version() {
107 return (bool) \version_compare(
108 self::get_version(),
109 ACTIVITYPUB_PLUGIN_VERSION,
110 '=='
111 );
112 }
113
114 /**
115 * Updates the database structure if necessary.
116 */
117 public static function maybe_migrate() {
118 if ( self::is_latest_version() ) {
119 return;
120 }
121
122 if ( self::is_locked() ) {
123 return;
124 }
125
126 self::lock();
127
128 $version_from_db = self::get_version();
129
130 // Check for initial migration.
131 if ( ! $version_from_db ) {
132 self::add_default_settings();
133 $version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
134 }
135
136 // Schedule the async migration.
137 if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
138 \wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
139 }
140 if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
141 self::migrate_from_0_16();
142 }
143 if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
144 self::migrate_from_1_2_0();
145 }
146 if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
147 self::migrate_from_2_0_0();
148 }
149 if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) {
150 self::migrate_from_2_2_0();
151 }
152 if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) {
153 self::migrate_from_2_6_0();
154 }
155 if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) {
156 self::migrate_to_4_0_0();
157 }
158 if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
159 self::migrate_to_4_1_0();
160 }
161 if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
162 \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
163 }
164 if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) {
165 self::migrate_to_4_7_1();
166 }
167
168 /**
169 * Fires when the system has to be migrated.
170 *
171 * @param string $version_from_db The version from which to migrate.
172 * @param string $target_version The target version to migrate to.
173 */
174 \do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION );
175
176 \update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION );
177
178 self::unlock();
179 }
180
181 /**
182 * Asynchronously migrates the database structure.
183 *
184 * @param string $version_from_db The version from which to migrate.
185 */
186 public static function async_migration( $version_from_db ) {
187 if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
188 self::migrate_from_0_17();
189 }
190 }
191
192 /**
193 * Updates the custom template to use shortcodes instead of the deprecated templates.
194 */
195 private static function migrate_from_0_16() {
196 // Get the custom template.
197 $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
198
199 /*
200 * If the old content exists but is a blank string, we're going to need a flag to updated it even
201 * after setting it to the default contents.
202 */
203 $need_update = false;
204
205 // If the old contents is blank, use the defaults.
206 if ( '' === $old_content ) {
207 $old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
208 $need_update = true;
209 }
210
211 // Set the new content to be the old content.
212 $content = $old_content;
213
214 // Convert old templates to shortcodes.
215 $content = \str_replace( '%title%', '[ap_title]', $content );
216 $content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
217 $content = \str_replace( '%content%', '[ap_content]', $content );
218 $content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
219 $content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
220 $content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
221 $content = \str_replace( '%tags%', '[ap_hashtags]', $content );
222
223 // Store the new template if required.
224 if ( $content !== $old_content || $need_update ) {
225 \update_option( 'activitypub_custom_post_content', $content );
226 }
227 }
228
229 /**
230 * Updates the DB-schema of the followers-list.
231 */
232 public static function migrate_from_0_17() {
233 // Migrate followers.
234 foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
235 $followers = get_user_meta( $user_id, 'activitypub_followers', true );
236
237 if ( $followers ) {
238 foreach ( $followers as $actor ) {
239 Followers::add_follower( $user_id, $actor );
240 }
241 }
242 }
243
244 Activitypub::flush_rewrite_rules();
245 }
246
247 /**
248 * Clear the cache after updating to 1.3.0.
249 */
250 private static function migrate_from_1_2_0() {
251 $user_ids = \get_users(
252 array(
253 'fields' => 'ID',
254 'capability__in' => array( 'publish_posts' ),
255 )
256 );
257
258 foreach ( $user_ids as $user_id ) {
259 wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
260 }
261 }
262
263 /**
264 * Unschedule Hooks after updating to 2.0.0.
265 */
266 private static function migrate_from_2_0_0() {
267 wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
268 wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
269 wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
270
271 wp_unschedule_hook( 'activitypub_send_post_activity' );
272 wp_unschedule_hook( 'activitypub_send_update_activity' );
273 wp_unschedule_hook( 'activitypub_send_delete_activity' );
274
275 $object_type = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
276 if ( 'article' === $object_type ) {
277 \update_option( 'activitypub_object_type', 'wordpress-post-format' );
278 }
279 }
280
281 /**
282 * Add the ActivityPub capability to all users that can publish posts
283 * Delete old meta to store followers.
284 */
285 private static function migrate_from_2_2_0() {
286 // Add the ActivityPub capability to all users that can publish posts.
287 self::add_activitypub_capability();
288 }
289
290 /**
291 * Rename DB fields.
292 */
293 private static function migrate_from_2_6_0() {
294 wp_cache_flush();
295
296 self::update_usermeta_key( 'activitypub_user_description', 'activitypub_description' );
297
298 self::update_options_key( 'activitypub_blog_user_description', 'activitypub_blog_description' );
299 self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
300 }
301
302 /**
303 * * Update actor-mode settings.
304 * * Get the ID of the latest blog post and save it to the options table.
305 */
306 private static function migrate_to_4_0_0() {
307 $latest_post_id = 0;
308
309 // Get the ID of the latest blog post and save it to the options table.
310 $latest_post = get_posts(
311 array(
312 'numberposts' => 1,
313 'orderby' => 'ID',
314 'order' => 'DESC',
315 'post_type' => 'any',
316 'post_status' => 'publish',
317 )
318 );
319
320 if ( $latest_post ) {
321 $latest_post_id = $latest_post[0]->ID;
322 }
323
324 \update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
325
326 $users = \get_users(
327 array(
328 'capability__in' => array( 'activitypub' ),
329 )
330 );
331
332 foreach ( $users as $user ) {
333 $followers = Followers::get_followers( $user->ID );
334
335 if ( $followers ) {
336 \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
337 }
338 }
339
340 $followers = Followers::get_followers( Actors::BLOG_USER_ID );
341
342 if ( $followers ) {
343 \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
344 }
345
346 self::migrate_actor_mode();
347 }
348
349 /**
350 * Upate to 4.1.0
351 *
352 * * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
353 */
354 public static function migrate_to_4_1_0() {
355 $content_type = \get_option( 'activitypub_post_content_type' );
356
357 switch ( $content_type ) {
358 case 'excerpt':
359 $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
360 break;
361 case 'title':
362 $template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]";
363 break;
364 case 'content':
365 $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
366 break;
367 case 'custom':
368 $template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
369 break;
370 default:
371 $template = ACTIVITYPUB_CUSTOM_POST_CONTENT;
372 break;
373 }
374
375 \update_option( 'activitypub_custom_post_content', $template );
376
377 \delete_option( 'activitypub_post_content_type' );
378
379 $object_type = \get_option( 'activitypub_object_type', false );
380 if ( ! $object_type ) {
381 \update_option( 'activitypub_object_type', 'note' );
382 }
383
384 // Clean up empty visibility meta.
385 global $wpdb;
386 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
387 "DELETE FROM $wpdb->postmeta
388 WHERE meta_key = 'activitypub_content_visibility'
389 AND (meta_value IS NULL OR meta_value = '')"
390 );
391 }
392
393 /**
394 * Updates post meta keys to be prefixed with an underscore.
395 */
396 public static function migrate_to_4_7_1() {
397 global $wpdb;
398
399 $meta_keys = array(
400 'activitypub_actor_json',
401 'activitypub_canonical_url',
402 'activitypub_errors',
403 'activitypub_inbox',
404 'activitypub_user_id',
405 );
406
407 foreach ( $meta_keys as $meta_key ) {
408 // phpcs:ignore WordPress.DB
409 $wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) );
410 }
411 }
412
413 /**
414 * Update comment counts for posts in batches.
415 *
416 * @see Comment::pre_wp_update_comment_count_now()
417 * @param int $batch_size Optional. Number of posts to process per batch. Default 100.
418 * @param int $offset Optional. Number of posts to skip. Default 0.
419 */
420 public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
421 global $wpdb;
422
423 // Bail if the existing lock is still valid.
424 if ( self::is_locked() ) {
425 \wp_schedule_single_event(
426 time() + ( 5 * MINUTE_IN_SECONDS ),
427 'activitypub_update_comment_counts',
428 array(
429 'batch_size' => $batch_size,
430 'offset' => $offset,
431 )
432 );
433 return;
434 }
435
436 self::lock();
437
438 Comment::register_comment_types();
439 $comment_types = Comment::get_comment_type_slugs();
440 $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
441
442 // Get and process this batch.
443 $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
444 $wpdb->prepare(
445 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
446 "SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
447 $batch_size,
448 $offset
449 )
450 );
451
452 foreach ( $post_ids as $post_id ) {
453 \wp_update_comment_count_now( $post_id );
454 }
455
456 if ( count( $post_ids ) === $batch_size ) {
457 // Schedule next batch.
458 \wp_schedule_single_event(
459 time() + MINUTE_IN_SECONDS,
460 'activitypub_update_comment_counts',
461 array(
462 'batch_size' => $batch_size,
463 'offset' => $offset + $batch_size,
464 )
465 );
466 }
467
468 self::unlock();
469 }
470
471 /**
472 * Set the defaults needed for the plugin to work.
473 *
474 * Add the ActivityPub capability to all users that can publish posts.
475 */
476 public static function add_default_settings() {
477 self::add_activitypub_capability();
478 self::add_notification_defaults();
479 }
480
481 /**
482 * Add the ActivityPub capability to all users that can publish posts.
483 */
484 private static function add_activitypub_capability() {
485 // Get all WP_User objects that can publish posts.
486 $users = \get_users(
487 array(
488 'capability__in' => array( 'publish_posts' ),
489 )
490 );
491
492 // Add ActivityPub capability to all users that can publish posts.
493 foreach ( $users as $user ) {
494 $user->add_cap( 'activitypub' );
495 }
496 }
497
498 /**
499 * Add default notification settings.
500 */
501 private static function add_notification_defaults() {
502 \add_option( 'activitypub_mailer_new_follower', '1' );
503 \add_option( 'activitypub_mailer_new_dm', '1' );
504 }
505
506 /**
507 * Rename meta keys.
508 *
509 * @param string $old_key The old comment meta key.
510 * @param string $new_key The new comment meta key.
511 */
512 private static function update_usermeta_key( $old_key, $new_key ) {
513 global $wpdb;
514
515 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
516 $wpdb->usermeta,
517 array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
518 array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
519 array( '%s' ),
520 array( '%s' )
521 );
522 }
523
524 /**
525 * Rename option keys.
526 *
527 * @param string $old_key The old option key.
528 * @param string $new_key The new option key.
529 */
530 private static function update_options_key( $old_key, $new_key ) {
531 global $wpdb;
532
533 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
534 $wpdb->options,
535 array( 'option_name' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
536 array( 'option_name' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
537 array( '%s' ),
538 array( '%s' )
539 );
540 }
541
542 /**
543 * Migrate the actor mode settings.
544 */
545 public static function migrate_actor_mode() {
546 $blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
547 $author_profiles = \get_option( 'activitypub_enable_users', '1' );
548
549 if (
550 '1' === $blog_profile &&
551 '1' === $author_profiles
552 ) {
553 \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
554 } elseif (
555 '1' === $blog_profile &&
556 '1' !== $author_profiles
557 ) {
558 \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
559 } elseif (
560 '1' !== $blog_profile &&
561 '1' === $author_profiles
562 ) {
563 \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
564 }
565 }
566 }
567