Skip to content

Commit 010689b

Browse files
pfefferleobenland
andauthored
Add bulk ActivityPub capability removal confirmation (#2150)
Co-authored-by: Konstantin Obenland <[email protected]>
1 parent c3bb2d0 commit 010689b

File tree

4 files changed

+236
-18
lines changed

4 files changed

+236
-18
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: added
3+
4+
Added confirmation step for bulk removal of ActivityPub capability, asking whether to also delete users from the Fediverse.

includes/scheduler/class-actor.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,6 @@ public static function sticky_post_update( $post_id ) {
174174
* @param int $user_id The user ID being deleted.
175175
*/
176176
public static function schedule_user_delete( $user_id ) {
177-
// Don't bother if the user can't publish ActivityPub content.
178-
if ( ! \user_can( $user_id, 'activitypub' ) ) {
179-
return;
180-
}
181-
182177
// Get the actor before deletion to ensure we have the data.
183178
$actor = Actors::get_by_id( $user_id );
184179
if ( \is_wp_error( $actor ) ) {

includes/wp-admin/class-admin.php

Lines changed: 170 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Activitypub\Comment;
1313
use Activitypub\Model\Blog;
1414
use Activitypub\Moderation;
15+
use Activitypub\Scheduler\Actor;
1516

1617
use function Activitypub\count_followers;
1718
use function Activitypub\get_content_visibility;
@@ -52,6 +53,9 @@ public static function init() {
5253
\add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) );
5354
\add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 );
5455

56+
\add_action( 'admin_post_delete_actor_confirmed', array( self::class, 'handle_bulk_actor_delete_confirmation' ) );
57+
\add_action( 'admin_action_activitypub_confirm_removal', array( self::class, 'handle_bulk_actor_delete_page' ) );
58+
5559
if ( user_can_activitypub( \get_current_user_id() ) ) {
5660
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
5761
}
@@ -582,7 +586,8 @@ public static function user_bulk_options( $actions ) {
582586
* Handle bulk activitypub requests.
583587
*
584588
* * `add_activitypub_cap` - Add the activitypub capability to the selected users.
585-
* * `remove_activitypub_cap` - Remove the activitypub capability from the selected users.
589+
* * `remove_activitypub_cap` - Remove the activitypub capability from the selected users (redirects to confirmation page).
590+
* * `delete_actor_confirmed` - Actually remove the capability after confirmation.
586591
*
587592
* @param string $send_back The URL to send the user back to.
588593
* @param string $action The requested action.
@@ -591,20 +596,172 @@ public static function user_bulk_options( $actions ) {
591596
* @return string The URL to send the user back to.
592597
*/
593598
public static function handle_bulk_request( $send_back, $action, $users ) {
594-
if (
595-
'remove_activitypub_cap' !== $action &&
596-
'add_activitypub_cap' !== $action
597-
) {
598-
return $send_back;
599+
switch ( $action ) {
600+
case 'add_activitypub_cap':
601+
foreach ( $users as $user_id ) {
602+
$user = new \WP_User( $user_id );
603+
$user->add_cap( 'activitypub' );
604+
}
605+
return $send_back;
606+
case 'remove_activitypub_cap':
607+
$removed_count = 0;
608+
609+
// Remove capabilities immediately.
610+
foreach ( $users as $key => $user_id ) {
611+
$user = new \WP_User( $user_id );
612+
613+
// Check if user has ActivityPub capability.
614+
if ( ! $user->has_cap( 'activitypub' ) ) {
615+
unset( $users[ $key ] );
616+
continue;
617+
}
618+
619+
// Remove the capability.
620+
$user->remove_cap( 'activitypub' );
621+
622+
// Force cache refresh for user capabilities.
623+
\wp_cache_delete( $user_id, 'users' );
624+
\wp_cache_delete( $user_id, 'user_meta' );
625+
626+
++$removed_count;
627+
}
628+
629+
// Build the query args with proper array handling for fediverse deletion confirmation.
630+
$query_args = array(
631+
'action' => 'activitypub_confirm_removal',
632+
'send_back' => \rawurlencode( $send_back ),
633+
);
634+
635+
// Add user IDs as separate parameters.
636+
foreach ( $users as $index => $user_id ) {
637+
$query_args[ sprintf( 'users[%d]', $index ) ] = absint( $user_id );
638+
}
639+
640+
$confirmation_url = \add_query_arg( $query_args, \admin_url( 'users.php' ) );
641+
642+
// Force redirect instead of just returning URL.
643+
\wp_safe_redirect( $confirmation_url );
644+
exit;
645+
case 'delete_actor_confirmed':
646+
// Use unified method with no fediverse deletion (keep).
647+
return self::process_capability_removal( $users, 'keep', $send_back );
648+
default:
649+
return $send_back;
599650
}
651+
}
600652

601-
foreach ( $users as $user_id ) {
602-
$user = new \WP_User( $user_id );
603-
if ( 'add_activitypub_cap' === $action ) {
604-
$user->add_cap( 'activitypub' );
605-
} elseif ( 'remove_activitypub_cap' === $action ) {
606-
$user->remove_cap( 'activitypub' );
607-
}
653+
/**
654+
* Handle the bulk capability removal page request directly.
655+
*/
656+
public static function handle_bulk_actor_delete_page() {
657+
658+
// Check permissions.
659+
if ( ! \current_user_can( 'edit_users' ) ) {
660+
\wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'activitypub' ) );
661+
}
662+
663+
// Get parameters.
664+
// phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
665+
$users = \wp_unslash( $_GET['users'] ?? array() );
666+
// phpcs:ignore WordPress.Security.NonceVerification
667+
$send_back = \urldecode( \sanitize_text_field( \wp_unslash( $_GET['send_back'] ?? '' ) ) );
668+
669+
// Sanitize user IDs.
670+
$users = \array_map( 'absint', (array) $users );
671+
$users = \array_filter( $users );
672+
673+
// Validate send_back URL.
674+
if ( empty( $send_back ) ) {
675+
$send_back = \admin_url( 'users.php' );
676+
}
677+
678+
// Load template and exit to prevent WordPress from trying to load other admin pages.
679+
\load_template(
680+
ACTIVITYPUB_PLUGIN_DIR . 'templates/bulk-actor-delete-confirmation.php',
681+
false,
682+
array(
683+
'users' => $users,
684+
'send_back' => $send_back,
685+
)
686+
);
687+
exit;
688+
}
689+
690+
691+
/**
692+
* Handle the bulk capability removal confirmation form submission.
693+
*/
694+
public static function handle_bulk_actor_delete_confirmation() {
695+
// Verify nonce.
696+
if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'bulk-users' ) ) {
697+
\wp_die( \esc_html__( 'Security check failed.', 'activitypub' ) );
698+
}
699+
700+
// Check permissions.
701+
if ( ! \current_user_can( 'edit_users' ) ) {
702+
\wp_die( \esc_html__( 'You do not have sufficient permissions to perform this action.', 'activitypub' ) );
703+
}
704+
705+
// Get form data.
706+
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
707+
$selected_users = \wp_unslash( $_POST['selected_users'] ?? array() );
708+
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
709+
$remove_from_fediverse = \wp_unslash( $_POST['remove_from_fediverse'] ?? array() );
710+
$send_back = \esc_url_raw( \wp_unslash( $_POST['send_back'] ?? '' ) );
711+
712+
// Sanitize user IDs.
713+
$selected_users = \array_map( 'absint', (array) $selected_users );
714+
$selected_users = \array_filter( $selected_users );
715+
716+
if ( empty( $selected_users ) ) {
717+
\wp_safe_redirect( $send_back );
718+
exit;
719+
}
720+
721+
// Process capability removal using unified method.
722+
$result = self::process_capability_removal( $selected_users, $remove_from_fediverse, $send_back );
723+
724+
// Redirect back.
725+
\wp_safe_redirect( $result );
726+
exit;
727+
}
728+
729+
730+
/**
731+
* Process fediverse deletion for users (capabilities already removed).
732+
*
733+
* @param array $users Array of user IDs.
734+
* @param array|string $remove_from_fediverse Array of user IDs to delete from fediverse, or 'delete'/'keep' for all users.
735+
* @param string $send_back URL to redirect back to.
736+
*
737+
* @return string The URL to redirect to.
738+
*/
739+
public static function process_capability_removal( $users, $remove_from_fediverse, $send_back ) {
740+
// Normalize fediverse removal parameter.
741+
if ( is_string( $remove_from_fediverse ) ) {
742+
// Legacy format: 'delete' or 'keep' for all users.
743+
$delete_all = ( 'delete' === $remove_from_fediverse );
744+
$users_to_delete = $delete_all ? $users : array();
745+
} else {
746+
// New format: array of specific user IDs to delete from fediverse.
747+
$remove_from_fediverse = \array_map( 'absint', (array) $remove_from_fediverse );
748+
$users_to_delete = \array_filter( $remove_from_fediverse );
749+
}
750+
751+
// Schedule delete activities for users who should be removed from fediverse.
752+
if ( ! empty( $users_to_delete ) ) {
753+
// Temporarily bypass capability checks for delete activity scheduling since capabilities were already removed.
754+
\add_filter( 'activitypub_user_can_activitypub', '__return_true' );
755+
756+
\array_map(
757+
array(
758+
Actor::class,
759+
'schedule_user_delete',
760+
),
761+
$users_to_delete
762+
);
763+
764+
\remove_filter( 'activitypub_user_can_activitypub', '__return_true' );
608765
}
609766

610767
return $send_back;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
/**
3+
* Bulk ActivityPub actor deletion confirmation template.
4+
*
5+
* @package Activitypub
6+
*/
7+
8+
/* @var array $args Template arguments. */
9+
$args = wp_parse_args( $args ?? array() );
10+
11+
$users = $args['users'] ?? array();
12+
$send_back = $args['send_back'] ?? '';
13+
14+
// Validate users.
15+
if ( empty( $users ) ) {
16+
wp_die( esc_html__( 'No users selected.', 'activitypub' ), '', array( 'back_link' => true ) );
17+
}
18+
19+
// Prepare user data for display.
20+
$users = get_users( array( 'include' => $users ) );
21+
22+
// If no users with ActivityPub capability, redirect back.
23+
if ( ! $users ) {
24+
wp_safe_redirect( $send_back );
25+
exit;
26+
}
27+
28+
$GLOBALS['plugin_page'] = ''; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
29+
require_once ABSPATH . 'wp-admin/admin-header.php';
30+
?>
31+
<div class="wrap">
32+
<h1><?php esc_html_e( 'Delete Users from Fediverse', 'activitypub' ); ?></h1>
33+
<p><?php esc_html_e( 'You&#8217;ve just removed the capability to publish to the Fediverse for the selected users, do you want to also remove them from the Fediverse?', 'activitypub' ); ?></p>
34+
<p><?php echo wp_kses( __( 'Fediverse deletion is optional but recommended to properly notify your followers. <strong>This action is irreversible.</strong>', 'activitypub' ), array( 'strong' => array() ) ); ?></p>
35+
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
36+
<?php wp_nonce_field( 'bulk-users' ); ?>
37+
38+
<input type="hidden" name="action" value="delete_actor_confirmed" />
39+
<input type="hidden" name="send_back" value="<?php echo esc_url( $send_back ); ?>" />
40+
41+
<div class="activitypub-user-list">
42+
<ul>
43+
<?php foreach ( $users as $user ) : ?>
44+
<li>
45+
<label>
46+
<input type="checkbox" name="remove_from_fediverse[]" value="<?php echo esc_attr( $user->ID ); ?>" class="fediverse-removal-checkbox" />
47+
<input type="hidden" name="selected_users[]" value="<?php echo esc_attr( $user->ID ); ?>" />
48+
<strong><?php echo esc_html( $user->display_name ); ?></strong>
49+
</label>
50+
</li>
51+
<?php endforeach; ?>
52+
</ul>
53+
</div>
54+
55+
<p class="submit">
56+
<?php submit_button( __( 'Delete from Fediverse', 'activitypub' ), 'primary', 'submit', false ); ?>
57+
<a href="<?php echo esc_url( $send_back ); ?>" class="button"><?php esc_html_e( 'Skip', 'activitypub' ); ?></a>
58+
</p>
59+
</form>
60+
</div>
61+
<?php
62+
require_once ABSPATH . 'wp-admin/admin-footer.php';

0 commit comments

Comments
 (0)