id = 'albert/update-user'; $this->label = __( 'Update User', 'albert-ai-butler' ); $this->description = __( 'Update an existing WordPress user with new information.', 'albert-ai-butler' ); $this->category = 'user'; $this->group = 'users'; $this->input_schema = $this->get_input_schema(); $this->output_schema = $this->get_output_schema(); $this->meta = [ 'mcp' => [ 'public' => true, ], 'annotations' => Annotations::update( 'This ability cannot change a password and will refuse the request if you supply one. ' . 'Changing an existing account\'s password is account takeover, and the account holder can ' . 'reset their own from the login screen. Tell the user to do that instead.' ), ]; parent::__construct(); } /** * Get the input schema for this ability. * * @return array Input schema. * @since 1.0.0 */ protected function get_input_schema(): array { // Get all available WordPress roles dynamically. $wp_roles = wp_roles(); $role_names = array_keys( $wp_roles->roles ); return [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'description' => 'The user ID to update', ], 'email' => [ 'type' => 'string', 'format' => 'email', 'description' => 'The email address for the user', ], 'first_name' => [ 'type' => 'string', 'description' => 'User first name', ], 'last_name' => [ 'type' => 'string', 'description' => 'User last name', ], 'roles' => [ 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => $role_names, ], 'description' => 'User roles', ], 'url' => [ 'type' => 'string', 'format' => 'uri', 'description' => 'User website URL', ], 'description' => [ 'type' => 'string', 'description' => 'User biographical info', ], ], 'required' => [ 'id' ], ]; } /** * Get the output schema for this ability. * * @return array Output schema. * @since 1.0.0 */ protected function get_output_schema(): array { return [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer' ], 'username' => [ 'type' => 'string' ], 'email' => [ 'type' => 'string' ], 'roles' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], ], 'edit_url' => [ 'type' => 'string' ], ], 'required' => [ 'id', 'username', 'email' ], ]; } /** * Check if current user has permission to execute this ability. * * Delegates to the REST API endpoint's own permission callback. * * @return bool|WP_Error True if permitted, WP_Error with details otherwise. * @since 1.0.0 */ public function check_permission(): bool|WP_Error { return $this->check_rest_permission( '/wp/v2/users/(?P[\\d]+)', 'POST', 'edit_users' ); } /** * Execute the ability - update a user using WordPress REST API. * * @param array $args { * Input parameters. * * @type int $id User ID (required). * @type string $email Email address. * @type string $first_name First name. * @type string $last_name Last name. * @type array $roles User roles. * @type string $url Website URL. * @type string $description Biographical info. * } * @return array|WP_Error User data on success, WP_Error on failure. * @since 1.0.0 */ public function execute( array $args ): array|WP_Error { // Belt and braces. `password` is gone from the input schema, so a // schema-validating client is already refused, but the Abilities API // does not forbid unrecognised keys, and a refusal that only exists in // a schema is not a control. Refused here with its own error code so // the attempt lands in the activity log rather than being swallowed as // a validation rejection, which is the one thing worth seeing. if ( isset( $args['password'] ) ) { return new WP_Error( 'password_change_refused', __( 'This ability cannot change a password. The account holder can reset their own from the login screen.', 'albert-ai-butler' ), [ 'status' => 400 ] ); } $user_id = absint( $args['id'] ); // Check if user exists. if ( ! get_userdata( $user_id ) ) { return new WP_Error( 'user_not_found', __( 'User not found.', 'albert-ai-butler' ), [ 'status' => 404 ] ); } // Prepare REST API request data (only include provided fields). $request_data = []; if ( isset( $args['email'] ) ) { $request_data['email'] = sanitize_email( $args['email'] ); } if ( isset( $args['first_name'] ) ) { $request_data['first_name'] = sanitize_text_field( $args['first_name'] ); } if ( isset( $args['last_name'] ) ) { $request_data['last_name'] = sanitize_text_field( $args['last_name'] ); } if ( isset( $args['roles'] ) && is_array( $args['roles'] ) ) { $request_data['roles'] = array_map( 'sanitize_key', $args['roles'] ); } if ( isset( $args['url'] ) ) { $request_data['url'] = esc_url_raw( $args['url'] ); } if ( isset( $args['description'] ) ) { $request_data['description'] = sanitize_textarea_field( $args['description'] ); } // Create REST request. $request = new WP_REST_Request( 'POST', '/wp/v2/users/' . $user_id ); foreach ( $request_data as $key => $value ) { $request->set_param( $key, $value ); } // Execute the request. $response = rest_do_request( $request ); $server = rest_get_server(); $data = $server->response_to_data( $response, false ); // Check for errors. if ( is_wp_error( $data ) ) { return $data; } if ( $response->is_error() ) { return new WP_Error( $data['code'] ?? 'rest_error', $data['message'] ?? __( 'An error occurred while updating the user.', 'albert-ai-butler' ), [ 'status' => $response->get_status() ] ); } // Return formatted user data. return [ 'id' => $data['id'], 'username' => $data['slug'] ?? '', 'email' => $data['email'] ?? '', 'roles' => $data['roles'] ?? [], 'edit_url' => admin_url( 'user-edit.php?user_id=' . $data['id'] ), ]; } }