1334 lines
41 KiB
PHP
1334 lines
41 KiB
PHP
<?php
|
|
/**
|
|
* Migration class file.
|
|
*
|
|
* @package Activitypub
|
|
*/
|
|
|
|
namespace Activitypub;
|
|
|
|
use Activitypub\Collection\Actors;
|
|
use Activitypub\Collection\Extra_Fields;
|
|
use Activitypub\Collection\Followers;
|
|
use Activitypub\Collection\Following;
|
|
use Activitypub\Collection\Outbox;
|
|
use Activitypub\Collection\Remote_Actors;
|
|
use Activitypub\Transformer\Factory;
|
|
|
|
/**
|
|
* ActivityPub Migration Class
|
|
*
|
|
* @author Matthias Pfefferle
|
|
*/
|
|
class Migration {
|
|
/**
|
|
* Initialize the class, registering WordPress hooks.
|
|
*/
|
|
public static function init() {
|
|
self::maybe_migrate();
|
|
|
|
Scheduler::register_async_batch_callback( 'activitypub_migrate_from_0_17', array( self::class, 'migrate_from_0_17' ) );
|
|
Scheduler::register_async_batch_callback( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ) );
|
|
Scheduler::register_async_batch_callback( 'activitypub_create_post_outbox_items', array( self::class, 'create_post_outbox_items' ) );
|
|
Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) );
|
|
Scheduler::register_async_batch_callback( 'activitypub_migrate_avatar_to_remote_actors', array( self::class, 'migrate_avatar_to_remote_actors' ) );
|
|
Scheduler::register_async_batch_callback( 'activitypub_migrate_actor_emoji', array( self::class, 'migrate_actor_emoji' ) );
|
|
Scheduler::register_async_batch_callback( 'activitypub_backfill_statistics', array( Statistics::class, 'backfill_historical_stats' ) );
|
|
Scheduler::register_async_batch_callback( 'activitypub_tombstone_migrate', array( self::class, 'migrate_tombstones_to_cpt' ) );
|
|
}
|
|
|
|
/**
|
|
* The current version of the database structure.
|
|
*
|
|
* @return string The current version.
|
|
*/
|
|
public static function get_version() {
|
|
return get_option( 'activitypub_db_version', 0 );
|
|
}
|
|
|
|
/**
|
|
* Locks the database migration process to prevent simultaneous migrations.
|
|
*
|
|
* @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
|
|
*/
|
|
public static function lock() {
|
|
global $wpdb;
|
|
|
|
// Try to lock.
|
|
$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
|
|
|
|
if ( ! $lock_result ) {
|
|
$lock_result = \get_option( 'activitypub_migration_lock' );
|
|
}
|
|
|
|
return $lock_result;
|
|
}
|
|
|
|
/**
|
|
* Unlocks the database migration process.
|
|
*/
|
|
public static function unlock() {
|
|
\delete_option( 'activitypub_migration_lock' );
|
|
}
|
|
|
|
/**
|
|
* Whether the database migration process is locked.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public static function is_locked() {
|
|
$lock = \get_option( 'activitypub_migration_lock' );
|
|
|
|
if ( ! $lock ) {
|
|
return false;
|
|
}
|
|
|
|
$lock = (int) $lock;
|
|
|
|
if ( $lock < \time() - 1800 ) {
|
|
self::unlock();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Whether the database structure is up to date.
|
|
*
|
|
* @return bool True if the database structure is up to date, false otherwise.
|
|
*/
|
|
public static function is_latest_version() {
|
|
return (bool) \version_compare(
|
|
self::get_version(),
|
|
ACTIVITYPUB_PLUGIN_VERSION,
|
|
'=='
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates the database structure if necessary.
|
|
*/
|
|
public static function maybe_migrate() {
|
|
if ( self::is_latest_version() ) {
|
|
return;
|
|
}
|
|
|
|
if ( self::is_locked() ) {
|
|
return;
|
|
}
|
|
|
|
self::lock();
|
|
|
|
$version_from_db = self::get_version();
|
|
|
|
// Check for initial migration.
|
|
if ( ! $version_from_db ) {
|
|
self::add_default_settings();
|
|
$version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
|
|
}
|
|
|
|
if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
|
|
self::migrate_from_0_16();
|
|
}
|
|
if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
|
\wp_schedule_single_event( \time(), 'activitypub_migrate_from_0_17' );
|
|
}
|
|
if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
|
self::migrate_from_1_2_0();
|
|
}
|
|
if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
|
|
self::migrate_from_2_0_0();
|
|
}
|
|
if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) {
|
|
self::migrate_from_2_2_0();
|
|
}
|
|
if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) {
|
|
self::migrate_from_2_6_0();
|
|
}
|
|
if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) {
|
|
self::migrate_to_4_0_0();
|
|
}
|
|
if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
|
|
self::migrate_to_4_1_0();
|
|
}
|
|
if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
|
|
\wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
|
|
}
|
|
if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) {
|
|
self::migrate_to_4_7_1();
|
|
}
|
|
if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
|
|
self::migrate_to_4_7_2();
|
|
}
|
|
if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
|
|
Scheduler::register_schedules();
|
|
\wp_schedule_single_event( \time(), 'activitypub_create_post_outbox_items' );
|
|
\wp_schedule_single_event( \time() + 15, 'activitypub_create_comment_outbox_items' );
|
|
}
|
|
if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
|
|
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
|
|
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
|
|
}
|
|
if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
|
|
self::delete_mastodon_api_orphaned_extra_fields();
|
|
}
|
|
if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) {
|
|
self::update_notification_options();
|
|
}
|
|
if ( \version_compare( $version_from_db, '6.0.0', '<' ) ) {
|
|
self::migrate_followers_to_ap_actor_cpt();
|
|
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
|
|
}
|
|
if ( \version_compare( $version_from_db, '6.0.1', '<' ) ) {
|
|
self::migrate_followers_to_ap_actor_cpt();
|
|
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
|
|
}
|
|
if ( \version_compare( $version_from_db, '7.0.0', '<' ) ) {
|
|
wp_unschedule_hook( 'activitypub_update_followers' );
|
|
wp_unschedule_hook( 'activitypub_cleanup_followers' );
|
|
|
|
if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) {
|
|
\wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' );
|
|
}
|
|
|
|
if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) {
|
|
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' );
|
|
}
|
|
}
|
|
if ( \version_compare( $version_from_db, '7.3.0', '<' ) ) {
|
|
self::remove_pending_application_user_follow_requests();
|
|
}
|
|
if ( \version_compare( $version_from_db, '7.5.0', '<' ) ) {
|
|
self::sync_jetpack_following_meta();
|
|
}
|
|
if ( \version_compare( $version_from_db, '7.6.0', '<' ) ) {
|
|
self::clean_up_inbox();
|
|
\wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' );
|
|
}
|
|
if ( \version_compare( $version_from_db, '7.9.0', '<' ) ) {
|
|
\wp_schedule_single_event( \time(), 'activitypub_migrate_actor_emoji' );
|
|
}
|
|
if ( \version_compare( $version_from_db, '8.1.0', '<' ) && ! \wp_next_scheduled( 'activitypub_backfill_statistics' ) ) {
|
|
// Backfill historical statistics data (delay + jitter to avoid load spikes on hosts running many sites).
|
|
\wp_schedule_single_event( \time() + HOUR_IN_SECONDS + \wp_rand( 0, 6 * HOUR_IN_SECONDS ), 'activitypub_backfill_statistics' );
|
|
}
|
|
if ( \version_compare( $version_from_db, '8.3.0', '<' ) ) {
|
|
if ( ! \wp_next_scheduled( 'activitypub_tombstone_migrate' ) ) {
|
|
\wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_tombstone_migrate' );
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Defer the flush to late in the `init` cycle (priority 20). Migration::init
|
|
* runs at priority 1, which is earlier than most plugins register their
|
|
* rewrite rules. Flushing synchronously here would persist a truncated
|
|
* ruleset that omits third-party rules added on `init` at priority 10.
|
|
*/
|
|
\add_action( 'init', array( Activitypub::class, 'flush_rewrite_rules' ), 20 );
|
|
|
|
// Ensure all required cron schedules are registered.
|
|
Scheduler::register_schedules();
|
|
|
|
/*
|
|
* Add new update routines above this comment. ^
|
|
*
|
|
* Use 'unreleased' as the version number for new migrations and add tests for the callback directly.
|
|
* The release script will automatically replace it with the actual version number.
|
|
* Example:
|
|
*
|
|
* if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
|
|
* // Update routine.
|
|
* }
|
|
*/
|
|
|
|
/**
|
|
* Fires when the system has to be migrated.
|
|
*
|
|
* @param string $version_from_db The version from which to migrate.
|
|
* @param string $target_version The target version to migrate to.
|
|
*/
|
|
\do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION );
|
|
|
|
\update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION );
|
|
|
|
self::unlock();
|
|
}
|
|
|
|
/**
|
|
* Updates the custom template to use shortcodes instead of the deprecated templates.
|
|
*/
|
|
private static function migrate_from_0_16() {
|
|
// Get the custom template.
|
|
$old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
|
|
|
/*
|
|
* If the old content exists but is a blank string, we're going to need a flag to updated it even
|
|
* after setting it to the default contents.
|
|
*/
|
|
$need_update = false;
|
|
|
|
// If the old contents is blank, use the defaults.
|
|
if ( '' === $old_content ) {
|
|
$old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
|
$need_update = true;
|
|
}
|
|
|
|
// Set the new content to be the old content.
|
|
$content = $old_content;
|
|
|
|
// Convert old templates to shortcodes.
|
|
$content = \str_replace( '%title%', '[ap_title]', $content );
|
|
$content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
|
|
$content = \str_replace( '%content%', '[ap_content]', $content );
|
|
$content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
|
|
$content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
|
|
$content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
|
|
$content = \str_replace( '%tags%', '[ap_hashtags]', $content );
|
|
|
|
// Store the new template if required.
|
|
if ( $content !== $old_content || $need_update ) {
|
|
\update_option( 'activitypub_custom_post_content', $content );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the DB-schema of the followers-list.
|
|
*/
|
|
public static function migrate_from_0_17() {
|
|
// Migrate followers.
|
|
foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
|
|
$followers = get_user_meta( $user_id, 'activitypub_followers', true );
|
|
|
|
if ( $followers ) {
|
|
foreach ( $followers as $actor ) {
|
|
Followers::add( $user_id, $actor );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the cache after updating to 1.3.0.
|
|
*/
|
|
private static function migrate_from_1_2_0() {
|
|
$user_ids = \get_users(
|
|
array(
|
|
'fields' => 'ID',
|
|
'capability__in' => array( 'publish_posts' ),
|
|
)
|
|
);
|
|
|
|
foreach ( $user_ids as $user_id ) {
|
|
wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unschedule Hooks after updating to 2.0.0.
|
|
*/
|
|
private static function migrate_from_2_0_0() {
|
|
wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
|
|
wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
|
|
wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
|
|
|
|
wp_unschedule_hook( 'activitypub_send_post_activity' );
|
|
wp_unschedule_hook( 'activitypub_send_update_activity' );
|
|
wp_unschedule_hook( 'activitypub_send_delete_activity' );
|
|
|
|
$object_type = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
|
|
if ( 'article' === $object_type ) {
|
|
\update_option( 'activitypub_object_type', 'wordpress-post-format' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the ActivityPub capability to all users that can publish posts
|
|
* Delete old meta to store followers.
|
|
*/
|
|
private static function migrate_from_2_2_0() {
|
|
// Add the ActivityPub capability to all users that can publish posts.
|
|
self::add_activitypub_capability();
|
|
}
|
|
|
|
/**
|
|
* Rename DB fields.
|
|
*/
|
|
private static function migrate_from_2_6_0() {
|
|
wp_cache_flush();
|
|
|
|
self::update_usermeta_key( 'activitypub_user_description', 'activitypub_description' );
|
|
|
|
self::update_options_key( 'activitypub_blog_user_description', 'activitypub_blog_description' );
|
|
self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
|
|
}
|
|
|
|
/**
|
|
* * Update actor-mode settings.
|
|
* * Get the ID of the latest blog post and save it to the options table.
|
|
*/
|
|
private static function migrate_to_4_0_0() {
|
|
$latest_post_id = 0;
|
|
|
|
// Get the ID of the latest blog post and save it to the options table.
|
|
$latest_post = get_posts(
|
|
array(
|
|
'numberposts' => 1,
|
|
'orderby' => 'ID',
|
|
'order' => 'DESC',
|
|
'post_type' => 'any',
|
|
'post_status' => 'publish',
|
|
)
|
|
);
|
|
|
|
if ( $latest_post ) {
|
|
$latest_post_id = $latest_post[0]->ID;
|
|
}
|
|
|
|
\update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
|
|
|
|
$users = \get_users(
|
|
array(
|
|
'capability__in' => array( 'activitypub' ),
|
|
)
|
|
);
|
|
|
|
foreach ( $users as $user ) {
|
|
$followers = Followers::get_many( $user->ID );
|
|
|
|
if ( $followers ) {
|
|
\update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
|
|
}
|
|
}
|
|
|
|
$followers = Followers::get_many( Actors::BLOG_USER_ID );
|
|
|
|
if ( $followers ) {
|
|
\update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
|
|
}
|
|
|
|
self::migrate_actor_mode();
|
|
}
|
|
|
|
/**
|
|
* Update to 4.1.0
|
|
*
|
|
* * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
|
|
*/
|
|
public static function migrate_to_4_1_0() {
|
|
$content_type = \get_option( 'activitypub_post_content_type' );
|
|
|
|
switch ( $content_type ) {
|
|
case 'excerpt':
|
|
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
|
|
break;
|
|
case 'title':
|
|
$template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]";
|
|
break;
|
|
case 'content':
|
|
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
|
|
break;
|
|
case 'custom':
|
|
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
|
break;
|
|
default:
|
|
$template = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
|
break;
|
|
}
|
|
|
|
\update_option( 'activitypub_custom_post_content', $template );
|
|
|
|
\delete_option( 'activitypub_post_content_type' );
|
|
|
|
$object_type = \get_option( 'activitypub_object_type', false );
|
|
if ( ! $object_type ) {
|
|
\update_option( 'activitypub_object_type', 'note' );
|
|
}
|
|
|
|
// Clean up empty visibility meta.
|
|
global $wpdb;
|
|
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
"DELETE FROM $wpdb->postmeta
|
|
WHERE meta_key = 'activitypub_content_visibility'
|
|
AND (meta_value IS NULL OR meta_value = '')"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Updates post meta keys to be prefixed with an underscore.
|
|
*/
|
|
public static function migrate_to_4_7_1() {
|
|
global $wpdb;
|
|
|
|
$meta_keys = array(
|
|
'activitypub_actor_json',
|
|
'activitypub_canonical_url',
|
|
'activitypub_errors',
|
|
'activitypub_inbox',
|
|
'activitypub_user_id',
|
|
);
|
|
|
|
foreach ( $meta_keys as $meta_key ) {
|
|
// phpcs:ignore WordPress.DB
|
|
$wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the post cache for Followers, we should have done this in 4.7.1 when we renamed those keys.
|
|
*/
|
|
public static function migrate_to_4_7_2() {
|
|
global $wpdb;
|
|
// phpcs:ignore WordPress.DB
|
|
$followers = $wpdb->get_col(
|
|
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Remote_Actors::POST_TYPE )
|
|
);
|
|
foreach ( $followers as $id ) {
|
|
clean_post_cache( $id );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update comment counts for posts in batches.
|
|
*
|
|
* @see Comment::pre_wp_update_comment_count_now()
|
|
* @param int $batch_size Optional. Number of posts to process per batch. Default 100.
|
|
* @param int $offset Optional. Number of posts to skip. Default 0.
|
|
*
|
|
* @return int[]|void Array with batch size and offset if there are more posts to process.
|
|
*/
|
|
public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
|
|
global $wpdb;
|
|
|
|
Comment::register_comment_types();
|
|
$comment_types = Comment::get_comment_type_slugs();
|
|
$type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
|
|
|
|
// Get and process this batch.
|
|
$post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
|
|
$wpdb->prepare(
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
"SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
|
|
$batch_size,
|
|
$offset
|
|
)
|
|
);
|
|
|
|
foreach ( $post_ids as $post_id ) {
|
|
\wp_update_comment_count_now( $post_id );
|
|
}
|
|
|
|
if ( count( $post_ids ) === $batch_size ) {
|
|
// Schedule next batch.
|
|
return array( $batch_size, $offset + $batch_size );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create outbox items for posts in batches.
|
|
*
|
|
* @param int $batch_size Optional. Number of posts to process per batch. Default 50.
|
|
* @param int $offset Optional. Number of posts to skip. Default 0.
|
|
* @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
|
|
*/
|
|
public static function create_post_outbox_items( $batch_size = 50, $offset = 0 ) {
|
|
$posts = \get_posts(
|
|
array(
|
|
// our own `ap_outbox` will be excluded from `any` by virtue of its `exclude_from_search` arg.
|
|
'post_type' => 'any',
|
|
'posts_per_page' => $batch_size,
|
|
'offset' => $offset,
|
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
|
'meta_query' => array(
|
|
array(
|
|
'key' => 'activitypub_status',
|
|
'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
// Avoid multiple queries for post meta.
|
|
\update_postmeta_cache( \wp_list_pluck( $posts, 'ID' ) );
|
|
|
|
foreach ( $posts as $post ) {
|
|
$visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
|
|
|
|
self::add_to_outbox( $post, 'Create', $post->post_author, $visibility );
|
|
|
|
// Add Update activity when the post has been modified.
|
|
if ( $post->post_modified !== $post->post_date ) {
|
|
self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
|
|
}
|
|
}
|
|
|
|
if ( count( $posts ) === $batch_size ) {
|
|
return array(
|
|
'batch_size' => $batch_size,
|
|
'offset' => $offset + $batch_size,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create outbox items for comments in batches.
|
|
*
|
|
* @param int $batch_size Optional. Number of posts to process per batch. Default 50.
|
|
* @param int $offset Optional. Number of posts to skip. Default 0.
|
|
* @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
|
|
*/
|
|
public static function create_comment_outbox_items( $batch_size = 50, $offset = 0 ) {
|
|
$comments = \get_comments(
|
|
array(
|
|
'author__not_in' => array( 0 ), // Limit to comments by registered users.
|
|
'number' => $batch_size,
|
|
'offset' => $offset,
|
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
|
'meta_query' => array(
|
|
array(
|
|
'key' => 'activitypub_status',
|
|
'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
foreach ( $comments as $comment ) {
|
|
self::add_to_outbox( $comment, 'Create', $comment->user_id );
|
|
}
|
|
|
|
if ( count( $comments ) === $batch_size ) {
|
|
return array(
|
|
'batch_size' => $batch_size,
|
|
'offset' => $offset + $batch_size,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Update _activitypub_actor_json meta values to ensure they are properly slashed.
|
|
*
|
|
* @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
|
|
* @param int $offset Optional. Number of meta values to skip. Default 0.
|
|
* @return array|null Array with batch size and offset if there are more meta values to process, null otherwise.
|
|
*/
|
|
public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$meta_values = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d",
|
|
$batch_size,
|
|
$offset
|
|
)
|
|
);
|
|
|
|
foreach ( $meta_values as $meta ) {
|
|
$json = \json_decode( $meta->meta_value, true );
|
|
|
|
// If json_decode fails, try adding slashes.
|
|
if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) {
|
|
$escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value );
|
|
$json = \json_decode( $escaped_value, true );
|
|
|
|
// Update the meta if json_decode succeeds with slashes.
|
|
if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) {
|
|
\update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( \count( $meta_values ) === $batch_size ) {
|
|
return array(
|
|
'batch_size' => $batch_size,
|
|
'offset' => $offset + $batch_size,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Update comment author emails with webfinger addresses for ActivityPub comments.
|
|
*
|
|
* @param int $batch_size Optional. Number of comments to process per batch. Default 50.
|
|
* @param int $offset Optional. Number of comments to skip. Default 0.
|
|
* @return array|null Array with batch size and offset if there are more comments to process, null otherwise.
|
|
*/
|
|
public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) {
|
|
$comments = \get_comments(
|
|
array(
|
|
'number' => $batch_size,
|
|
'offset' => $offset,
|
|
'orderby' => 'comment_ID',
|
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
|
'meta_query' => array(
|
|
array(
|
|
'key' => 'protocol',
|
|
'value' => 'activitypub',
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
foreach ( $comments as $comment ) {
|
|
$comment_author_url = $comment->comment_author_url;
|
|
if ( empty( $comment_author_url ) ) {
|
|
continue;
|
|
}
|
|
|
|
$webfinger = Webfinger::uri_to_acct( $comment_author_url );
|
|
if ( \is_wp_error( $webfinger ) ) {
|
|
continue;
|
|
}
|
|
|
|
\wp_update_comment(
|
|
array(
|
|
'comment_ID' => $comment->comment_ID,
|
|
'comment_author_email' => \str_replace( 'acct:', '', $webfinger ),
|
|
)
|
|
);
|
|
}
|
|
|
|
if ( count( $comments ) === $batch_size ) {
|
|
return array(
|
|
'batch_size' => $batch_size,
|
|
'offset' => $offset + $batch_size,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Set the defaults needed for the plugin to work.
|
|
*
|
|
* Add the ActivityPub capability to all users that can publish posts.
|
|
*/
|
|
public static function add_default_settings() {
|
|
self::add_activitypub_capability();
|
|
self::add_default_extra_field();
|
|
}
|
|
|
|
/**
|
|
* Add an activity to the outbox without federating it.
|
|
*
|
|
* @param \WP_Post|\WP_Comment $comment The comment or post object.
|
|
* @param string $activity_type The type of activity.
|
|
* @param int $user_id The user ID.
|
|
* @param string $visibility Optional. The visibility of the content. Default 'public'.
|
|
*/
|
|
private static function add_to_outbox( $comment, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
|
|
$transformer = Factory::get_transformer( $comment );
|
|
if ( ! $transformer || \is_wp_error( $transformer ) ) {
|
|
return;
|
|
}
|
|
|
|
$activity = $transformer->to_activity( $activity_type );
|
|
if ( ! $activity || \is_wp_error( $activity ) ) {
|
|
return;
|
|
}
|
|
|
|
// If the user is disabled, fall back to the blog user when available.
|
|
if ( ! user_can_activitypub( $user_id ) ) {
|
|
if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
|
|
$user_id = Actors::BLOG_USER_ID;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$post_id = Outbox::add( $activity, $user_id, $visibility );
|
|
|
|
// Immediately set to publish, no federation needed.
|
|
\wp_publish_post( $post_id );
|
|
}
|
|
|
|
/**
|
|
* Add the ActivityPub capability to all users that can publish posts.
|
|
*/
|
|
private static function add_activitypub_capability() {
|
|
// Get all WP_User objects that can publish posts.
|
|
$users = \get_users(
|
|
array(
|
|
'capability__in' => array( 'publish_posts' ),
|
|
)
|
|
);
|
|
|
|
// Add ActivityPub capability to all users that can publish posts.
|
|
foreach ( $users as $user ) {
|
|
$user->add_cap( 'activitypub' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a default extra field for the user.
|
|
*/
|
|
private static function add_default_extra_field() {
|
|
$users = \get_users(
|
|
array(
|
|
'capability__in' => array( 'activitypub' ),
|
|
)
|
|
);
|
|
|
|
$title = \__( 'Powered by', 'activitypub' );
|
|
$content = 'WordPress';
|
|
|
|
// Add a default extra field for each user.
|
|
foreach ( $users as $user ) {
|
|
\wp_insert_post(
|
|
array(
|
|
'post_type' => Extra_Fields::USER_POST_TYPE,
|
|
'post_author' => $user->ID,
|
|
'post_status' => 'publish',
|
|
'post_title' => $title,
|
|
'post_content' => $content,
|
|
)
|
|
);
|
|
}
|
|
|
|
\wp_insert_post(
|
|
array(
|
|
'post_type' => Extra_Fields::BLOG_POST_TYPE,
|
|
'post_author' => 0,
|
|
'post_status' => 'publish',
|
|
'post_title' => $title,
|
|
'post_content' => $content,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rename user meta keys.
|
|
*
|
|
* @param string $old_key The old comment meta key.
|
|
* @param string $new_key The new comment meta key.
|
|
*/
|
|
private static function update_usermeta_key( $old_key, $new_key ) {
|
|
global $wpdb;
|
|
|
|
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->usermeta,
|
|
array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
array( '%s' ),
|
|
array( '%s' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update post meta keys.
|
|
*
|
|
* @param string $old_key The old post meta key.
|
|
* @param string $new_key The new post meta key.
|
|
*/
|
|
private static function update_postmeta_key( $old_key, $new_key ) {
|
|
global $wpdb;
|
|
|
|
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->postmeta,
|
|
array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
array( '%s' ),
|
|
array( '%s' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Rename option keys.
|
|
*
|
|
* @param string $old_key The old option key.
|
|
* @param string $new_key The new option key.
|
|
*/
|
|
private static function update_options_key( $old_key, $new_key ) {
|
|
global $wpdb;
|
|
|
|
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->options,
|
|
array( 'option_name' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
array( 'option_name' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
array( '%s' ),
|
|
array( '%s' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Migrate the actor mode settings.
|
|
*/
|
|
public static function migrate_actor_mode() {
|
|
$blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
|
|
$author_profiles = \get_option( 'activitypub_enable_users', '1' );
|
|
|
|
if (
|
|
'1' === $blog_profile &&
|
|
'1' === $author_profiles
|
|
) {
|
|
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
|
|
} elseif (
|
|
'1' === $blog_profile &&
|
|
'1' !== $author_profiles
|
|
) {
|
|
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
|
|
} elseif (
|
|
'1' !== $blog_profile &&
|
|
'1' === $author_profiles
|
|
) {
|
|
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes user extra fields where the author is the blog user.
|
|
*
|
|
* These extra fields were created when the Enable Mastodon Apps integration passed
|
|
* an author_url instead of a user_id to the mastodon_api_account filter. This caused
|
|
* Extra_Fields::default_actor_extra_fields() to run but fail to cache the fact it ran
|
|
* for non-existent users. The result is a number of user extra fields with no author.
|
|
*
|
|
* @ticket https://github.com/Automattic/wordpress-activitypub/pull/1554
|
|
*/
|
|
public static function delete_mastodon_api_orphaned_extra_fields() {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->delete(
|
|
$wpdb->posts,
|
|
array(
|
|
'post_type' => Extra_Fields::USER_POST_TYPE,
|
|
'post_author' => Actors::BLOG_USER_ID,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Update notification options.
|
|
*/
|
|
public static function update_notification_options() {
|
|
$new_dm = \get_option( 'activitypub_mailer_new_dm', '1' );
|
|
$new_follower = \get_option( 'activitypub_mailer_new_follower', '1' );
|
|
|
|
// Add the blog user notification options.
|
|
\add_option( 'activitypub_blog_user_mailer_new_dm', $new_dm );
|
|
\add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower );
|
|
\add_option( 'activitypub_blog_user_mailer_new_mention', '1' );
|
|
|
|
$user_ids = \get_users(
|
|
array(
|
|
'capability__in' => array( 'activitypub' ),
|
|
'fields' => 'id',
|
|
)
|
|
);
|
|
|
|
// Add the actor notification options.
|
|
foreach ( $user_ids as $user_id ) {
|
|
\update_user_option( $user_id, 'activitypub_mailer_new_dm', $new_dm );
|
|
\update_user_option( $user_id, 'activitypub_mailer_new_follower', $new_follower );
|
|
\update_user_option( $user_id, 'activitypub_mailer_new_mention', '1' );
|
|
}
|
|
|
|
// Delete the old notification options.
|
|
\delete_option( 'activitypub_mailer_new_dm' );
|
|
\delete_option( 'activitypub_mailer_new_follower' );
|
|
}
|
|
|
|
/**
|
|
* Migrate followers to the new CPT.
|
|
*/
|
|
public static function migrate_followers_to_ap_actor_cpt() {
|
|
global $wpdb;
|
|
|
|
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->posts,
|
|
array( 'post_type' => Remote_Actors::POST_TYPE ),
|
|
array( 'post_type' => 'ap_follower' ),
|
|
array( '%s' ),
|
|
array( '%s' )
|
|
);
|
|
|
|
self::update_postmeta_key( '_activitypub_user_id', Followers::FOLLOWER_META_KEY );
|
|
}
|
|
|
|
/**
|
|
* Update _activitypub_actor_json meta values to ensure they are properly slashed.
|
|
*
|
|
* @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
|
|
*
|
|
* @return array|void Array with batch size and offset if there are more meta values to process, void otherwise.
|
|
*/
|
|
public static function update_actor_json_storage( $batch_size = 100 ) {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$meta_values = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d",
|
|
$batch_size
|
|
)
|
|
);
|
|
|
|
$has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
|
|
if ( $has_kses ) {
|
|
// Prevent KSES from corrupting JSON in post_content.
|
|
\kses_remove_filters();
|
|
}
|
|
|
|
foreach ( $meta_values as $meta ) {
|
|
$post = \get_post( $meta->post_id );
|
|
|
|
if ( ! $post ) {
|
|
\delete_post_meta( $meta->post_id, '_activitypub_actor_json' );
|
|
continue;
|
|
}
|
|
|
|
$post_content = \json_decode( $meta->meta_value, true );
|
|
|
|
if ( \json_last_error() !== JSON_ERROR_NONE ) {
|
|
$post_content = Http::get_remote_object( $post->guid );
|
|
|
|
if ( \is_wp_error( $post_content ) ) {
|
|
\delete_post_meta( $post->ID, '_activitypub_actor_json' );
|
|
continue;
|
|
}
|
|
}
|
|
|
|
\wp_update_post(
|
|
array(
|
|
'ID' => $post->ID,
|
|
'post_content' => \wp_slash( \wp_json_encode( $post_content ) ),
|
|
)
|
|
);
|
|
|
|
\delete_post_meta( $post->ID, '_activitypub_actor_json' );
|
|
}
|
|
|
|
if ( $has_kses ) {
|
|
// Restore KSES filters.
|
|
\kses_init_filters();
|
|
}
|
|
|
|
if ( \count( $meta_values ) === $batch_size ) {
|
|
return array(
|
|
'batch_size' => $batch_size,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes pending follow requests for the application user.
|
|
*/
|
|
public static function remove_pending_application_user_follow_requests() {
|
|
global $wpdb;
|
|
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->delete(
|
|
$wpdb->postmeta,
|
|
array(
|
|
'meta_key' => '_activitypub_following', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
|
'meta_value' => Actors::APPLICATION_USER_ID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Sync Jetpack meta for all followings.
|
|
*
|
|
* Replays the added_post_meta sync action for Jetpack with the Following::FOLLOWING_META_KEY meta key.
|
|
*/
|
|
public static function sync_jetpack_following_meta() {
|
|
if ( ! \class_exists( 'Jetpack' ) || ! \Jetpack::is_connection_ready() ) {
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
|
|
// Get all posts that have the following meta key.
|
|
$posts_with_following = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prepare(
|
|
"SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
|
|
Following::FOLLOWING_META_KEY
|
|
),
|
|
ARRAY_N
|
|
);
|
|
|
|
// Trigger the added_post_meta action for each following relationship.
|
|
foreach ( $posts_with_following as $meta ) {
|
|
/**
|
|
* Fires when post meta is added.
|
|
*
|
|
* @param int $meta_id ID of the metadata entry.
|
|
* @param int $object_id Post ID.
|
|
* @param string $meta_key Metadata key.
|
|
* @param mixed $meta_value Metadata value.
|
|
*/
|
|
\do_action( 'added_post_meta', ...$meta );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up inbox items for shared inbox migration.
|
|
*
|
|
* Deletes all existing inbox items to prepare for the new shared inbox structure
|
|
* where activities are stored once with multiple recipients as metadata.
|
|
*/
|
|
private static function clean_up_inbox() {
|
|
global $wpdb;
|
|
|
|
// Get all inbox post IDs.
|
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$inbox_ids = $wpdb->get_col(
|
|
$wpdb->prepare(
|
|
"SELECT ID FROM {$wpdb->posts} WHERE post_type = %s",
|
|
\Activitypub\Collection\Inbox::POST_TYPE
|
|
)
|
|
);
|
|
|
|
// Delete all inbox items and their metadata.
|
|
foreach ( $inbox_ids as $post_id ) {
|
|
\wp_delete_post( $post_id, true );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate URLs from the legacy `activitypub_tombstone_urls` option into the
|
|
* `ap_tombstone` custom post type.
|
|
*
|
|
* Chunked async migration. Locking and rescheduling is handled by
|
|
* Scheduler::async_batch — the callback returns `array( 'batch_size' => N )`
|
|
* to request another run, or `null` when the option is fully drained.
|
|
*
|
|
* Legacy entries are already-normalized strings (no scheme), so we bypass
|
|
* URL validation and insert directly via wp_insert_post.
|
|
*
|
|
* @since 8.3.0
|
|
*
|
|
* @param int $batch_size Optional. Number of URLs to process per call. Default 500.
|
|
* @return array|null Args for the next run, or null when migration is complete.
|
|
*/
|
|
public static function migrate_tombstones_to_cpt( $batch_size = 500 ) {
|
|
global $wpdb;
|
|
|
|
$urls = \get_option( 'activitypub_tombstone_urls', null );
|
|
|
|
if ( null === $urls || ! \is_array( $urls ) || empty( $urls ) ) {
|
|
\delete_option( 'activitypub_tombstone_urls' );
|
|
return null;
|
|
}
|
|
|
|
$chunk = \array_slice( $urls, 0, (int) $batch_size );
|
|
$remaining = \array_slice( $urls, (int) $batch_size );
|
|
$progressed = false;
|
|
|
|
foreach ( $chunk as $normalized ) {
|
|
if ( ! \is_string( $normalized ) || '' === $normalized ) {
|
|
// Drop garbage entries — counts as progress.
|
|
$progressed = true;
|
|
continue;
|
|
}
|
|
|
|
$hash = \md5( $normalized );
|
|
|
|
/*
|
|
* Light existence check. `get_page_by_path()` would hydrate a
|
|
* full `WP_Post` per loop iteration; on a large registry that
|
|
* adds up fast. We only need a boolean here.
|
|
*/
|
|
$exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prepare(
|
|
"SELECT 1 FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s LIMIT 1",
|
|
Tombstone::POST_TYPE,
|
|
$hash
|
|
)
|
|
);
|
|
if ( $exists ) {
|
|
$progressed = true;
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* `guid` is intentionally omitted: the legacy option only kept
|
|
* the normalized (schemeless) form, so we can't reconstruct the
|
|
* original URL. Storing the schemeless string would be mangled
|
|
* by `esc_url()`. Leave WordPress to auto-generate the guid
|
|
* — it's not used for lookups, only for debugging.
|
|
*/
|
|
$result = \wp_insert_post(
|
|
array(
|
|
'post_type' => Tombstone::POST_TYPE,
|
|
'post_status' => 'publish',
|
|
'post_name' => $hash,
|
|
'post_author' => 0,
|
|
),
|
|
true
|
|
);
|
|
|
|
if ( \is_wp_error( $result ) || ! $result ) {
|
|
/*
|
|
* Keep failed inserts in the legacy option so the next batch
|
|
* retries them. `Tombstone::exists_local()` still falls back
|
|
* to the option, so the tombstone remains discoverable.
|
|
*/
|
|
$remaining[] = $normalized;
|
|
} else {
|
|
$progressed = true;
|
|
}
|
|
}
|
|
|
|
if ( empty( $remaining ) ) {
|
|
\delete_option( 'activitypub_tombstone_urls' );
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Disable autoload while we drain. The point of the migration is to
|
|
* stop this option from contributing to `alloptions` pressure, so
|
|
* flip the flag immediately rather than waiting for the option to
|
|
* be fully empty before the relief kicks in.
|
|
*/
|
|
\update_option( 'activitypub_tombstone_urls', \array_values( $remaining ), false );
|
|
|
|
/*
|
|
* If nothing in this batch was drained — every insert errored and
|
|
* nothing was already migrated — halt the scheduler so we don't loop
|
|
* forever on a persistent failure. The legacy option still backs
|
|
* exists_local(), so the data isn't lost; an admin can re-trigger
|
|
* the migration via `wp cron event run activitypub_tombstone_migrate`
|
|
* after fixing the underlying cause.
|
|
*/
|
|
if ( ! $progressed ) {
|
|
return null;
|
|
}
|
|
|
|
return array( 'batch_size' => (int) $batch_size );
|
|
}
|
|
|
|
/**
|
|
* Migrate avatar URLs from comment meta to remote actors in batches.
|
|
*
|
|
* This migration:
|
|
* 1. Finds all comments with ActivityPub protocol and avatar_url meta
|
|
* 2. Looks up the remote actor by comment_author_url
|
|
* 3. Adds _activitypub_remote_actor_id to comment meta
|
|
* 4. Stores avatar_url in remote actor post meta
|
|
*
|
|
* Note: We don't use offset because as we add _activitypub_remote_actor_id,
|
|
* comments are filtered out of the query. We just keep fetching the next
|
|
* batch until no more comments match the criteria.
|
|
*
|
|
* @param int $batch_size Optional. Number of comments to process per batch. Default 50.
|
|
* @return array|null Array with batch size if there are more comments to process, null otherwise.
|
|
*/
|
|
public static function migrate_avatar_to_remote_actors( $batch_size = 50 ) {
|
|
global $wpdb;
|
|
|
|
/*
|
|
* Get comments with avatar_url meta that don't have _activitypub_remote_actor_id yet.
|
|
* Uses conditional aggregation to reduce JOINs from 3 to 1, improving query performance.
|
|
* Filters meta_key before GROUP BY to reduce rows processed during aggregation.
|
|
* No offset needed - as we process comments, they're filtered out by the HAVING clause.
|
|
*/
|
|
$comments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
$wpdb->prepare(
|
|
"SELECT c.comment_ID, c.comment_author_url,
|
|
MAX(CASE WHEN cm.meta_key = 'avatar_url' THEN cm.meta_value END) AS avatar_url,
|
|
MAX(CASE WHEN cm.meta_key = 'protocol' THEN cm.meta_value END) AS protocol,
|
|
MAX(CASE WHEN cm.meta_key = '_activitypub_remote_actor_id' THEN cm.meta_value END) AS remote_actor_id
|
|
FROM {$wpdb->comments} c
|
|
INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id
|
|
WHERE cm.meta_key IN ('avatar_url', 'protocol', '_activitypub_remote_actor_id')
|
|
GROUP BY c.comment_ID, c.comment_author_url
|
|
HAVING protocol = 'activitypub'
|
|
AND avatar_url IS NOT NULL
|
|
AND (remote_actor_id IS NULL OR remote_actor_id = '')
|
|
LIMIT %d",
|
|
$batch_size
|
|
)
|
|
);
|
|
|
|
foreach ( $comments as $comment ) {
|
|
if ( empty( $comment->comment_author_url ) ) {
|
|
continue;
|
|
}
|
|
|
|
// Try to get the remote actor by URI.
|
|
$remote_actor = Remote_Actors::fetch_by_uri( $comment->comment_author_url );
|
|
|
|
// If we have a valid remote actor, store the reference.
|
|
if ( ! \is_wp_error( $remote_actor ) ) {
|
|
// Add _activitypub_remote_actor_id to comment meta.
|
|
\add_comment_meta( $comment->comment_ID, '_activitypub_remote_actor_id', $remote_actor->ID, true );
|
|
|
|
// Ensure avatar is stored on remote actor if not already present.
|
|
$existing_avatar = \get_post_meta( $remote_actor->ID, '_activitypub_avatar_url', true );
|
|
if ( empty( $existing_avatar ) && ! empty( $comment->avatar_url ) ) {
|
|
\update_post_meta( $remote_actor->ID, '_activitypub_avatar_url', \esc_url_raw( $comment->avatar_url ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return batch info if there are more comments to process.
|
|
if ( count( $comments ) === $batch_size ) {
|
|
return array(
|
|
'batch_size' => $batch_size,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Migrate emoji data from stored actor JSON to post meta.
|
|
*
|
|
* This migration:
|
|
* 1. Finds all remote actor posts without _activitypub_emoji meta
|
|
* 2. Extracts emoji from stored JSON in post_content
|
|
* 3. Stores as _activitypub_emoji post meta
|
|
*
|
|
* @param int $batch_size Optional. Number of actors to process per batch. Default 50.
|
|
* @param int $offset Optional. Offset for pagination. Default 0.
|
|
* @return array|null Array with batch size if there are more actors to process, null otherwise.
|
|
*/
|
|
public static function migrate_actor_emoji( $batch_size = 50, $offset = 0 ) {
|
|
$actors = \get_posts(
|
|
array(
|
|
'post_type' => Remote_Actors::POST_TYPE,
|
|
'posts_per_page' => $batch_size,
|
|
'offset' => $offset,
|
|
'post_status' => 'any',
|
|
'orderby' => 'ID',
|
|
'order' => 'ASC',
|
|
)
|
|
);
|
|
|
|
foreach ( $actors as $actor_post ) {
|
|
if ( empty( $actor_post->post_content ) ) {
|
|
continue;
|
|
}
|
|
|
|
$actor_data = \json_decode( $actor_post->post_content, true );
|
|
if ( ! $actor_data ) {
|
|
continue;
|
|
}
|
|
|
|
$emoji_meta = Emoji::prepare_actor_meta( $actor_data );
|
|
if ( ! empty( $emoji_meta['_activitypub_emoji'] ) ) {
|
|
\update_post_meta( $actor_post->ID, '_activitypub_emoji', $emoji_meta['_activitypub_emoji'] );
|
|
}
|
|
}
|
|
|
|
// Return batch info if there are more actors to process.
|
|
if ( count( $actors ) === $batch_size ) {
|
|
return array(
|
|
'batch_size' => $batch_size,
|
|
'offset' => $offset + $batch_size,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|