From ffc18896c9d287d5363b4c609c43c3fc3e4bbae0 Mon Sep 17 00:00:00 2001 From: Yash Yadav Date: Thu, 9 Apr 2026 11:19:45 +0000 Subject: [PATCH] Added support for invalidating application password by passing app_id in the request --- .../class-wp-application-passwords.php | 41 +++++++++ ...-rest-application-passwords-controller.php | 27 +++++- .../rest-application-passwords-controller.php | 91 +++++++++++++++++++ 3 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-application-passwords.php b/src/wp-includes/class-wp-application-passwords.php index 6e84e0a2b2d0b..73d79c36b92e5 100644 --- a/src/wp-includes/class-wp-application-passwords.php +++ b/src/wp-includes/class-wp-application-passwords.php @@ -429,6 +429,47 @@ public static function delete_all_application_passwords( $user_id ) { return 0; } + /** + * Deletes all application passwords for the given user with a matching app ID. + * + * @since 7.1.0 + * + * @param int $user_id User ID. + * @param string $app_id The application ID. + * @return int|WP_Error The number of passwords that were deleted or a WP_Error on failure. + */ + public static function delete_application_passwords_by_app_id( $user_id, $app_id ) { + $passwords = static::get_user_application_passwords( $user_id ); + $remaining = array(); + $deleted_passwords = array(); + + foreach ( $passwords as $password ) { + if ( $password['app_id'] === $app_id ) { + $deleted_passwords[] = $password; + continue; + } + + $remaining[] = $password; + } + + if ( ! $deleted_passwords ) { + return 0; + } + + $saved = static::set_user_application_passwords( $user_id, $remaining ); + + if ( ! $saved ) { + return new WP_Error( 'db_error', __( 'Could not delete application passwords.' ) ); + } + + foreach ( $deleted_passwords as $password ) { + /** This action is documented in wp-includes/class-wp-application-passwords.php */ + do_action( 'wp_delete_application_password', $user_id, $password ); + } + + return count( $deleted_passwords ); + } + /** * Sets a user's application passwords. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php index 767917d6f6fd0..185c8fab1d6e7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php @@ -52,6 +52,7 @@ public function register_routes() { 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_items' ), 'permission_callback' => array( $this, 'delete_items_permissions_check' ), + 'args' => $this->get_delete_collection_params(), ), 'schema' => array( $this, 'get_public_item_schema' ), ) @@ -385,6 +386,7 @@ public function delete_items_permissions_check( $request ) { * Deletes all application passwords for a user. * * @since 5.6.0 + * @since 7.1.0 Supports deleting application passwords by app ID. * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. @@ -396,7 +398,11 @@ public function delete_items( $request ) { return $user; } - $deleted = WP_Application_Passwords::delete_all_application_passwords( $user->ID ); + if ( ! empty( $request['app_id'] ) ) { + $deleted = WP_Application_Passwords::delete_application_passwords_by_app_id( $user->ID, $request['app_id'] ); + } else { + $deleted = WP_Application_Passwords::delete_all_application_passwords( $user->ID ); + } if ( is_wp_error( $deleted ) ) { return $deleted; @@ -410,6 +416,25 @@ public function delete_items( $request ) { ); } + /** + * Retrieves the query params for deleting a collection of application passwords. + * + * @since 7.1.0 + * + * @return array Query parameters for the delete collection endpoint. + */ + protected function get_delete_collection_params() { + return array( + 'app_id' => array( + 'description' => __( 'The app ID of application passwords to delete.' ), + 'type' => 'string', + 'format' => 'uuid', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + ); + } + /** * Checks if a given request has access to delete a specific application password for a user. * diff --git a/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php b/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php index 060a5c0912a94..e8cca01c18909 100644 --- a/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php +++ b/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php @@ -794,6 +794,97 @@ public function test_delete_items_invalid_user_id() { $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 ); } + /** + * @ticket 61644 + */ + public function test_delete_items_by_app_id() { + wp_set_current_user( self::$admin ); + + $app_id = wp_generate_uuid4(); + + list( , $first_item ) = WP_Application_Passwords::create_new_application_password( + self::$admin, + array( + 'name' => 'App 1', + 'app_id' => $app_id, + ) + ); + list( , $second_item ) = WP_Application_Passwords::create_new_application_password( + self::$admin, + array( + 'name' => 'App 2', + 'app_id' => $app_id, + ) + ); + list( , $third_item ) = WP_Application_Passwords::create_new_application_password( + self::$admin, + array( + 'name' => 'App 3', + 'app_id' => wp_generate_uuid4(), + ) + ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/users/me/application-passwords' ); + $request->set_query_params( + array( + 'app_id' => $app_id, + ) + ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertTrue( $response->get_data()['deleted'] ); + $this->assertSame( 2, $response->get_data()['count'] ); + $this->assertNull( WP_Application_Passwords::get_user_application_password( self::$admin, $first_item['uuid'] ) ); + $this->assertNull( WP_Application_Passwords::get_user_application_password( self::$admin, $second_item['uuid'] ) ); + $this->assertSame( $third_item, WP_Application_Passwords::get_user_application_password( self::$admin, $third_item['uuid'] ) ); + } + + /** + * @ticket 61644 + */ + public function test_delete_items_by_app_id_with_no_matches() { + wp_set_current_user( self::$admin ); + + list( , $item ) = WP_Application_Passwords::create_new_application_password( + self::$admin, + array( + 'name' => 'App 1', + 'app_id' => wp_generate_uuid4(), + ) + ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/users/me/application-passwords' ); + $request->set_query_params( + array( + 'app_id' => wp_generate_uuid4(), + ) + ); + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertTrue( $response->get_data()['deleted'] ); + $this->assertSame( 0, $response->get_data()['count'] ); + $this->assertSame( $item, WP_Application_Passwords::get_user_application_password( self::$admin, $item['uuid'] ) ); + } + + /** + * @ticket 61644 + */ + public function test_delete_items_by_app_id_rejects_invalid_app_id() { + wp_set_current_user( self::$admin ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/users/me/application-passwords' ); + $request->set_query_params( + array( + 'app_id' => 'not-a-uuid', + ) + ); + $response = rest_do_request( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + /** * @ticket 42790 */