PluginProbe
ActivityPub / 9.1.0
ActivityPub v9.1.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.1.0, at includes/wp-admin/class-admin.php

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