PluginProbe
ActivityPub / 7.8.0
ActivityPub v7.8.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 7.8.0, at includes/wp-admin/class-admin.php

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