Improve User management (#703)

* Use an ActivityPub capability to better enable/disable ActivityPub support

* split PRs

* remove test hook

* do not run migrations for new installs

* fix unit tests

* fix unit tests

* remove abandoned schedule!

* fix migration class

* fix order

* restructuring

* remove follower migration

* do not yet remove legacy followers

* remove blog-user changes

* use a const for the version number

* add user tests and fix old ones

* use a more generic async migrator

* optimized test
This commit is contained in:
Matthias Pfefferle 2024-03-11 15:19:07 +01:00 committed by GitHub
parent 96626c6438
commit 79f400d88a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 249 additions and 41 deletions

View file

@ -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'];

View file

@ -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;
}

View file

@ -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' );
}
}
}

View file

@ -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;
}
}

View file

@ -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' );
}
}
}

View file

@ -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;
}

View file

@ -268,7 +268,7 @@ class Users {
public static function get_collection() {
$users = \get_users(
array(
'capability__in' => array( 'publish_posts' ),
'capability__in' => array( 'activitypub' ),
)
);

View file

@ -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' ),
)
);

View file

@ -73,4 +73,11 @@
<rule ref="WordPress.Arrays.ArrayDeclarationSpacing">
<exclude-pattern>**/*.asset.php</exclude-pattern>
</rule>
<rule ref="WordPress.WP.Capabilities">
<properties>
<property name="custom_capabilities" type="array">
<element value="activitypub" />
</property>
</properties>
</rule>
</ruleset>

View file

@ -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();

View file

@ -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 );

View file

@ -0,0 +1,33 @@
<?php
class Test_Activitypub_User extends WP_UnitTestCase {
public function test_activitypub_cap() {
$userdata = array(
'user_email' => '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 );
}
}