PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.2
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 8.0.2, at includes/wp-admin/class-admin.php

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