base && Extra_Fields::is_extra_fields_post_type( $current_screen->post_type ) ) { ?>
'1' === \get_option( 'activitypub_following_ui', '0' ), 'actorMode' => \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ), 'canManageOptions' => \current_user_can( 'manage_options' ), ) ); } if ( false !== strpos( $hook_suffix, 'activitypub' ) && 'dashboard_page_activitypub-social-web' !== $hook_suffix ) { wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), ACTIVITYPUB_PLUGIN_VERSION ); wp_enqueue_script( 'activitypub-admin-script', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery', 'wp-util' ), ACTIVITYPUB_PLUGIN_VERSION, false ); // Plugin cards in help tab. \wp_enqueue_script( 'plugin-install' ); \add_thickbox(); \wp_enqueue_script( 'updates' ); } if ( 'index.php' === $hook_suffix ) { wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), ACTIVITYPUB_PLUGIN_VERSION ); } } /** * Enqueue moderation admin scripts. */ public static function enqueue_moderation_scripts() { \wp_enqueue_script( 'activitypub-moderation-admin', ACTIVITYPUB_PLUGIN_URL . 'assets/js/activitypub-moderation-admin.js', array( 'jquery', 'wp-util', 'wp-a11y', 'wp-i18n' ), ACTIVITYPUB_PLUGIN_VERSION, true ); \wp_set_script_translations( 'activitypub-moderation-admin', 'activitypub', ACTIVITYPUB_PLUGIN_DIR . 'languages' ); // Localize script with translations and nonces. \wp_localize_script( 'activitypub-moderation-admin', 'activitypubModerationL10n', array( 'nonce' => \wp_create_nonce( 'activitypub_moderation_settings' ), ) ); } /** * Hook into the edit_comment functionality. * * Disables the edit_comment capability for federated comments. */ public static function edit_comment() { // phpcs:ignore WordPress.Security.NonceVerification $comment_id = \absint( $_GET['c'] ?? 0 ); if ( Comment::was_received( $comment_id ) ) { $path = 'edit-comments.php'; switch ( \wp_get_comment_status( $comment_id ) ) { // phpcs:ignore WordPress.Security.NonceVerification case 'spam': $path = 'edit-comments.php?comment_status=spam'; break; case 'trash': $path = 'edit-comments.php?comment_status=trash'; break; case 'unapproved': $path = 'edit-comments.php?comment_status=moderated'; break; } // Redirect to the appropriate comments page. \wp_safe_redirect( \admin_url( $path ) ); exit; } } /** * Hook into the edit_post functionality. * * Disables the edit_post capability for federated posts. */ public static function edit_post() { // Disable the edit_post capability for federated posts. \add_filter( 'user_has_cap', static function ( $all_caps, $caps, $arg ) { if ( 'edit_post' !== $arg[0] ) { return $all_caps; } $post = get_post( $arg[2] ); if ( ! Extra_Fields::is_extra_field_post_type( $post->post_type ) ) { return $all_caps; } if ( get_current_user_id() !== (int) $post->post_author ) { return false; } return $all_caps; }, 1, 3 ); } /** * Add ActivityPub specific actions/filters to the post list view. */ public static function list_posts() { // Remove all views for the extra fields. $screen_id = get_current_screen()->id; add_filter( "views_{$screen_id}", static function ( $views ) { if ( Extra_Fields::is_extra_fields_post_type( get_current_screen()->post_type ) ) { return array(); } return $views; } ); } /** * Comment row actions. * * @param array $actions The existing actions. * @param int|\WP_Comment $comment The comment object or ID. * * @return array The modified actions. */ public static function comment_row_actions( $actions, $comment ) { if ( was_comment_received( $comment ) ) { unset( $actions['edit'], $actions['quickedit'] ); } if ( in_array( get_comment_type( $comment ), Comment::get_comment_type_slugs(), true ) ) { unset( $actions['reply'] ); } return $actions; } /** * Add a column "activitypub". * * This column shows if the user has the capability to use ActivityPub. * * @param array $columns The columns. * * @return array The columns extended by the activitypub. */ public static function manage_users_columns( $columns ) { $columns['activitypub'] = __( 'ActivityPub', 'activitypub' ); return $columns; } /** * Add "comment-type" and "protocol" as column in WP-Admin. * * @param array $columns The list of column names. * * @return array The extended list of column names. */ public static function manage_comment_columns( $columns ) { $columns['comment_type'] = esc_attr__( 'Comment-Type', 'activitypub' ); $columns['comment_protocol'] = esc_attr__( 'Protocol', 'activitypub' ); return $columns; } /** * Add "post_content" as column for Extra-Fields in WP-Admin. * * @param array $columns The list of column names. * @param string $post_type The post type. * * @return array The extended list of column names. */ public static function manage_post_columns( $columns, $post_type ) { if ( Extra_Fields::is_extra_fields_post_type( $post_type ) ) { $after_key = 'title'; $index = array_search( $after_key, array_keys( $columns ), true ); $columns = array_slice( $columns, 0, $index + 1 ) + array( 'extra_field_content' => esc_attr__( 'Content', 'activitypub' ) ) + $columns; } return $columns; } /** * Add "comment-type" and "protocol" as column in WP-Admin. * * @param array $column The column to implement. * @param int $comment_id The comment id. */ public static function manage_comments_custom_column( $column, $comment_id ) { if ( 'comment_type' === $column && ! defined( 'WEBMENTION_PLUGIN_DIR' ) ) { echo esc_attr( ucfirst( get_comment_type( $comment_id ) ) ); } elseif ( 'comment_protocol' === $column ) { $protocol = get_comment_meta( $comment_id, 'protocol', true ); if ( $protocol ) { echo esc_attr( ucfirst( str_replace( 'activitypub', 'ActivityPub', $protocol ) ) ); } else { esc_attr_e( 'Local', 'activitypub' ); } } } /** * Add the new ActivityPub comment types to the comment types dropdown. * * @param array $types The existing comment types. * * @return array The extended comment types. */ public static function comment_types_dropdown( $types ) { foreach ( Comment::get_comment_types() as $comment_type ) { $types[ $comment_type['type'] ] = esc_html( $comment_type['label'] ); } return $types; } /** * Return the results for the activitypub column. * * @param string $output Custom column output. Default empty. * @param string $column_name Column name. * @param int $user_id ID of the currently-listed user. * * @return string The column contents. */ public static function manage_users_custom_column( $output, $column_name, $user_id ) { if ( 'activitypub' !== $column_name ) { return $output; } if ( \user_can( $user_id, 'activitypub' ) ) { return '' . esc_html__( 'ActivityPub enabled for this author', 'activitypub' ) . ''; } else { return '' . esc_html__( 'ActivityPub disabled for this author', 'activitypub' ) . ''; } } /** * Add a column "extra_field_content" to the post list view. * * @param string $column_name The column name. * @param int $post_id The post ID. * * @return void */ public static function manage_posts_custom_column( $column_name, $post_id ) { if ( 'extra_field_content' === $column_name ) { $post = get_post( $post_id ); if ( Extra_Fields::is_extra_fields_post_type( $post->post_type ) ) { echo esc_attr( wp_strip_all_tags( $post->post_content ) ); } } } /** * Add options to the Bulk dropdown on the users page. * * @param array $actions The existing bulk options. * * @return array The extended bulk options. */ public static function user_bulk_options( $actions ) { $actions['add_activitypub_cap'] = __( 'Enable for ActivityPub', 'activitypub' ); $actions['remove_activitypub_cap'] = __( 'Disable for ActivityPub', 'activitypub' ); return $actions; } /** * Handle bulk activitypub requests. * * * `add_activitypub_cap` - Add the activitypub capability to the selected users. * * `remove_activitypub_cap` - Remove the activitypub capability from the selected users (redirects to confirmation page). * * `delete_actor_confirmed` - Actually remove the capability after confirmation. * * @param string $send_back The URL to send the user back to. * @param string $action The requested action. * @param array $users The selected users. * * @return string The URL to send the user back to. */ public static function handle_bulk_request( $send_back, $action, $users ) { switch ( $action ) { case 'add_activitypub_cap': foreach ( $users as $user_id ) { $user = new \WP_User( $user_id ); $user->add_cap( 'activitypub' ); } return $send_back; case 'remove_activitypub_cap': $removed_count = 0; // Remove capabilities immediately. foreach ( $users as $key => $user_id ) { $user = new \WP_User( $user_id ); // Check if user has ActivityPub capability. if ( ! $user->has_cap( 'activitypub' ) ) { unset( $users[ $key ] ); continue; } // Remove the capability. $user->remove_cap( 'activitypub' ); // Force cache refresh for user capabilities. \wp_cache_delete( $user_id, 'users' ); \wp_cache_delete( $user_id, 'user_meta' ); ++$removed_count; } // Build the query args with proper array handling for fediverse deletion confirmation. $query_args = array( 'action' => 'activitypub_confirm_removal', 'send_back' => \rawurlencode( $send_back ), ); // Add user IDs as separate parameters. foreach ( $users as $index => $user_id ) { $query_args[ sprintf( 'users[%d]', $index ) ] = absint( $user_id ); } $confirmation_url = \add_query_arg( $query_args, \admin_url( 'users.php' ) ); // Force redirect instead of just returning URL. \wp_safe_redirect( $confirmation_url ); exit; case 'delete_actor_confirmed': // Use unified method with no fediverse deletion (keep). return self::process_capability_removal( $users, 'keep', $send_back ); default: return $send_back; } } /** * Handle the bulk capability removal page request directly. */ public static function handle_bulk_actor_delete_page() { // Check permissions. if ( ! \current_user_can( 'edit_users' ) ) { \wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'activitypub' ) ); } // Get parameters. // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput $users = \wp_unslash( $_GET['users'] ?? array() ); // phpcs:ignore WordPress.Security.NonceVerification $send_back = \urldecode( \sanitize_text_field( \wp_unslash( $_GET['send_back'] ?? '' ) ) ); // Sanitize user IDs. $users = \array_map( 'absint', (array) $users ); $users = \array_filter( $users ); // Validate send_back URL. if ( empty( $send_back ) ) { $send_back = \admin_url( 'users.php' ); } // Load template and exit to prevent WordPress from trying to load other admin pages. \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/bulk-actor-delete-confirmation.php', false, array( 'users' => $users, 'send_back' => $send_back, ) ); exit; } /** * Handle the bulk capability removal confirmation form submission. */ public static function handle_bulk_actor_delete_confirmation() { // Verify nonce. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'bulk-users' ) ) { \wp_die( \esc_html__( 'Security check failed.', 'activitypub' ) ); } // Check permissions. if ( ! \current_user_can( 'edit_users' ) ) { \wp_die( \esc_html__( 'You do not have sufficient permissions to perform this action.', 'activitypub' ) ); } // Get form data. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $selected_users = \wp_unslash( $_POST['selected_users'] ?? array() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput $remove_from_fediverse = \wp_unslash( $_POST['remove_from_fediverse'] ?? array() ); $send_back = \esc_url_raw( \wp_unslash( $_POST['send_back'] ?? '' ) ); // Sanitize user IDs. $selected_users = \array_map( 'absint', (array) $selected_users ); $selected_users = \array_filter( $selected_users ); if ( empty( $selected_users ) ) { \wp_safe_redirect( $send_back ); exit; } // Process capability removal using unified method. $result = self::process_capability_removal( $selected_users, $remove_from_fediverse, $send_back ); // Redirect back. \wp_safe_redirect( $result ); exit; } /** * Process fediverse deletion for users (capabilities already removed). * * @param array $users Array of user IDs. * @param array|string $remove_from_fediverse Array of user IDs to delete from fediverse, or 'delete'/'keep' for all users. * @param string $send_back URL to redirect back to. * * @return string The URL to redirect to. */ public static function process_capability_removal( $users, $remove_from_fediverse, $send_back ) { // Normalize fediverse removal parameter. if ( is_string( $remove_from_fediverse ) ) { // Legacy format: 'delete' or 'keep' for all users. $delete_all = ( 'delete' === $remove_from_fediverse ); $users_to_delete = $delete_all ? $users : array(); } else { // New format: array of specific user IDs to delete from fediverse. $remove_from_fediverse = \array_map( 'absint', (array) $remove_from_fediverse ); $users_to_delete = \array_filter( $remove_from_fediverse ); } // Schedule delete activities for users who should be removed from fediverse. if ( ! empty( $users_to_delete ) ) { // Temporarily bypass capability checks for delete activity scheduling since capabilities were already removed. \add_filter( 'activitypub_user_can_activitypub', '__return_true' ); \array_map( array( Actor::class, 'schedule_user_delete', ), $users_to_delete ); \remove_filter( 'activitypub_user_can_activitypub', '__return_true' ); } return $send_back; } /** * Add ActivityPub infos to the dashboard glance items. * * @param array $items The existing glance items. * * @return array The extended glance items. */ public static function dashboard_glance_items( $items ) { \add_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 2 ); if ( user_can_activitypub( \get_current_user_id() ) ) { $follower_count = sprintf( // translators: %s: number of followers. _n( '%s Follower', '%s Followers', count_followers( \get_current_user_id() ), 'activitypub' ), \number_format_i18n( count_followers( \get_current_user_id() ) ) ); $items['activitypub-followers-user'] = sprintf( '%3$s', \esc_url( \admin_url( 'users.php?page=activitypub-followers-list' ) ), \esc_attr__( 'Your followers', 'activitypub' ), \esc_html( $follower_count ) ); } if ( ! is_user_type_disabled( 'blog' ) && current_user_can( 'manage_options' ) ) { $follower_count = sprintf( // translators: %s: number of followers. _n( '%s Follower (Blog)', '%s Followers (Blog)', count_followers( Actors::BLOG_USER_ID ), 'activitypub' ), \number_format_i18n( count_followers( Actors::BLOG_USER_ID ) ) ); $items['activitypub-followers-blog'] = sprintf( '%3$s', \esc_url( \admin_url( 'options-general.php?page=activitypub&tab=followers' ) ), \esc_attr__( 'The Blog\'s followers', 'activitypub' ), \esc_html( $follower_count ) ); } \remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers' ); return $items; } /** * Add a "Fediverse Preview ⁂" link to the row actions. * * @param array $actions The existing actions. * @param \WP_Post $post The post object. * * @return array The modified actions. */ public static function row_actions( $actions, $post ) { // check if the post is enabled for ActivityPub. if ( ! \post_type_supports( \get_post_type( $post ), 'activitypub' ) || ! in_array( $post->post_status, array( 'pending', 'draft', 'future', 'publish' ), true ) || ! \current_user_can( 'edit_post', $post->ID ) || ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL === get_content_visibility( $post->ID ) || ( site_supports_blocks() && \use_block_editor_for_post_type( $post->post_type ) ) ) { return $actions; } $preview_url = add_query_arg( 'activitypub', 'true', \get_preview_post_link( $post ) ); $actions['activitypub'] = sprintf( '%s', \esc_url( $preview_url ), \esc_html__( 'Fediverse Preview ⁂', 'activitypub' ) ); return $actions; } /** * Add plugin settings link. * * @param array $actions The current actions. */ public static function add_plugin_settings_link( $actions ) { $actions[] = \sprintf( '%2s', \menu_page_url( 'activitypub', false ), \__( 'Settings', 'activitypub' ) ); return $actions; } /** * Display plugin upgrade notice to users. * * @param array $data The plugin data. * @param object $update The plugin update data. */ public static function plugin_update_message( $data, $update ) { if ( ! isset( $update->upgrade_notice ) ) { return; } echo '
' . wp_strip_all_tags( $update->upgrade_notice ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } /** * Adds meta box on wp-admin/tools.php. */ public static function tool_box() { \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/toolbox.php' ); } /** * Open the help tab. * * This function is used to open the help tab, * it is triggered by the hash in the URL. */ public static function open_help_tab() { // get all tabs registered for the ActivityPub settings page. $tabs = \get_current_screen()->get_help_tabs(); $ids = \array_values( \wp_list_pluck( $tabs, 'id' ) ); $ids = \array_map( static function ( $id ) { return '#tab-link-' . $id; }, $ids ); ?> '; \wp_widget_rss_output( array( 'url' => 'https://activitypub.blog/feed/', 'items' => 3, 'show_summary' => 1, 'show_author' => 0, 'show_date' => 1, ) ); echo ''; } /** * Add the ActivityPub Author profile as a Dashboard widget. */ public static function profile_dashboard_widget() { $user = Actors::get_by_id( \get_current_user_id() ); ?>

\__( 'Invalid context or action.', 'activitypub' ) ) ); } if ( empty( $type ) || empty( $value ) || ! in_array( $type, array( 'domain', 'keyword' ), true ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid parameters.', 'activitypub' ) ) ); } // Verify nonce for all operations. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_moderation_settings' ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) ); } if ( ! \current_user_can( 'manage_options' ) ) { \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) ); } if ( 'user' === $context ) { $user_id = (int) ( \sanitize_text_field( \wp_unslash( $_POST['user_id'] ?? 0 ) ) ); // Check permissions. if ( \get_current_user_id() !== $user_id ) { \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) ); } if ( ! $user_id ) { \wp_send_json_error( array( 'message' => \__( 'Invalid user ID.', 'activitypub' ) ) ); } if ( 'add' === $operation ) { $success = Moderation::add_user_block( $user_id, $type, $value ); $error_message = \__( 'Failed to add block.', 'activitypub' ); } else { $success = Moderation::remove_user_block( $user_id, $type, $value ); $error_message = \__( 'Failed to remove block.', 'activitypub' ); } } elseif ( 'add' === $operation ) { $success = Moderation::add_site_block( $type, $value ); $error_message = \__( 'Failed to add block.', 'activitypub' ); } else { $success = Moderation::remove_site_block( $type, $value ); $error_message = \__( 'Failed to remove block.', 'activitypub' ); } if ( $success ) { \wp_send_json_success(); } else { \wp_send_json_error( array( 'message' => $error_message ) ); } } /** * AJAX handler for blocklist subscriptions (add/remove). */ public static function ajax_blocklist_subscription() { $operation = \sanitize_text_field( \wp_unslash( $_POST['operation'] ?? '' ) ); $url = \sanitize_url( \wp_unslash( $_POST['url'] ?? '' ) ); // Validate required parameters. if ( ! \in_array( $operation, array( 'add', 'remove' ), true ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid operation.', 'activitypub' ) ) ); } if ( empty( $url ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid URL.', 'activitypub' ) ) ); } // Verify nonce. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_moderation_settings' ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) ); } if ( ! \current_user_can( 'manage_options' ) ) { \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) ); } if ( 'add' === $operation ) { // First add the subscription (validates URL format). if ( ! Blocklist_Subscriptions::add( $url ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid URL.', 'activitypub' ) ) ); } // Then sync to validate it works and import domains. $result = Blocklist_Subscriptions::sync( $url ); if ( false === $result ) { // Remove the subscription since sync failed. Blocklist_Subscriptions::remove( $url ); \wp_send_json_error( array( 'message' => \__( 'Failed to fetch blocklist. The URL may be unreachable or not contain valid domains.', 'activitypub' ) ) ); } \wp_send_json_success(); } elseif ( Blocklist_Subscriptions::remove( $url ) ) { \wp_send_json_success(); } else { \wp_send_json_error( array( 'message' => \__( 'Failed to remove subscription.', 'activitypub' ) ) ); } } }