PluginProbe
ActivityPub / 9.3.0
ActivityPub v9.3.0
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 / wp-admin / class-admin.php

class-admin.php in ActivityPub 9.3.0, at includes/wp-admin/class-admin.php

1,283 lines 42.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\WP_Admin;
9
10 use Activitypub\Blocklist_Subscriptions;
11 use Activitypub\Collection\Actors;
12 use Activitypub\Collection\Extra_Fields;
13 use Activitypub\Comment;
14 use Activitypub\Moderation;
15 use Activitypub\OAuth\Client;
16 use Activitypub\OAuth\Token;
17 use Activitypub\Scheduler\Actor;
18 use Activitypub\Tombstone;
19
20 use function Activitypub\count_followers;
21 use function Activitypub\get_content_visibility;
22 use function Activitypub\is_user_type_disabled;
23 use function Activitypub\site_supports_blocks;
24 use function Activitypub\user_can_activitypub;
25 use function Activitypub\was_comment_received;
26
27 /**
28 * ActivityPub Admin Class.
29 *
30 * @author Matthias Pfefferle
31 */
32 class Admin {
33 /**
34 * Initialize the class, registering WordPress hooks,
35 */
36 public static function init() {
37 \add_action( 'load-comment.php', array( self::class, 'edit_comment' ) );
38 \add_action( 'load-post.php', array( self::class, 'edit_post' ) );
39 \add_action( 'load-edit.php', array( self::class, 'list_posts' ) );
40 \add_filter( 'page_row_actions', array( self::class, 'row_actions' ), 10, 2 );
41 \add_filter( 'post_row_actions', array( self::class, 'row_actions' ), 10, 2 );
42 \add_action( 'personal_options_update', array( self::class, 'save_user_settings' ) );
43 \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
44 \add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
45
46 \add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 );
47 \add_filter( 'manage_edit-comments_columns', array( static::class, 'manage_comment_columns' ) );
48 \add_action( 'manage_comments_custom_column', array( static::class, 'manage_comments_custom_column' ), 9, 2 );
49 \add_filter( 'admin_comment_types_dropdown', array( static::class, 'comment_types_dropdown' ) );
50
51 \add_filter( 'manage_posts_columns', array( static::class, 'manage_post_columns' ), 10, 2 );
52 \add_action( 'manage_posts_custom_column', array( self::class, 'manage_posts_custom_column' ), 10, 2 );
53
54 \add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ) );
55 \add_filter( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 );
56 \add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) );
57 \add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 );
58
59 \add_action( 'admin_post_delete_actor_confirmed', array( self::class, 'handle_bulk_actor_delete_confirmation' ) );
60 \add_action( 'admin_action_activitypub_confirm_removal', array( self::class, 'handle_bulk_actor_delete_page' ) );
61
62 if ( user_can_activitypub( \get_current_user_id() ) ) {
63 \add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
64 if ( \get_option( 'activitypub_api', false ) ) {
65 \add_action( 'show_user_profile', array( User_Settings_Fields::class, 'connected_apps_section' ) );
66 }
67 }
68
69 \add_filter( 'dashboard_glance_items', array( self::class, 'dashboard_glance_items' ) );
70 \add_filter( 'plugin_action_links_' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'add_plugin_settings_link' ) );
71 \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ), 10, 2 );
72
73 if ( site_supports_blocks() ) {
74 \add_action( 'tool_box', array( self::class, 'tool_box' ) );
75 }
76
77 \add_action( 'admin_print_scripts-profile.php', array( self::class, 'enqueue_moderation_scripts' ) );
78 \add_action( 'admin_print_scripts-profile.php', array( self::class, 'enqueue_connected_apps_scripts' ) );
79 \add_action( 'admin_print_scripts-settings_page_activitypub', array( self::class, 'enqueue_moderation_scripts' ) );
80 \add_action( 'admin_print_footer_scripts-settings_page_activitypub', array( self::class, 'open_help_tab' ) );
81
82 \add_action( 'wp_ajax_activitypub_moderation_settings', array( self::class, 'ajax_moderation_settings' ) );
83 \add_action( 'wp_ajax_activitypub_blocklist_subscription', array( self::class, 'ajax_blocklist_subscription' ) );
84 \add_action( 'wp_ajax_activitypub_register_oauth_client', array( self::class, 'ajax_register_oauth_client' ) );
85 \add_action( 'wp_ajax_activitypub_delete_oauth_client', array( self::class, 'ajax_delete_oauth_client' ) );
86 \add_action( 'wp_ajax_activitypub_delete_all_oauth_clients', array( self::class, 'ajax_delete_all_oauth_clients' ) );
87 \add_action( 'wp_ajax_activitypub_revoke_oauth_token', array( self::class, 'ajax_revoke_oauth_token' ) );
88 \add_action( 'wp_ajax_activitypub_revoke_all_oauth_tokens', array( self::class, 'ajax_revoke_all_oauth_tokens' ) );
89 }
90
91 /**
92 * Display admin menu notices about configuration problems or conflicts.
93 */
94 public static function admin_notices() {
95 $current_screen = \get_current_screen();
96
97 if ( ! $current_screen ) {
98 return;
99 }
100
101 // Check for self-destruct completion notice.
102 $self_destruct_complete = \get_option( 'activitypub_self_destruct_complete' );
103 if ( $self_destruct_complete ) {
104 // Show the notice only once, then remove it.
105 \delete_option( 'activitypub_self_destruct_complete' );
106 ?>
107 <div class="notice notice-success is-dismissible">
108 <p>
109 <strong><?php \esc_html_e( 'ActivityPub Self-Destruct Complete!', 'activitypub' ); ?></strong>
110 </p>
111 <p>
112 <?php \esc_html_e( 'All Delete activities have been successfully sent to the Fediverse. Your blog is no longer discoverable via ActivityPub and all followers have been notified of the deletion.', 'activitypub' ); ?>
113 </p>
114 </div>
115 <?php
116 }
117
118 if ( 'edit' === $current_screen->base && Extra_Fields::is_extra_fields_post_type( $current_screen->post_type ) ) {
119 ?>
120 <div class="notice" style="margin: 0; background: none; border: none; box-shadow: none; padding: 15px 0 0 0; font-size: 14px;">
121 <?php
122 \esc_html_e( 'These are extra fields that are used for your ActivityPub profile. You can use your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' );
123 ?>
124 </div>
125 <?php
126 }
127 }
128
129 /**
130 * Load user settings page.
131 */
132 public static function followers_list_page() {
133 // User has to be able to publish posts.
134 if ( user_can_activitypub( \get_current_user_id() ) ) {
135 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' );
136 }
137 }
138
139 /**
140 * Load user following list page.
141 */
142 public static function following_list_page() {
143 // User has to be able to publish posts.
144 if ( user_can_activitypub( \get_current_user_id() ) ) {
145 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/following-list.php' );
146 }
147 }
148
149 /**
150 * Load blocked actors page.
151 */
152 public static function blocked_actors_list_page() {
153 // User has to be able to publish posts.
154 if ( user_can_activitypub( \get_current_user_id() ) ) {
155 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blocked-actors-list.php' );
156 }
157 }
158
159 /**
160 * Creates the followers and following list tables in ActivityPub settings.
161 */
162 public static function add_settings_list_tables() {
163 $tab = \sanitize_text_field( \wp_unslash( $_GET['tab'] ?? 'welcome' ) ); // phpcs:ignore WordPress.Security.NonceVerification
164
165 switch ( $tab ) {
166 case 'followers':
167 self::add_followers_list_table();
168 break;
169 case 'following':
170 self::add_following_list_table();
171 break;
172 case 'blocked-actors':
173 self::add_blocked_actors_list_table();
174 break;
175 }
176 }
177
178 /**
179 * Creates the followers list table.
180 */
181 public static function add_followers_list_table() {
182 $GLOBALS['followers_list_table'] = new Table\Followers();
183 }
184
185 /**
186 * Creates the following list table.
187 */
188 public static function add_following_list_table() {
189 $GLOBALS['following_list_table'] = new Table\Following();
190 }
191
192 /**
193 * Creates the blocked actors list table.
194 */
195 public static function add_blocked_actors_list_table() {
196 $GLOBALS['blocked_actors_list_table'] = new Table\Blocked_Actors();
197 }
198
199 /**
200 * Render user settings.
201 */
202 public static function add_profile() {
203 \wp_enqueue_media();
204 \wp_enqueue_script( 'activitypub-header-image' );
205
206 \wp_nonce_field( 'activitypub-user-settings', '_apnonce' );
207 \do_settings_sections( 'activitypub_user_settings' );
208 }
209
210 /**
211 * Save the user settings.
212 *
213 * Handles the saving of the ActivityPub settings.
214 *
215 * @param int $user_id The user ID.
216 */
217 public static function save_user_settings( $user_id ) {
218 if ( ! isset( $_REQUEST['_apnonce'] ) ) {
219 return;
220 }
221
222 $nonce = \sanitize_text_field( \wp_unslash( $_REQUEST['_apnonce'] ) );
223 if (
224 ! \wp_verify_nonce( $nonce, 'activitypub-user-settings' ) ||
225 ! \current_user_can( 'edit_user', $user_id )
226 ) {
227 return;
228 }
229
230 // User options that should be processed with `sanitize_textarea_field()`.
231 $textarea_field_user_options = array(
232 'activitypub_also_known_as',
233 'activitypub_description',
234 );
235
236 foreach ( $textarea_field_user_options as $option ) {
237 if ( ! empty( $_POST[ $option ] ) ) {
238 \update_user_option( $user_id, $option, \sanitize_textarea_field( \wp_unslash( $_POST[ $option ] ) ) );
239 } else {
240 \delete_user_option( $user_id, $option );
241 }
242 }
243
244 // User options that should be processed with `sanitize_text_field()`.
245 $text_field_user_options = array(
246 'activitypub_header_image',
247 );
248
249 foreach ( $text_field_user_options as $option ) {
250 if ( ! empty( $_POST[ $option ] ) ) {
251 \update_user_option( $user_id, $option, \sanitize_text_field( \wp_unslash( $_POST[ $option ] ) ) );
252 } else {
253 \delete_user_option( $user_id, $option );
254 }
255 }
256
257 // User options that have a default value and therefore can't be empty (Empty triggers the default value).
258 $required_user_options = array(
259 'activitypub_hide_social_graph',
260 'activitypub_mailer_new_dm',
261 'activitypub_mailer_new_follower',
262 'activitypub_mailer_new_mention',
263 'activitypub_mailer_new_reaction',
264 'activitypub_mailer_annual_report',
265 'activitypub_mailer_monthly_report',
266 );
267
268 foreach ( $required_user_options as $option ) {
269 \update_user_option( $user_id, $option, \sanitize_text_field( \wp_unslash( $_POST[ $option ] ?? 0 ) ) );
270 }
271 }
272
273 /**
274 * Enqueue the admin scripts and styles.
275 *
276 * @param string $hook_suffix The current page.
277 */
278 public static function enqueue_scripts( $hook_suffix ) {
279 \wp_register_script(
280 'activitypub-header-image',
281 \plugins_url(
282 'assets/js/activitypub-header-image.js',
283 ACTIVITYPUB_PLUGIN_FILE
284 ),
285 array( 'jquery' ),
286 ACTIVITYPUB_PLUGIN_VERSION,
287 false
288 );
289
290 // Register and enqueue command palette integration.
291 if ( user_can_activitypub( \get_current_user_id() ) || \current_user_can( 'manage_options' ) ) {
292 $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/command-palette/plugin.asset.php';
293 \wp_enqueue_script(
294 'activitypub-command-palette',
295 \plugins_url( 'build/command-palette/plugin.js', ACTIVITYPUB_PLUGIN_FILE ),
296 $asset_data['dependencies'],
297 $asset_data['version'],
298 true
299 );
300
301 \wp_localize_script(
302 'activitypub-command-palette',
303 'activitypubCommandPalette',
304 array(
305 'followingEnabled' => '1' === \get_option( 'activitypub_following_ui', '0' ),
306 'actorMode' => \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ),
307 'canManageOptions' => \current_user_can( 'manage_options' ),
308 )
309 );
310 }
311
312 if ( false !== \strpos( $hook_suffix, 'activitypub' ) && 'dashboard_page_activitypub-social-web' !== $hook_suffix ) {
313 \wp_enqueue_style(
314 'activitypub-admin-styles',
315 \plugins_url(
316 'assets/css/activitypub-admin.css',
317 ACTIVITYPUB_PLUGIN_FILE
318 ),
319 array(),
320 ACTIVITYPUB_PLUGIN_VERSION
321 );
322 \wp_enqueue_script(
323 'activitypub-admin-script',
324 \plugins_url(
325 'assets/js/activitypub-admin.js',
326 ACTIVITYPUB_PLUGIN_FILE
327 ),
328 array( 'jquery', 'wp-util' ),
329 ACTIVITYPUB_PLUGIN_VERSION,
330 false
331 );
332
333 // Plugin cards in help tab.
334 \wp_enqueue_script( 'plugin-install' );
335 \add_thickbox();
336 \wp_enqueue_script( 'updates' );
337 }
338
339 if ( 'index.php' === $hook_suffix ) {
340 \wp_enqueue_style(
341 'activitypub-admin-styles',
342 \plugins_url(
343 'assets/css/activitypub-admin.css',
344 ACTIVITYPUB_PLUGIN_FILE
345 ),
346 array(),
347 ACTIVITYPUB_PLUGIN_VERSION
348 );
349 }
350
351 if ( 'edit-comments.php' === $hook_suffix ) {
352 \wp_add_inline_style(
353 'wp-emoji-styles',
354 '.column-author img.emoji { float: none; }'
355 );
356 }
357 }
358
359 /**
360 * Enqueue moderation admin scripts.
361 */
362 public static function enqueue_moderation_scripts() {
363 \wp_enqueue_script(
364 'activitypub-moderation-admin',
365 ACTIVITYPUB_PLUGIN_URL . 'assets/js/activitypub-moderation-admin.js',
366 array( 'jquery', 'wp-util', 'wp-a11y', 'wp-i18n' ),
367 ACTIVITYPUB_PLUGIN_VERSION,
368 true
369 );
370
371 \wp_set_script_translations(
372 'activitypub-moderation-admin',
373 'activitypub',
374 ACTIVITYPUB_PLUGIN_DIR . 'languages'
375 );
376
377 // Localize script with translations and nonces.
378 \wp_localize_script(
379 'activitypub-moderation-admin',
380 'activitypubModerationL10n',
381 array(
382 'nonce' => \wp_create_nonce( 'activitypub_moderation_settings' ),
383 )
384 );
385 }
386
387 /**
388 * Enqueue connected apps admin scripts on the profile page.
389 *
390 * @since 8.1.0
391 */
392 public static function enqueue_connected_apps_scripts() {
393 \wp_enqueue_script(
394 'activitypub-connected-apps',
395 ACTIVITYPUB_PLUGIN_URL . 'assets/js/activitypub-connected-apps.js',
396 array( 'jquery' ),
397 ACTIVITYPUB_PLUGIN_VERSION,
398 true
399 );
400
401 \wp_localize_script(
402 'activitypub-connected-apps',
403 'activitypubConnectedApps',
404 array(
405 'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
406 'nonce' => \wp_create_nonce( 'activitypub_connected_apps' ),
407 'confirm' => \__( 'Are you sure you want to revoke this application token? This action cannot be undone.', 'activitypub' ),
408 'confirmAll' => \__( 'Are you sure you want to revoke all connected applications? This action cannot be undone.', 'activitypub' ),
409 'confirmDelete' => \__( 'Are you sure you want to delete this application? This action cannot be undone.', 'activitypub' ),
410 'confirmDeleteAll' => \__( 'Are you sure you want to delete all registered applications? This action cannot be undone.', 'activitypub' ),
411 'registerError' => \__( 'Failed to register application.', 'activitypub' ),
412 'deleteLabel' => \__( 'Delete', 'activitypub' ),
413 'dismiss' => \__( 'Dismiss this notice.', 'activitypub' ),
414 'clientIdLabel' => \__( 'Your new Client ID:', 'activitypub' ),
415 'clientSecretLabel' => \__( 'Your new Client Secret:', 'activitypub' ),
416 'copy' => \__( 'Copy', 'activitypub' ),
417 'copied' => \__( 'Copied!', 'activitypub' ),
418 'saveWarning' => \__( 'Be sure to save this in a safe location. You will not be able to retrieve it.', 'activitypub' ),
419 'appRevoked' => \__( 'Application token revoked.', 'activitypub' ),
420 'allAppsRevoked' => \__( 'All application tokens revoked.', 'activitypub' ),
421 'appDeleted' => \__( 'Application deleted.', 'activitypub' ),
422 'allAppsDeleted' => \__( 'All registered applications deleted.', 'activitypub' ),
423 )
424 );
425 }
426
427 /**
428 * Hook into the edit_comment functionality.
429 *
430 * Disables the edit_comment capability for federated comments.
431 */
432 public static function edit_comment() {
433 // phpcs:ignore WordPress.Security.NonceVerification
434 $comment_id = \absint( $_GET['c'] ?? 0 );
435 if ( Comment::was_received( $comment_id ) ) {
436 $path = 'edit-comments.php';
437
438 switch ( \wp_get_comment_status( $comment_id ) ) { // phpcs:ignore WordPress.Security.NonceVerification
439 case 'spam':
440 $path = 'edit-comments.php?comment_status=spam';
441 break;
442
443 case 'trash':
444 $path = 'edit-comments.php?comment_status=trash';
445 break;
446
447 case 'unapproved':
448 $path = 'edit-comments.php?comment_status=moderated';
449 break;
450 }
451
452 // Redirect to the appropriate comments page.
453 \wp_safe_redirect( \admin_url( $path ) );
454 exit;
455 }
456 }
457
458 /**
459 * Hook into the edit_post functionality.
460 *
461 * Disables the edit_post capability for federated posts.
462 */
463 public static function edit_post() {
464 // Disable the edit_post capability for federated posts.
465 \add_filter(
466 'user_has_cap',
467 static function ( $all_caps, $caps, $arg ) {
468 if ( 'edit_post' !== $arg[0] ) {
469 return $all_caps;
470 }
471
472 $post = \get_post( $arg[2] );
473
474 if ( ! Extra_Fields::is_extra_field_post_type( $post->post_type ) ) {
475 return $all_caps;
476 }
477
478 if ( \get_current_user_id() !== (int) $post->post_author ) {
479 return false;
480 }
481
482 return $all_caps;
483 },
484 1,
485 3
486 );
487 }
488
489 /**
490 * Add ActivityPub specific actions/filters to the post list view.
491 */
492 public static function list_posts() {
493 // Remove all views for the extra fields.
494 $screen_id = \get_current_screen()->id;
495
496 \add_filter(
497 "views_{$screen_id}",
498 static function ( $views ) {
499 if ( Extra_Fields::is_extra_fields_post_type( \get_current_screen()->post_type ) ) {
500 return array();
501 }
502
503 return $views;
504 }
505 );
506 }
507
508 /**
509 * Comment row actions.
510 *
511 * @param array $actions The existing actions.
512 * @param int|\WP_Comment $comment The comment object or ID.
513 *
514 * @return array The modified actions.
515 */
516 public static function comment_row_actions( $actions, $comment ) {
517 if ( was_comment_received( $comment ) ) {
518 unset( $actions['edit'], $actions['quickedit'] );
519 }
520
521 if ( \in_array( \get_comment_type( $comment ), Comment::get_comment_type_slugs(), true ) ) {
522 unset( $actions['reply'] );
523 }
524
525 return $actions;
526 }
527
528 /**
529 * Add a column "activitypub".
530 *
531 * This column shows if the user has the capability to use ActivityPub.
532 *
533 * @param array $columns The columns.
534 *
535 * @return array The columns extended by the activitypub.
536 */
537 public static function manage_users_columns( $columns ) {
538 $columns['activitypub'] = \__( 'ActivityPub', 'activitypub' );
539 return $columns;
540 }
541
542 /**
543 * Add "comment-type" and "protocol" as column in WP-Admin.
544 *
545 * @param array $columns The list of column names.
546 *
547 * @return array The extended list of column names.
548 */
549 public static function manage_comment_columns( $columns ) {
550 $columns['comment_type'] = \esc_attr__( 'Comment-Type', 'activitypub' );
551 $columns['comment_protocol'] = \esc_attr__( 'Protocol', 'activitypub' );
552
553 return $columns;
554 }
555
556 /**
557 * Add "post_content" as column for Extra-Fields in WP-Admin.
558 *
559 * @param array $columns The list of column names.
560 * @param string $post_type The post type.
561 *
562 * @return array The extended list of column names.
563 */
564 public static function manage_post_columns( $columns, $post_type ) {
565 if ( Extra_Fields::is_extra_fields_post_type( $post_type ) ) {
566 $after_key = 'title';
567 $index = \array_search( $after_key, \array_keys( $columns ), true );
568 $columns = \array_slice( $columns, 0, $index + 1 ) + array( 'extra_field_content' => \esc_attr__( 'Content', 'activitypub' ) ) + $columns;
569 }
570
571 return $columns;
572 }
573
574 /**
575 * Add "comment-type" and "protocol" as column in WP-Admin.
576 *
577 * @param array $column The column to implement.
578 * @param int $comment_id The comment id.
579 */
580 public static function manage_comments_custom_column( $column, $comment_id ) {
581 if ( 'comment_type' === $column && ! \defined( 'WEBMENTION_PLUGIN_DIR' ) ) {
582 echo \esc_attr( \ucfirst( \get_comment_type( $comment_id ) ) );
583 } elseif ( 'comment_protocol' === $column ) {
584 $protocol = \get_comment_meta( $comment_id, 'protocol', true );
585
586 if ( $protocol ) {
587 echo \esc_attr( \ucfirst( \str_replace( 'activitypub', 'ActivityPub', $protocol ) ) );
588 } else {
589 \esc_attr_e( 'Local', 'activitypub' );
590 }
591 }
592 }
593
594 /**
595 * Add the new ActivityPub comment types to the comment types dropdown.
596 *
597 * @param array $types The existing comment types.
598 *
599 * @return array The extended comment types.
600 */
601 public static function comment_types_dropdown( $types ) {
602 foreach ( Comment::get_comment_types() as $comment_type ) {
603 $types[ $comment_type['type'] ] = \esc_html( $comment_type['label'] );
604 }
605
606 return $types;
607 }
608
609 /**
610 * Return the results for the activitypub column.
611 *
612 * @param string $output Custom column output. Default empty.
613 * @param string $column_name Column name.
614 * @param int $user_id ID of the currently-listed user.
615 *
616 * @return string The column contents.
617 */
618 public static function manage_users_custom_column( $output, $column_name, $user_id ) {
619 if ( 'activitypub' !== $column_name ) {
620 return $output;
621 }
622
623 if ( \user_can( $user_id, 'activitypub' ) ) {
624 return '<span aria-hidden="true">&#x2713;</span><span class="screen-reader-text">' . \esc_html__( 'ActivityPub enabled for this author', 'activitypub' ) . '</span>';
625 } else {
626 return '<span aria-hidden="true">&#x2717;</span><span class="screen-reader-text">' . \esc_html__( 'ActivityPub disabled for this author', 'activitypub' ) . '</span>';
627 }
628 }
629
630 /**
631 * Add a column "extra_field_content" to the post list view.
632 *
633 * @param string $column_name The column name.
634 * @param int $post_id The post ID.
635 *
636 * @return void
637 */
638 public static function manage_posts_custom_column( $column_name, $post_id ) {
639 if ( 'extra_field_content' === $column_name ) {
640 $post = \get_post( $post_id );
641 if ( Extra_Fields::is_extra_fields_post_type( $post->post_type ) ) {
642 echo \esc_attr( \wp_strip_all_tags( $post->post_content ) );
643 }
644 }
645 }
646
647 /**
648 * Add options to the Bulk dropdown on the users page.
649 *
650 * @param array $actions The existing bulk options.
651 *
652 * @return array The extended bulk options.
653 */
654 public static function user_bulk_options( $actions ) {
655 $actions['add_activitypub_cap'] = \__( 'Enable for ActivityPub', 'activitypub' );
656 $actions['remove_activitypub_cap'] = \__( 'Disable for ActivityPub', 'activitypub' );
657
658 return $actions;
659 }
660
661 /**
662 * Handle bulk activitypub requests.
663 *
664 * * `add_activitypub_cap` - Add the activitypub capability to the selected users.
665 * * `remove_activitypub_cap` - Remove the activitypub capability from the selected users (redirects to confirmation page).
666 * * `delete_actor_confirmed` - Actually remove the capability after confirmation.
667 *
668 * @param string $send_back The URL to send the user back to.
669 * @param string $action The requested action.
670 * @param array $users The selected users.
671 *
672 * @return string The URL to send the user back to.
673 */
674 public static function handle_bulk_request( $send_back, $action, $users ) {
675 switch ( $action ) {
676 case 'add_activitypub_cap':
677 foreach ( $users as $user_id ) {
678 $user = new \WP_User( $user_id );
679 $user->add_cap( 'activitypub' );
680
681 // Remove user from tombstone registry if they were previously buried.
682 $actor = Actors::get_by_id( $user_id );
683 if ( ! \is_wp_error( $actor ) ) {
684 Tombstone::remove( $actor->get_id(), $actor->get_url() );
685 }
686 }
687 return $send_back;
688 case 'remove_activitypub_cap':
689 $removed_count = 0;
690
691 // Remove capabilities immediately.
692 foreach ( $users as $key => $user_id ) {
693 $user = new \WP_User( $user_id );
694
695 // Check if user has ActivityPub capability.
696 if ( ! $user->has_cap( 'activitypub' ) ) {
697 unset( $users[ $key ] );
698 continue;
699 }
700
701 // Remove the capability.
702 $user->remove_cap( 'activitypub' );
703
704 // Force cache refresh for user capabilities.
705 \wp_cache_delete( $user_id, 'users' );
706 \wp_cache_delete( $user_id, 'user_meta' );
707
708 ++$removed_count;
709 }
710
711 // Build the query args with proper array handling for fediverse deletion confirmation.
712 $query_args = array(
713 'action' => 'activitypub_confirm_removal',
714 'send_back' => \rawurlencode( $send_back ),
715 );
716
717 // Add user IDs as separate parameters.
718 foreach ( $users as $index => $user_id ) {
719 $query_args[ \sprintf( 'users[%d]', $index ) ] = \absint( $user_id );
720 }
721
722 $confirmation_url = \add_query_arg( $query_args, \admin_url( 'users.php' ) );
723
724 // Force redirect instead of just returning URL.
725 \wp_safe_redirect( $confirmation_url );
726 exit;
727 case 'delete_actor_confirmed':
728 // Use unified method with no fediverse deletion (keep).
729 return self::process_capability_removal( $users, 'keep', $send_back );
730 default:
731 return $send_back;
732 }
733 }
734
735 /**
736 * Handle the bulk capability removal page request directly.
737 */
738 public static function handle_bulk_actor_delete_page() {
739
740 // Check permissions.
741 if ( ! \current_user_can( 'edit_users' ) ) {
742 \wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'activitypub' ) );
743 }
744
745 // Get parameters.
746 // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
747 $users = \wp_unslash( $_GET['users'] ?? array() );
748 // phpcs:ignore WordPress.Security.NonceVerification
749 $send_back = \urldecode( \sanitize_text_field( \wp_unslash( $_GET['send_back'] ?? '' ) ) );
750
751 // Sanitize user IDs.
752 $users = \array_map( 'absint', (array) $users );
753 $users = \array_filter( $users );
754
755 // Validate send_back URL.
756 if ( empty( $send_back ) ) {
757 $send_back = \admin_url( 'users.php' );
758 }
759
760 // Load template and exit to prevent WordPress from trying to load other admin pages.
761 \load_template(
762 ACTIVITYPUB_PLUGIN_DIR . 'templates/bulk-actor-delete-confirmation.php',
763 false,
764 array(
765 'users' => $users,
766 'send_back' => $send_back,
767 )
768 );
769 exit;
770 }
771
772
773 /**
774 * Handle the bulk capability removal confirmation form submission.
775 */
776 public static function handle_bulk_actor_delete_confirmation() {
777 // Verify nonce.
778 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'bulk-users' ) ) {
779 \wp_die( \esc_html__( 'Security check failed.', 'activitypub' ) );
780 }
781
782 // Check permissions.
783 if ( ! \current_user_can( 'edit_users' ) ) {
784 \wp_die( \esc_html__( 'You do not have sufficient permissions to perform this action.', 'activitypub' ) );
785 }
786
787 // Get form data.
788 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
789 $selected_users = \wp_unslash( $_POST['selected_users'] ?? array() );
790 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
791 $remove_from_fediverse = \wp_unslash( $_POST['remove_from_fediverse'] ?? array() );
792 $send_back = \esc_url_raw( \wp_unslash( $_POST['send_back'] ?? '' ) );
793
794 // Sanitize user IDs.
795 $selected_users = \array_map( 'absint', (array) $selected_users );
796 $selected_users = \array_filter( $selected_users );
797
798 if ( empty( $selected_users ) ) {
799 \wp_safe_redirect( $send_back );
800 exit;
801 }
802
803 // Process capability removal using unified method.
804 $result = self::process_capability_removal( $selected_users, $remove_from_fediverse, $send_back );
805
806 // Redirect back.
807 \wp_safe_redirect( $result );
808 exit;
809 }
810
811
812 /**
813 * Process fediverse deletion for users (capabilities already removed).
814 *
815 * @param array $users Array of user IDs.
816 * @param array|string $remove_from_fediverse Array of user IDs to delete from fediverse, or 'delete'/'keep' for all users.
817 * @param string $send_back URL to redirect back to.
818 *
819 * @return string The URL to redirect to.
820 */
821 public static function process_capability_removal( $users, $remove_from_fediverse, $send_back ) {
822 // Normalize fediverse removal parameter.
823 if ( \is_string( $remove_from_fediverse ) ) {
824 // Legacy format: 'delete' or 'keep' for all users.
825 $delete_all = ( 'delete' === $remove_from_fediverse );
826 $users_to_delete = $delete_all ? $users : array();
827 } else {
828 // New format: array of specific user IDs to delete from fediverse.
829 $remove_from_fediverse = \array_map( 'absint', (array) $remove_from_fediverse );
830 $users_to_delete = \array_filter( $remove_from_fediverse );
831 }
832
833 // Schedule delete activities for users who should be removed from fediverse.
834 if ( ! empty( $users_to_delete ) ) {
835 // Temporarily bypass capability checks for delete activity scheduling since capabilities were already removed.
836 \add_filter( 'activitypub_user_can_activitypub', '__return_true' );
837
838 \array_map(
839 array(
840 Actor::class,
841 'schedule_user_delete',
842 ),
843 $users_to_delete
844 );
845
846 \remove_filter( 'activitypub_user_can_activitypub', '__return_true' );
847 }
848
849 return $send_back;
850 }
851
852 /**
853 * Add ActivityPub infos to the dashboard glance items.
854 *
855 * @param array $items The existing glance items.
856 *
857 * @return array The extended glance items.
858 */
859 public static function dashboard_glance_items( $items ) {
860 \add_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 2 );
861
862 if ( user_can_activitypub( \get_current_user_id() ) ) {
863 $follower_count = \sprintf(
864 // translators: %s: number of followers.
865 \_n(
866 '%s Follower',
867 '%s Followers',
868 count_followers( \get_current_user_id() ),
869 'activitypub'
870 ),
871 \number_format_i18n( count_followers( \get_current_user_id() ) )
872 );
873 $items['activitypub-followers-user'] = \sprintf(
874 '<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
875 \esc_url( \admin_url( 'users.php?page=activitypub-followers-list' ) ),
876 \esc_attr__( 'Your followers', 'activitypub' ),
877 \esc_html( $follower_count )
878 );
879 }
880
881 if ( ! is_user_type_disabled( 'blog' ) && \current_user_can( 'manage_options' ) ) {
882 $follower_count = \sprintf(
883 // translators: %s: number of followers.
884 \_n(
885 '%s Follower (Blog)',
886 '%s Followers (Blog)',
887 count_followers( Actors::BLOG_USER_ID ),
888 'activitypub'
889 ),
890 \number_format_i18n( count_followers( Actors::BLOG_USER_ID ) )
891 );
892 $items['activitypub-followers-blog'] = \sprintf(
893 '<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
894 \esc_url( \admin_url( 'options-general.php?page=activitypub&tab=followers' ) ),
895 \esc_attr__( 'The Blog\'s followers', 'activitypub' ),
896 \esc_html( $follower_count )
897 );
898 }
899
900 \remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers' );
901
902 return $items;
903 }
904
905 /**
906 * Add a "Fediverse Preview ⁂" link to the row actions.
907 *
908 * @param array $actions The existing actions.
909 * @param \WP_Post $post The post object.
910 *
911 * @return array The modified actions.
912 */
913 public static function row_actions( $actions, $post ) {
914 // check if the post is enabled for ActivityPub.
915 if (
916 ! \post_type_supports( \get_post_type( $post ), 'activitypub' ) ||
917 ! \in_array( $post->post_status, array( 'pending', 'draft', 'future', 'publish' ), true ) ||
918 ! \current_user_can( 'edit_post', $post->ID ) ||
919 ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL === get_content_visibility( $post->ID ) ||
920 ( site_supports_blocks() && \use_block_editor_for_post_type( $post->post_type ) )
921 ) {
922 return $actions;
923 }
924
925 $preview_url = \add_query_arg( 'activitypub', 'true', \get_preview_post_link( $post ) );
926
927 $actions['activitypub'] = \sprintf(
928 '<a href="%s" target="_blank">%s</a>',
929 \esc_url( $preview_url ),
930 \esc_html__( 'Fediverse Preview ⁂', 'activitypub' )
931 );
932
933 return $actions;
934 }
935
936 /**
937 * Add plugin settings link.
938 *
939 * @param array $actions The current actions.
940 */
941 public static function add_plugin_settings_link( $actions ) {
942 $actions[] = \sprintf(
943 '<a href="%1s">%2s</a>',
944 \menu_page_url( 'activitypub', false ),
945 \__( 'Settings', 'activitypub' )
946 );
947
948 return $actions;
949 }
950
951 /**
952 * Display plugin upgrade notice to users.
953 *
954 * @param array $data The plugin data.
955 * @param object $update The plugin update data.
956 */
957 public static function plugin_update_message( $data, $update ) {
958 if ( ! isset( $update->upgrade_notice ) ) {
959 return;
960 }
961
962 echo '<br>' . \wp_strip_all_tags( $update->upgrade_notice ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
963 }
964
965 /**
966 * Adds meta box on wp-admin/tools.php.
967 */
968 public static function tool_box() {
969 \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/toolbox.php' );
970 }
971
972 /**
973 * Open the help tab.
974 *
975 * This function is used to open the help tab,
976 * it is triggered by the hash in the URL.
977 */
978 public static function open_help_tab() {
979 // get all tabs registered for the ActivityPub settings page.
980 $tabs = \get_current_screen()->get_help_tabs();
981 $ids = \array_values( \wp_list_pluck( $tabs, 'id' ) );
982 $ids = \array_map(
983 static function ( $id ) {
984 return '#tab-link-' . $id;
985 },
986 $ids
987 );
988 ?>
989 <script type="text/javascript">
990 function activitypub_open_help_tab(event) {
991 const allowed_ids = <?php echo \wp_json_encode( $ids ); ?>;
992
993 if ( allowed_ids.includes( window.location.hash ) ) {
994 const delay = ( event && event.type === 'hashchange' ) ? 0 : 200;
995
996 setTimeout( function() {
997 document.getElementById( 'contextual-help-link' ).click();
998 document.querySelector( window.location.hash + ' > a[href^="#tab-panel-"]' ).click();
999 }, delay );
1000 }
1001 }
1002 window.addEventListener( 'DOMContentLoaded', activitypub_open_help_tab );
1003 window.addEventListener( 'hashchange', activitypub_open_help_tab );
1004 </script>
1005 <?php
1006 }
1007
1008 /**
1009 * AJAX handler for moderation settings (add/remove blocks).
1010 */
1011 public static function ajax_moderation_settings() {
1012 $context = \sanitize_text_field( \wp_unslash( $_POST['context'] ?? '' ) );
1013 $operation = \sanitize_text_field( \wp_unslash( $_POST['operation'] ?? '' ) );
1014 $type = \sanitize_text_field( \wp_unslash( $_POST['type'] ?? '' ) );
1015 $value = \sanitize_text_field( \wp_unslash( $_POST['value'] ?? '' ) );
1016
1017 // Validate required parameters.
1018 if ( ! \in_array( $context, array( 'user', 'site' ), true ) || ! \in_array( $operation, array( 'add', 'remove' ), true ) ) {
1019 \wp_send_json_error( array( 'message' => \__( 'Invalid context or action.', 'activitypub' ) ) );
1020 }
1021
1022 if ( empty( $type ) || empty( $value ) || ! \in_array( $type, array( 'domain', 'keyword' ), true ) ) {
1023 \wp_send_json_error( array( 'message' => \__( 'Invalid parameters.', 'activitypub' ) ) );
1024 }
1025
1026 // Verify nonce for all operations.
1027 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_moderation_settings' ) ) {
1028 \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) );
1029 }
1030
1031 if ( ! \current_user_can( 'manage_options' ) ) {
1032 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1033 }
1034
1035 if ( 'user' === $context ) {
1036 $user_id = (int) ( \sanitize_text_field( \wp_unslash( $_POST['user_id'] ?? 0 ) ) );
1037
1038 // Check permissions.
1039 if ( \get_current_user_id() !== $user_id ) {
1040 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1041 }
1042
1043 if ( ! $user_id ) {
1044 \wp_send_json_error( array( 'message' => \__( 'Invalid user ID.', 'activitypub' ) ) );
1045 }
1046
1047 if ( 'add' === $operation ) {
1048 $success = Moderation::add_user_block( $user_id, $type, $value );
1049 $error_message = \__( 'Failed to add block.', 'activitypub' );
1050 } else {
1051 $success = Moderation::remove_user_block( $user_id, $type, $value );
1052 $error_message = \__( 'Failed to remove block.', 'activitypub' );
1053 }
1054 } elseif ( 'add' === $operation ) {
1055 $success = Moderation::add_site_block( $type, $value );
1056 $error_message = \__( 'Failed to add block.', 'activitypub' );
1057 } else {
1058 $success = Moderation::remove_site_block( $type, $value );
1059 $error_message = \__( 'Failed to remove block.', 'activitypub' );
1060 }
1061
1062 if ( $success ) {
1063 \wp_send_json_success();
1064 } else {
1065 \wp_send_json_error( array( 'message' => $error_message ) );
1066 }
1067 }
1068
1069 /**
1070 * AJAX handler for blocklist subscriptions (add/remove).
1071 */
1072 public static function ajax_blocklist_subscription() {
1073 $operation = \sanitize_text_field( \wp_unslash( $_POST['operation'] ?? '' ) );
1074 $url = \sanitize_url( \wp_unslash( $_POST['url'] ?? '' ) );
1075
1076 // Validate required parameters.
1077 if ( ! \in_array( $operation, array( 'add', 'remove' ), true ) ) {
1078 \wp_send_json_error( array( 'message' => \__( 'Invalid operation.', 'activitypub' ) ) );
1079 }
1080
1081 if ( empty( $url ) ) {
1082 \wp_send_json_error( array( 'message' => \__( 'Invalid URL.', 'activitypub' ) ) );
1083 }
1084
1085 // Verify nonce.
1086 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_moderation_settings' ) ) {
1087 \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) );
1088 }
1089
1090 if ( ! \current_user_can( 'manage_options' ) ) {
1091 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1092 }
1093
1094 if ( 'add' === $operation ) {
1095 // First add the subscription (validates URL format).
1096 if ( ! Blocklist_Subscriptions::add( $url ) ) {
1097 \wp_send_json_error( array( 'message' => \__( 'Invalid URL.', 'activitypub' ) ) );
1098 }
1099
1100 // Then sync to validate it works and import domains.
1101 $result = Blocklist_Subscriptions::sync( $url );
1102 if ( false === $result ) {
1103 // Remove the subscription since sync failed.
1104 Blocklist_Subscriptions::remove( $url );
1105 \wp_send_json_error( array( 'message' => \__( 'Failed to fetch blocklist. The URL may be unreachable or not contain valid domains.', 'activitypub' ) ) );
1106 }
1107
1108 \wp_send_json_success();
1109 } elseif ( Blocklist_Subscriptions::remove( $url ) ) {
1110 \wp_send_json_success();
1111 } else {
1112 \wp_send_json_error( array( 'message' => \__( 'Failed to remove subscription.', 'activitypub' ) ) );
1113 }
1114 }
1115
1116 /**
1117 * AJAX handler for registering a new OAuth client from the user profile.
1118 *
1119 * @since 8.1.0
1120 */
1121 public static function ajax_register_oauth_client() {
1122 // Verify nonce.
1123 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) {
1124 \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) );
1125 }
1126
1127 if ( ! \current_user_can( 'manage_options' ) ) {
1128 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1129 }
1130
1131 $name = \sanitize_text_field( \wp_unslash( $_POST['name'] ?? '' ) );
1132 $redirect_uri = \sanitize_url( \wp_unslash( $_POST['redirect_uri'] ?? '' ) );
1133
1134 if ( empty( $name ) ) {
1135 \wp_send_json_error( array( 'message' => \__( 'Application name is required.', 'activitypub' ) ) );
1136 }
1137
1138 if ( empty( $redirect_uri ) ) {
1139 \wp_send_json_error( array( 'message' => \__( 'Redirect URI is required.', 'activitypub' ) ) );
1140 }
1141
1142 $result = Client::register(
1143 array(
1144 'name' => $name,
1145 'redirect_uris' => array( $redirect_uri ),
1146 'is_public' => false,
1147 )
1148 );
1149
1150 if ( \is_wp_error( $result ) ) {
1151 \wp_send_json_error( array( 'message' => $result->get_error_message() ) );
1152 }
1153
1154 $data = array(
1155 'client_id' => $result['client_id'],
1156 'created' => \date_i18n( \get_option( 'date_format' ) ),
1157 );
1158
1159 if ( ! empty( $result['client_secret'] ) ) {
1160 $data['client_secret'] = $result['client_secret'];
1161 }
1162
1163 \wp_send_json_success( $data );
1164 }
1165
1166 /**
1167 * AJAX handler for deleting a registered OAuth client.
1168 *
1169 * @since 8.1.0
1170 */
1171 public static function ajax_delete_oauth_client() {
1172 // Verify nonce.
1173 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) {
1174 \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) );
1175 }
1176
1177 if ( ! \current_user_can( 'manage_options' ) ) {
1178 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1179 }
1180
1181 $client_id = \sanitize_text_field( \wp_unslash( $_POST['client_id'] ?? '' ) );
1182
1183 if ( empty( $client_id ) ) {
1184 \wp_send_json_error( array( 'message' => \__( 'Invalid client ID.', 'activitypub' ) ) );
1185 }
1186
1187 $deleted = Client::delete( $client_id );
1188
1189 if ( ! $deleted ) {
1190 \wp_send_json_error( array( 'message' => \__( 'Failed to delete application.', 'activitypub' ) ) );
1191 }
1192
1193 \wp_send_json_success( array( 'deleted' => true ) );
1194 }
1195
1196 /**
1197 * AJAX handler for deleting all manually registered OAuth clients.
1198 *
1199 * @since 8.1.0
1200 */
1201 public static function ajax_delete_all_oauth_clients() {
1202 // Verify nonce.
1203 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) {
1204 \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) );
1205 }
1206
1207 if ( ! \current_user_can( 'manage_options' ) ) {
1208 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1209 }
1210
1211 $clients = Client::get_manually_registered();
1212
1213 foreach ( $clients as $client ) {
1214 Client::delete( $client->get_client_id() );
1215 }
1216
1217 \wp_send_json_success( array( 'deleted' => ! empty( $clients ) ) );
1218 }
1219
1220 /**
1221 * AJAX handler for revoking an OAuth token from the user profile.
1222 *
1223 * Follows the WordPress core Application Passwords pattern.
1224 *
1225 * @since 8.1.0
1226 */
1227 public static function ajax_revoke_oauth_token() {
1228 // Verify nonce.
1229 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) {
1230 \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) );
1231 }
1232
1233 if ( ! \current_user_can( 'read' ) ) {
1234 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1235 }
1236
1237 $meta_key = \sanitize_text_field( \wp_unslash( $_POST['meta_key'] ?? '' ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a DB query parameter.
1238
1239 // Verify the meta key belongs to our token prefix.
1240 if ( 0 !== \strpos( $meta_key, Token::META_PREFIX ) ) {
1241 \wp_send_json_error( array( 'message' => \__( 'Invalid token.', 'activitypub' ) ) );
1242 }
1243
1244 $user_id = \get_current_user_id();
1245 $token_data = \get_user_meta( $user_id, $meta_key, true );
1246
1247 // Verify the token belongs to the current user.
1248 if ( empty( $token_data ) || ! \is_array( $token_data ) ) {
1249 \wp_send_json_error( array( 'message' => \__( 'Token not found.', 'activitypub' ) ) );
1250 }
1251
1252 // Delete the token.
1253 \delete_user_meta( $user_id, $meta_key );
1254
1255 // Delete the associated refresh token index.
1256 if ( ! empty( $token_data['refresh_token_hash'] ) ) {
1257 \delete_user_meta( $user_id, Token::REFRESH_INDEX_PREFIX . $token_data['refresh_token_hash'] );
1258 }
1259
1260 \wp_send_json_success( array( 'deleted' => true ) );
1261 }
1262
1263 /**
1264 * AJAX handler for revoking all OAuth tokens for the current user.
1265 *
1266 * @since 8.1.0
1267 */
1268 public static function ajax_revoke_all_oauth_tokens() {
1269 // Verify nonce.
1270 if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) {
1271 \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) );
1272 }
1273
1274 if ( ! \current_user_can( 'read' ) ) {
1275 \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) );
1276 }
1277
1278 $count = Token::revoke_all_for_user( \get_current_user_id() );
1279
1280 \wp_send_json_success( array( 'deleted' => $count > 0 ) );
1281 }
1282 }
1283