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 ); } if ( 'edit-comments.php' === $hook_suffix ) { \wp_add_inline_style( 'wp-emoji-styles', '.column-author img.emoji { float: none; }' ); } } /** * 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' ), ) ); } /** * Enqueue connected apps admin scripts on the profile page. * * @since 8.1.0 */ public static function enqueue_connected_apps_scripts() { \wp_enqueue_script( 'activitypub-connected-apps', ACTIVITYPUB_PLUGIN_URL . 'assets/js/activitypub-connected-apps.js', array( 'jquery' ), ACTIVITYPUB_PLUGIN_VERSION, true ); \wp_localize_script( 'activitypub-connected-apps', 'activitypubConnectedApps', array( 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), 'nonce' => \wp_create_nonce( 'activitypub_connected_apps' ), 'confirm' => \__( 'Are you sure you want to revoke this application token? This action cannot be undone.', 'activitypub' ), 'confirmAll' => \__( 'Are you sure you want to revoke all connected applications? This action cannot be undone.', 'activitypub' ), 'confirmDelete' => \__( 'Are you sure you want to delete this application? This action cannot be undone.', 'activitypub' ), 'confirmDeleteAll' => \__( 'Are you sure you want to delete all registered applications? This action cannot be undone.', 'activitypub' ), 'registerError' => \__( 'Failed to register application.', 'activitypub' ), 'deleteLabel' => \__( 'Delete', 'activitypub' ), 'dismiss' => \__( 'Dismiss this notice.', 'activitypub' ), 'clientIdLabel' => \__( 'Your new Client ID:', 'activitypub' ), 'clientSecretLabel' => \__( 'Your new Client Secret:', 'activitypub' ), 'copy' => \__( 'Copy', 'activitypub' ), 'copied' => \__( 'Copied!', 'activitypub' ), 'saveWarning' => \__( 'Be sure to save this in a safe location. You will not be able to retrieve it.', 'activitypub' ), 'appRevoked' => \__( 'Application token revoked.', 'activitypub' ), 'allAppsRevoked' => \__( 'All application tokens revoked.', 'activitypub' ), 'appDeleted' => \__( 'Application deleted.', 'activitypub' ), 'allAppsDeleted' => \__( 'All registered applications deleted.', 'activitypub' ), ) ); } /** * 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' ); // Remove user from tombstone registry if they were previously buried. $actor = Actors::get_by_id( $user_id ); if ( ! \is_wp_error( $actor ) ) { Tombstone::remove( $actor->get_id(), $actor->get_url() ); } } 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 ); ?> \__( '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' ) ) ); } } /** * AJAX handler for registering a new OAuth client from the user profile. * * @since 8.1.0 */ public static function ajax_register_oauth_client() { // Verify nonce. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) { \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' ) ) ); } $name = \sanitize_text_field( \wp_unslash( $_POST['name'] ?? '' ) ); $redirect_uri = \sanitize_url( \wp_unslash( $_POST['redirect_uri'] ?? '' ) ); if ( empty( $name ) ) { \wp_send_json_error( array( 'message' => \__( 'Application name is required.', 'activitypub' ) ) ); } if ( empty( $redirect_uri ) ) { \wp_send_json_error( array( 'message' => \__( 'Redirect URI is required.', 'activitypub' ) ) ); } $result = Client::register( array( 'name' => $name, 'redirect_uris' => array( $redirect_uri ), 'is_public' => false, ) ); if ( \is_wp_error( $result ) ) { \wp_send_json_error( array( 'message' => $result->get_error_message() ) ); } $data = array( 'client_id' => $result['client_id'], 'created' => \date_i18n( \get_option( 'date_format' ) ), ); if ( ! empty( $result['client_secret'] ) ) { $data['client_secret'] = $result['client_secret']; } \wp_send_json_success( $data ); } /** * AJAX handler for deleting a registered OAuth client. * * @since 8.1.0 */ public static function ajax_delete_oauth_client() { // Verify nonce. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) { \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' ) ) ); } $client_id = \sanitize_text_field( \wp_unslash( $_POST['client_id'] ?? '' ) ); if ( empty( $client_id ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid client ID.', 'activitypub' ) ) ); } $deleted = Client::delete( $client_id ); if ( ! $deleted ) { \wp_send_json_error( array( 'message' => \__( 'Failed to delete application.', 'activitypub' ) ) ); } \wp_send_json_success( array( 'deleted' => true ) ); } /** * AJAX handler for deleting all manually registered OAuth clients. * * @since 8.1.0 */ public static function ajax_delete_all_oauth_clients() { // Verify nonce. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) { \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' ) ) ); } $clients = Client::get_manually_registered(); foreach ( $clients as $client ) { Client::delete( $client->get_client_id() ); } \wp_send_json_success( array( 'deleted' => ! empty( $clients ) ) ); } /** * AJAX handler for revoking an OAuth token from the user profile. * * Follows the WordPress core Application Passwords pattern. * * @since 8.1.0 */ public static function ajax_revoke_oauth_token() { // Verify nonce. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) ); } if ( ! \current_user_can( 'read' ) ) { \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) ); } $meta_key = \sanitize_text_field( \wp_unslash( $_POST['meta_key'] ?? '' ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Not a DB query parameter. // Verify the meta key belongs to our token prefix. if ( 0 !== \strpos( $meta_key, Token::META_PREFIX ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid token.', 'activitypub' ) ) ); } $user_id = \get_current_user_id(); $token_data = \get_user_meta( $user_id, $meta_key, true ); // Verify the token belongs to the current user. if ( empty( $token_data ) || ! \is_array( $token_data ) ) { \wp_send_json_error( array( 'message' => \__( 'Token not found.', 'activitypub' ) ) ); } // Delete the token. \delete_user_meta( $user_id, $meta_key ); // Delete the associated refresh token index. if ( ! empty( $token_data['refresh_token_hash'] ) ) { \delete_user_meta( $user_id, Token::REFRESH_INDEX_PREFIX . $token_data['refresh_token_hash'] ); } \wp_send_json_success( array( 'deleted' => true ) ); } /** * AJAX handler for revoking all OAuth tokens for the current user. * * @since 8.1.0 */ public static function ajax_revoke_all_oauth_tokens() { // Verify nonce. if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'activitypub_connected_apps' ) ) { \wp_send_json_error( array( 'message' => \__( 'Invalid nonce.', 'activitypub' ) ) ); } if ( ! \current_user_can( 'read' ) ) { \wp_send_json_error( array( 'message' => \__( 'You do not have permission to perform this action.', 'activitypub' ) ) ); } $count = Token::revoke_all_for_user( \get_current_user_id() ); \wp_send_json_success( array( 'deleted' => $count > 0 ) ); } }