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

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