diff --git a/src/js/_enqueues/admin/auth-app.js b/src/js/_enqueues/admin/auth-app.js index 808bb0754b..3152ed3c5c 100644 --- a/src/js/_enqueues/admin/auth-app.js +++ b/src/js/_enqueues/admin/auth-app.js @@ -16,7 +16,8 @@ }; $approveBtn.click( function( e ) { - var name = $appNameField.val(); + var name = $appNameField.val(), + appId = $( 'input[name="app_id"]', $form ).val(); e.preventDefault(); @@ -32,6 +33,10 @@ name: name }; + if ( appId.length > 0 ) { + request.app_id = appId; + } + /** * Filters the request data used to Authorize an Application Password request. * diff --git a/src/wp-admin/authorize-application.php b/src/wp-admin/authorize-application.php index 88804b7d74..b9cc0c0aac 100644 --- a/src/wp-admin/authorize-application.php +++ b/src/wp-admin/authorize-application.php @@ -18,6 +18,7 @@ if ( isset( $_POST['action'] ) && 'authorize_application_password' === $_POST['a $success_url = $_POST['success_url']; $reject_url = $_POST['reject_url']; $app_name = $_POST['app_name']; + $app_id = $_POST['app_id']; $redirect = ''; if ( isset( $_POST['reject'] ) ) { @@ -27,7 +28,13 @@ if ( isset( $_POST['action'] ) && 'authorize_application_password' === $_POST['a $redirect = admin_url(); } } elseif ( isset( $_POST['approve'] ) ) { - $created = WP_Application_Passwords::create_new_application_password( get_current_user_id(), array( 'name' => $app_name ) ); + $created = WP_Application_Passwords::create_new_application_password( + get_current_user_id(), + array( + 'name' => $app_name, + 'app_id' => $app_id, + ) + ); if ( is_wp_error( $created ) ) { $error = $created; @@ -56,6 +63,7 @@ if ( isset( $_POST['action'] ) && 'authorize_application_password' === $_POST['a $title = __( 'Authorize Application' ); $app_name = ! empty( $_REQUEST['app_name'] ) ? $_REQUEST['app_name'] : ''; +$app_id = ! empty( $_REQUEST['app_id'] ) ? $_REQUEST['app_id'] : ''; $success_url = ! empty( $_REQUEST['success_url'] ) ? $_REQUEST['success_url'] : null; if ( ! empty( $_REQUEST['reject_url'] ) ) { @@ -68,7 +76,7 @@ if ( ! empty( $_REQUEST['reject_url'] ) ) { $user = wp_get_current_user(); -$request = compact( 'app_name', 'success_url', 'reject_url' ); +$request = compact( 'app_name', 'app_id', 'success_url', 'reject_url' ); $is_valid = wp_is_authorize_application_password_request_valid( $request, $user ); if ( is_wp_error( $is_valid ) ) { @@ -183,6 +191,7 @@ require_once ABSPATH . 'wp-admin/admin-header.php';
+ diff --git a/src/wp-admin/includes/user.php b/src/wp-admin/includes/user.php index d519ee75bc..a08fe3f4b2 100644 --- a/src/wp-admin/includes/user.php +++ b/src/wp-admin/includes/user.php @@ -604,6 +604,7 @@ Please click the following link to activate your user account: * The array of request data. All arguments are optional and may be empty. * * @type string $app_name The suggested name of the application. + * @type string $app_id A uuid provided by the application to uniquely identify it. * @type string $success_url The url the user will be redirected to after approving the application. * @type string $reject_url The url the user will be redirected to after rejecting the application. * } @@ -635,6 +636,13 @@ function wp_is_authorize_application_password_request_valid( $request, $user ) { } } + if ( ! empty( $request['app_id'] ) && ! wp_is_uuid( $request['app_id'] ) ) { + $error->add( + 'invalid_app_id', + __( 'The app id must be a uuid.' ) + ); + } + /** * Fires before application password errors are returned. * diff --git a/src/wp-includes/class-wp-application-passwords.php b/src/wp-includes/class-wp-application-passwords.php index 876ffbed18..4500a571d8 100644 --- a/src/wp-includes/class-wp-application-passwords.php +++ b/src/wp-includes/class-wp-application-passwords.php @@ -51,6 +51,7 @@ class WP_Application_Passwords { $new_item = array( 'uuid' => wp_generate_uuid4(), + 'app_id' => empty( $args['app_id'] ) ? '' : $args['app_id'], 'name' => $args['name'], 'password' => $hashed_password, 'created' => time(), 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 fdb496b412..123722a793 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 @@ -412,6 +412,10 @@ class WP_REST_Application_Passwords_Controller extends WP_REST_Controller { 'name' => $request['name'], ); + if ( $request['app_id'] && ! $request['uuid'] ) { + $prepared->app_id = $request['app_id']; + } + /** * Filters an application password before it is inserted via the REST API. * @@ -441,6 +445,7 @@ class WP_REST_Application_Passwords_Controller extends WP_REST_Controller { $prepared = array( 'uuid' => $item['uuid'], + 'app_id' => empty( $item['app_id'] ) ? '' : $item['app_id'], 'name' => $item['name'], 'created' => gmdate( 'Y-m-d\TH:i:s', $item['created'] ), 'last_used' => $item['last_used'] ? gmdate( 'Y-m-d\TH:i:s', $item['last_used'] ) : null, @@ -615,6 +620,12 @@ class WP_REST_Application_Passwords_Controller extends WP_REST_Controller { 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ), + 'app_id' => array( + 'description' => __( 'A uuid provided by the application to uniquely identify it. It is recommended to use an UUID v5 with the URL or DNS namespace.' ), + 'type' => 'string', + 'format' => 'uuid', + 'context' => array( 'view', 'edit', 'embed' ), + ), 'name' => array( 'description' => __( 'The name of the application password.' ), 'type' => 'string', 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 c799634e27..33d21e6a20 100644 --- a/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php +++ b/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php @@ -308,8 +308,14 @@ class WP_Test_REST_Application_Passwords_Controller extends WP_Test_REST_Control public function test_create_item() { wp_set_current_user( self::$admin ); + $app_id = wp_generate_uuid4(); $request = new WP_REST_Request( 'POST', '/wp/v2/users/me/application-passwords' ); - $request->set_body_params( array( 'name' => 'App' ) ); + $request->set_body_params( + array( + 'name' => 'App', + 'app_id' => $app_id, + ) + ); $response = rest_do_request( $request ); $this->assertEquals( 201, $response->get_status() ); @@ -318,6 +324,7 @@ class WP_Test_REST_Application_Passwords_Controller extends WP_Test_REST_Control $this->assertCount( 1, $passwords ); $this->check_response( $response->get_data(), $passwords[0], true ); $this->assertEquals( 'App', $response->get_data()['name'] ); + $this->assertEquals( $app_id, $response->get_data()['app_id'] ); $this->assertNull( $response->get_data()['last_used'] ); $this->assertNull( $response->get_data()['last_ip'] ); } @@ -513,6 +520,36 @@ class WP_Test_REST_Application_Passwords_Controller extends WP_Test_REST_Control $this->assertErrorResponse( 'rest_application_password_not_found', $response, 404 ); } + /** + * @ticket 51583 + */ + public function test_update_item_cannot_overwrite_app_id() { + wp_set_current_user( self::$admin ); + list( , $item ) = WP_Application_Passwords::create_new_application_password( self::$admin, array( 'name' => 'App' ) ); + + $uuid = $item['uuid']; + $request = new WP_REST_Request( 'PUT', '/wp/v2/users/me/application-passwords/' . $uuid ); + $request->set_body_params( array( 'app_id' => wp_generate_uuid4() ) ); + $response = rest_do_request( $request ); + $this->assertEquals( '', $response->get_data()['app_id'] ); + + $app_id = wp_generate_uuid4(); + + list( , $item ) = WP_Application_Passwords::create_new_application_password( + self::$admin, + array( + 'name' => 'App', + 'app_id' => $app_id, + ) + ); + + $uuid = $item['uuid']; + $request = new WP_REST_Request( 'PUT', '/wp/v2/users/me/application-passwords/' . $uuid ); + $request->set_body_params( array( 'app_id' => wp_generate_uuid4() ) ); + $response = rest_do_request( $request ); + $this->assertEquals( $app_id, $response->get_data()['app_id'] ); + } + /** * @ticket 42790 */ @@ -775,12 +812,14 @@ class WP_Test_REST_Application_Passwords_Controller extends WP_Test_REST_Control */ protected function check_response( $response, $item, $password = false ) { $this->assertArrayHasKey( 'uuid', $response ); + $this->assertArrayHasKey( 'app_id', $response ); $this->assertArrayHasKey( 'name', $response ); $this->assertArrayHasKey( 'created', $response ); $this->assertArrayHasKey( 'last_used', $response ); $this->assertArrayHasKey( 'last_ip', $response ); $this->assertEquals( $item['uuid'], $response['uuid'] ); + $this->assertEquals( $item['app_id'], $response['app_id'] ); $this->assertEquals( $item['name'], $response['name'] ); $this->assertEquals( gmdate( 'Y-m-d\TH:i:s', $item['created'] ), $response['created'] ); @@ -812,12 +851,13 @@ class WP_Test_REST_Application_Passwords_Controller extends WP_Test_REST_Control $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 6, $properties ); $this->assertArrayHasKey( 'uuid', $properties ); + $this->assertArrayHasKey( 'app_id', $properties ); $this->assertArrayHasKey( 'name', $properties ); $this->assertArrayHasKey( 'password', $properties ); $this->assertArrayHasKey( 'created', $properties ); $this->assertArrayHasKey( 'last_used', $properties ); $this->assertArrayHasKey( 'last_ip', $properties ); + $this->assertCount( 7, $properties ); } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index b6b5768f5c..619115e58b 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -4917,6 +4917,11 @@ mockedApiResponse.Schema = { "POST" ], "args": { + "app_id": { + "description": "A machine-readable string provided by the application to uniquely identify it.", + "type": "string", + "required": false + }, "name": { "description": "The name of the application password.", "type": "string", @@ -4967,6 +4972,11 @@ mockedApiResponse.Schema = { "PATCH" ], "args": { + "app_id": { + "description": "A machine-readable string provided by the application to uniquely identify it.", + "type": "string", + "required": false + }, "name": { "description": "The name of the application password.", "type": "string",