diff --git a/activitypub.php b/activitypub.php index 6475042d..e4ac2960 100644 --- a/activitypub.php +++ b/activitypub.php @@ -21,6 +21,8 @@ use function Activitypub\site_supports_blocks; require_once __DIR__ . '/includes/compat.php'; require_once __DIR__ . '/includes/functions.php'; +\define( 'ACTIVITYPUB_PLUGIN_VERSION', '2.2.0' ); + /** * Initialize the plugin constants. */ @@ -101,6 +103,7 @@ function plugin_init() { } \add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' ); + /** * Class Autoloader */ @@ -218,6 +221,10 @@ function get_plugin_meta( $default_headers = array() ) { * Plugin Version Number used for caching. */ function get_plugin_version() { + if ( \defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) ) { + return ACTIVITYPUB_PLUGIN_VERSION; + } + $meta = get_plugin_meta( array( 'Version' => 'Version' ) ); return $meta['Version']; diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 6278c669..16387154 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -44,9 +44,6 @@ class Activity_Dispatcher { * @return void */ public static function send_activity_or_announce( $wp_object, $type ) { - // check if a migration is needed before sending new posts - Migration::maybe_migrate(); - if ( is_user_type_disabled( 'blog' ) ) { return; } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 45e6503a..442cdfa5 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -40,6 +40,8 @@ class Activitypub { \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 ); \add_action( 'init', array( self::class, 'theme_compat' ), 11 ); + \add_action( 'user_register', array( self::class, 'user_register' ) ); + \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) ); // register several post_types @@ -455,4 +457,17 @@ class Activitypub { \do_action( 'activitypub_after_register_post_type' ); } + + /** + * Add the 'activitypub' query variable so WordPress won't mangle it. + * + * @param int $user_id User ID. + * @param array $userdata The raw array of data passed to wp_insert_user(). + */ + public static function user_register( $user_id ) { + if ( \user_can( $user_id, 'publish_posts' ) ) { + $user = \get_user_by( 'id', $user_id ); + $user->add_cap( 'activitypub' ); + } + } } diff --git a/includes/class-admin.php b/includes/class-admin.php index 32617556..62f04469 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -26,6 +26,11 @@ class Admin { \add_action( 'admin_notices', array( self::class, 'admin_notices' ) ); \add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 ); + \add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ), 10, 1 ); + \add_filter( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 ); + \add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) ); + \add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 ); + if ( ! is_user_disabled( get_current_user_id() ) ) { \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); } @@ -345,4 +350,88 @@ class Admin { return $actions; } + + /** + * Add a column "activitypub" + * + * This column shows if the user has the capability to use ActivityPub. + * + * @param array $columns The columns. + * + * @return array The columns extended by the activitypub. + */ + public static function manage_users_columns( $columns ) { + $columns['activitypub'] = __( 'ActivityPub Support', 'activitypub' ); + return $columns; + } + + /** + * Return the results for the activitypub column. + * + * @param string $output Custom column output. Default empty. + * @param string $column_name Column name. + * @param int $user_id ID of the currently-listed user. + * + * @return string The column contents. + */ + public static function manage_users_custom_column( $output, $column_name, $user_id ) { + if ( 'activitypub' !== $column_name ) { + return $output; + } + + if ( \user_can( $user_id, 'activitypub' ) ) { + return '✓'; + } else { + return '✗'; + } + } + + /** + * Add options to the Bulk dropdown on the users page + * + * @param array $actions The existing bulk options. + * + * @return array The extended bulk options. + */ + public static function user_bulk_options( $actions ) { + $actions['add_activitypub_cap'] = __( 'Enable for ActivityPub', 'activitypub' ); + $actions['remove_activitypub_cap'] = __( 'Disable for ActivityPub', 'activitypub' ); + + return $actions; + } + + /** + * Handle bulk activitypub requests + * + * * `add_activitypub_cap` - Add the activitypub capability to the selected users. + * * `remove_activitypub_cap` - Remove the activitypub capability from the selected users. + * + * @param string $sendback The URL to send the user back to. + * @param string $action The requested action. + * @param array $users The selected users. + * + * @return string The URL to send the user back to. + */ + public static function handle_bulk_request( $sendback, $action, $users ) { + if ( + 'remove_activitypub_cap' !== $action && + 'add_activitypub_cap' !== $action + ) { + return $sendback; + } + + foreach ( $users as $user_id ) { + $user = new \WP_User( $user_id ); + if ( + 'add_activitypub_cap' === $action && + user_can( $user_id, 'publish_posts' ) + ) { + $user->add_cap( 'activitypub' ); + } elseif ( 'remove_activitypub_cap' === $action ) { + $user->remove_cap( 'activitypub' ); + } + } + + return $sendback; + } } diff --git a/includes/class-migration.php b/includes/class-migration.php index 219c0d83..52624c46 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -15,7 +15,9 @@ class Migration { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); + \add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) ); + + self::maybe_migrate(); } /** @@ -108,18 +110,28 @@ class Migration { $version_from_db = self::get_version(); + // check for inital migration + if ( ! $version_from_db ) { + self::add_default_settings(); + $version_from_db = self::get_target_version(); + } + + // schedule the async migration + if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) { + \wp_schedule_single_event( \time(), 'activitypub_migrate', $version_from_db ); + } if ( version_compare( $version_from_db, '0.17.0', '<' ) ) { self::migrate_from_0_16(); } - if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { - self::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_1_0(); + } update_option( 'activitypub_db_version', self::get_target_version() ); @@ -127,23 +139,14 @@ class Migration { } /** - * Updates the DB-schema of the followers-list + * Asynchronously migrates the database structure. * - * @return void + * @param string $version_from_db The version from which to migrate. */ - private 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_follower( $user_id, $actor ); - } - } + public static function async_migration( $version_from_db ) { + if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { + self::migrate_from_0_17(); } - - Activitypub::flush_rewrite_rules(); } /** @@ -183,13 +186,33 @@ class Migration { } } + /** + * Updates the DB-schema of the followers-list + * + * @return void + */ + 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_follower( $user_id, $actor ); + } + } + } + + Activitypub::flush_rewrite_rules(); + } + /** * Clear the cache after updating to 1.3.0 * * @return void */ private static function migrate_from_1_2_0() { - $user_ids = get_users( + $user_ids = \get_users( array( 'fields' => 'ID', 'capability__in' => array( 'publish_posts' ), @@ -220,4 +243,45 @@ class Migration { \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 + * + * @return void + */ + private static function migrate_from_2_1_0() { + // add the ActivityPub capability to all users that can publish posts + self::add_activitypub_capability(); + } + + /** + * Set the defaults needed for the plugin to work + * + * * Add the ActivityPub capability to all users that can publish posts + * + * @return void + */ + public static function add_default_settings() { + self::add_activitypub_capability(); + } + + /** + * Add the ActivityPub capability to all users that can publish posts + * + * @return void + */ + 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' ); + } + } } diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 0db5fe9f..48a556bc 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -64,9 +64,6 @@ class Scheduler { \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); - // Migration - \add_action( 'admin_init', array( self::class, 'schedule_migration' ) ); - // profile updates for blog options if ( ! is_user_type_disabled( 'blog' ) ) { \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) ); @@ -265,17 +262,6 @@ class Scheduler { } } - /** - * Schedule migration if DB-Version is not up to date. - * - * @return void - */ - public static function schedule_migration() { - if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) && ! Migration::is_latest_version() ) { - \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' ); - } - } - /** * Send a profile update when relevant user meta is updated. * @@ -287,7 +273,7 @@ class Scheduler { */ public static function user_meta_update( $meta_id, $user_id, $meta_key ) { // don't bother if the user can't publish - if ( ! \user_can( $user_id, 'publish_posts' ) ) { + if ( ! \user_can( $user_id, 'activitypub' ) ) { return; } // the user meta fields that affect a profile. @@ -311,7 +297,7 @@ class Scheduler { */ public static function user_update( $user_id ) { // don't bother if the user can't publish - if ( ! \user_can( $user_id, 'publish_posts' ) ) { + if ( ! \user_can( $user_id, 'activitypub' ) ) { return; } diff --git a/includes/collection/class-users.php b/includes/collection/class-users.php index ad94297c..a331c203 100644 --- a/includes/collection/class-users.php +++ b/includes/collection/class-users.php @@ -268,7 +268,7 @@ class Users { public static function get_collection() { $users = \get_users( array( - 'capability__in' => array( 'publish_posts' ), + 'capability__in' => array( 'activitypub' ), ) ); diff --git a/includes/functions.php b/includes/functions.php index ed516328..ad229b7a 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -384,7 +384,7 @@ function is_user_disabled( $user_id ) { break; } - if ( ! \user_can( $user_id, 'publish_posts' ) ) { + if ( ! \user_can( $user_id, 'activitypub' ) ) { $return = true; break; } @@ -644,7 +644,7 @@ function get_total_users() { $users = \get_users( array( - 'capability__in' => array( 'publish_posts' ), + 'capability__in' => array( 'activitypub' ), ) ); diff --git a/phpcs.xml b/phpcs.xml index 636c1052..c128c9b0 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -73,4 +73,11 @@ **/*.asset.php + + + + + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1aaa4f51..f4468fbc 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,3 +29,5 @@ function _manually_load_plugin() { // Start up the WP testing environment. require $_tests_dir . '/includes/bootstrap.php'; require __DIR__ . '/class-activitypub-testcase-cache-http.php'; + +\Activitypub\Migration::add_default_settings(); diff --git a/tests/test-class-activitypub-followers.php b/tests/test-class-activitypub-followers.php index 9a566650..f2ddfece 100644 --- a/tests/test-class-activitypub-followers.php +++ b/tests/test-class-activitypub-followers.php @@ -271,6 +271,8 @@ class Test_Activitypub_Followers extends WP_UnitTestCase { } public function test_migration() { + update_option( 'activitypub_db_version', '0.0.1' ); + $followers = array( 'https://example.com/author/jon', 'https://example.og/errors', @@ -286,6 +288,12 @@ class Test_Activitypub_Followers extends WP_UnitTestCase { \Activitypub\Migration::maybe_migrate(); + $schedule = \wp_next_scheduled( 'activitypub_migrate', '0.0.1' ); + + $this->assertNotFalse( $schedule ); + + do_action( 'activitypub_migrate', '0.0.1' ); + $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertCount( 3, $db_followers ); diff --git a/tests/test-class-activitypub-user.php b/tests/test-class-activitypub-user.php new file mode 100644 index 00000000..1c2e4870 --- /dev/null +++ b/tests/test-class-activitypub-user.php @@ -0,0 +1,33 @@ + 'subscriber@example.com', + 'first_name' => 'Max', + 'last_name' => 'Mustermann', + 'user_login' => 'subscriber', + 'user_pass' => 'subscriber', + 'role' => 'subscriber', + ); + + $user_id = wp_insert_user( $userdata ); + $can = user_can( $user_id, 'activitypub' ); + + $this->assertFalse( $can ); + + $userdata = array( + 'user_email' => 'editor@example.com', + 'first_name' => 'Max', + 'last_name' => 'Mustermann', + 'user_login' => 'editor', + 'user_pass' => 'editor', + 'role' => 'editor', + ); + + $user_id = wp_insert_user( $userdata ); + $can = user_can( $user_id, 'activitypub' ); + + $this->assertTrue( $can ); + } +}