diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index de6eff84b7549..d626edf9e05a1 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -90,6 +90,19 @@ final class WP_Comment_Type { */ public $internal = false; + /** + * Whether to include this comment type in the REST API. + * + * Comment types with this enabled are exposed by the read-only + * `/wp/v2/comment-types` discovery endpoint (name, slug, description, + * labels). It does not affect whether comments of this type are readable + * or queryable via `/wp/v2/comments`. Default is the value of $public. + * + * @since 7.1.0 + * @var bool + */ + public $show_in_rest; + /** * Whether this comment type is a native or "built-in" comment type. * @@ -183,15 +196,27 @@ public function set_props( $args ) { * treated as a provided value and overwrite the default name with false. */ $defaults = array( - 'labels' => array(), - 'description' => '', - 'public' => true, - 'internal' => false, - '_builtin' => false, + 'labels' => array(), + 'description' => '', + 'public' => true, + 'internal' => false, + 'show_in_rest' => null, + '_builtin' => false, ); $args = array_merge( $defaults, $args ); + /* + * If not set, default 'show_in_rest' to the setting for 'public'. This + * deliberately diverges from WP_Post_Type, which defaults it to false only + * for backward compatibility with post types registered before the REST API + * existed. A new registry has no such constraint, and 'show_in_rest' gates + * only the read-only comment-types discovery endpoint. + */ + if ( null === $args['show_in_rest'] ) { + $args['show_in_rest'] = $args['public']; + } + $args['name'] = $this->name; foreach ( $args as $property_name => $property_value ) { diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index b01a5c3c4bd8e..85303be94bead 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -361,19 +361,24 @@ function create_initial_comment_types() { * @param array|string $args { * Optional. Array or string of arguments for registering a comment type. Default empty array. * - * @type string $label Name of the comment type. Usually plural. - * Default is the value of $labels['name']. - * @type string[] $labels An array of labels for this comment type. If not set, the - * default comment labels are used. See get_comment_type_labels() - * for a full list of supported labels. - * @type string $description A short descriptive summary of what the comment type is. - * Default empty. - * @type bool $public Whether the comment type is intended for use publicly either via - * the admin interface or by front-end users. Core does not - * currently act on this argument. Default true. - * @type bool $internal Whether the comment type is for internal use only. Core does not - * currently consult this flag; it is intended to drive default - * query exclusions in the future. Default false. + * @type string $label Name of the comment type. Usually plural. + * Default is the value of $labels['name']. + * @type string[] $labels An array of labels for this comment type. If not set, the + * default comment labels are used. See get_comment_type_labels() + * for a full list of supported labels. + * @type string $description A short descriptive summary of what the comment type is. + * Default empty. + * @type bool $public Whether the comment type is intended for use publicly either via + * the admin interface or by front-end users. Core does not + * currently act on this argument. Default true. + * @type bool $internal Whether the comment type is for internal use only. Core does not + * currently consult this flag; it is intended to drive default + * query exclusions in the future. Default false. + * @type bool $show_in_rest Whether to expose this comment type in the REST API comment types + * discovery endpoint (`/wp/v2/comment-types`). Exposing a type + * reveals its name, slug, description, and labels only; it does not + * make comments of this type readable or queryable via + * `/wp/v2/comments`. Default is the value of $public. * } * @return WP_Comment_Type|WP_Error The registered comment type object on success, * WP_Error object on failure. diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index a4c22e8f1cca1..c49202811a8f1 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -324,6 +324,10 @@ function create_initial_rest_routes() { $controller = new WP_REST_Comments_Controller(); $controller->register_routes(); + // Comment types. + $controller = new WP_REST_Comment_Types_Controller(); + $controller->register_routes(); + $search_handlers = array( new WP_REST_Post_Search_Handler(), new WP_REST_Term_Search_Handler(), diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php new file mode 100644 index 0000000000000..0ffee5f97a50c --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php @@ -0,0 +1,322 @@ +namespace = 'wp/v2'; + $this->rest_base = 'comment-types'; + } + + /** + * Registers the routes for comment types. + * + * @since 7.1.0 + * + * @see register_rest_route() + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'type' => array( + 'description' => __( 'An alphanumeric identifier for the comment type.' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks whether a given request has permission to read comment types. + * + * @since 7.1.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { + return new WP_Error( + 'rest_cannot_view', + __( 'Sorry, you are not allowed to edit comments.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Retrieves comment types exposed in the REST API. + * + * @since 7.1.0 + * + * @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. + */ + public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response( array() ); + } + + $data = array(); + $types = get_comment_types( array( 'show_in_rest' => true ), 'objects' ); + + /* + * Once per-comment-type capabilities exist, edit-context collection requests + * should skip types the current user cannot moderate here, rather than + * relying solely on the global 'moderate_comments' gate in + * get_items_permissions_check() (matching how the post types controller + * hides non-editable types from edit-context collections). + */ + foreach ( $types as $type ) { + $comment_type = $this->prepare_item_for_response( $type, $request ); + $data[ $type->name ] = $this->prepare_response_for_collection( $comment_type ); + } + + return rest_ensure_response( $data ); + } + + /** + * Checks if a given request has access to read a comment type. + * + * @since 7.1.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { + return new WP_Error( + 'rest_forbidden_context', + __( 'Sorry, you are not allowed to edit comments.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Retrieves a specific comment type. + * + * @since 7.1.0 + * + * @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. + */ + public function get_item( $request ) { + $obj = get_comment_type_object( $request['type'] ); + + if ( empty( $obj ) ) { + return new WP_Error( + 'rest_type_invalid', + __( 'Invalid comment type.' ), + array( 'status' => 404 ) + ); + } + + if ( empty( $obj->show_in_rest ) ) { + return new WP_Error( + 'rest_cannot_read_type', + __( 'Cannot view comment type.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + $data = $this->prepare_item_for_response( $obj, $request ); + + return rest_ensure_response( $data ); + } + + /** + * Prepares a comment type object for serialization. + * + * @since 7.1.0 + * + * @param WP_Comment_Type $item Comment type object. + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + // Restores the more descriptive, specific name for use within this method. + $comment_type = $item; + + // Don't prepare the response body for HEAD requests. + if ( $request->is_method( 'HEAD' ) ) { + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php */ + return apply_filters( 'rest_prepare_comment_type', new WP_REST_Response( array() ), $comment_type, $request ); + } + + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'description', $fields ) ) { + $data['description'] = $comment_type->description; + } + + if ( rest_is_field_included( 'labels', $fields ) ) { + $data['labels'] = $comment_type->labels; + } + + if ( rest_is_field_included( 'name', $fields ) ) { + $data['name'] = $comment_type->label; + } + + if ( rest_is_field_included( 'slug', $fields ) ) { + $data['slug'] = $comment_type->name; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $response->add_links( $this->prepare_links( $comment_type ) ); + } + + /** + * Filters a comment type returned from the REST API. + * + * Allows modification of the comment type data right before it is returned. + * + * @since 7.1.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment_Type $comment_type The original comment type object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'rest_prepare_comment_type', $response, $comment_type, $request ); + } + + /** + * Prepares links for the request. + * + * @since 7.1.0 + * + * @param WP_Comment_Type $comment_type The comment type. + * @return array Links for the given comment type. + */ + protected function prepare_links( $comment_type ) { + return array( + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + 'https://api.w.org/items' => array( + 'href' => add_query_arg( 'type', $comment_type->name, rest_url( 'wp/v2/comments' ) ), + ), + ); + } + + /** + * Retrieves the comment type's schema, conforming to JSON Schema. + * + * @since 7.1.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'comment-type', + 'type' => 'object', + 'properties' => array( + 'description' => array( + 'description' => __( 'A human-readable description of the comment type.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'labels' => array( + 'description' => __( 'Human-readable labels for the comment type for various contexts.' ), + 'type' => 'object', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'The title for the comment type.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the comment type.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the query params for collections. + * + * @since 7.1.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 9ec2c3607271d..56869e68e6c40 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -334,6 +334,7 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-locations-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php'; +require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comment-types-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-search-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-blocks-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-types-controller.php'; diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index bd824984fa7bb..b07ff5cc5091c 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -75,6 +75,36 @@ public function test_register_comment_type_with_empty_name() { $this->assertInstanceOf( 'WP_Error', register_comment_type( '' ) ); } + /** + * @ticket 35214 + * + * @covers ::register_comment_type + */ + public function test_register_comment_type_show_in_rest_should_default_to_value_of_public() { + register_comment_type( 'public_type', array( 'public' => true ) ); + $this->assertTrue( get_comment_type_object( 'public_type' )->show_in_rest ); + + register_comment_type( 'private_type', array( 'public' => false ) ); + $this->assertFalse( get_comment_type_object( 'private_type' )->show_in_rest ); + } + + /** + * @ticket 35214 + * + * @covers ::register_comment_type + */ + public function test_register_comment_type_show_in_rest_can_be_overridden() { + register_comment_type( + 'private_but_in_rest', + array( + 'public' => false, + 'show_in_rest' => true, + ) + ); + + $this->assertTrue( get_comment_type_object( 'private_but_in_rest' )->show_in_rest ); + } + /** * @ticket 35214 * diff --git a/tests/phpunit/tests/rest-api/rest-comment-types-controller.php b/tests/phpunit/tests/rest-api/rest-comment-types-controller.php new file mode 100644 index 0000000000000..000fea0044698 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-comment-types-controller.php @@ -0,0 +1,482 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$subscriber_id ); + } + + /** + * @ticket 35214 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/comment-types', $routes ); + $this->assertArrayHasKey( '/wp/v2/comment-types/(?P[\w-]+)', $routes ); + } + + /** + * @ticket 35214 + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSameSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] ); + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSameSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] ); + } + + /** + * @ticket 35214 + */ + public function test_get_items() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + + $data = $response->get_data(); + $comment_types = get_comment_types( array( 'show_in_rest' => true ), 'objects' ); + $this->assertCount( count( $comment_types ), $data ); + $this->assertSame( $comment_types['comment']->name, $data['comment']['slug'] ); + $this->check_comment_type_obj( 'view', $comment_types['comment'], $data['comment'], $data['comment']['_links'] ); + $this->assertArrayHasKey( 'pingback', $data ); + $this->assertArrayHasKey( 'trackback', $data ); + } + + /** + * The internal `note` type is not exposed (show_in_rest defaults to public, which is false). + * + * @ticket 35214 + */ + public function test_get_items_excludes_non_rest_types() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayNotHasKey( 'note', $data ); + } + + /** + * @ticket 35214 + */ + public function test_get_items_includes_registered_custom_type() { + register_comment_type( 'review', array( 'public' => true ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'review', $data ); + } + + /** + * @ticket 35214 + */ + public function test_get_items_excludes_custom_type_opted_out_of_rest() { + register_comment_type( + 'review', + array( + 'public' => true, + 'show_in_rest' => false, + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayNotHasKey( 'review', $data ); + } + + /** + * @ticket 35214 + * + * @dataProvider data_readable_http_methods + * + * @param string $method HTTP method to use. + */ + public function test_get_items_invalid_permission_for_context( $method ) { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( $method, '/wp/v2/comment-types' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @ticket 35214 + */ + public function test_get_item() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $this->check_comment_type_object_response( 'view', $response ); + } + + /** + * @ticket 35214 + * + * @dataProvider data_readable_http_methods + * + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_type( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/comment-types/invalid' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_type_invalid', $response, 404 ); + } + + /** + * @ticket 35214 + */ + public function test_get_item_non_rest_type_is_not_readable() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/note' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read_type', $response, 401 ); + } + + /** + * @ticket 35214 + */ + public function test_get_item_edit_context_requires_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + /** + * An anonymous edit-context request is rejected with 401 before the type is resolved. + * + * This deliberately diverges from the post types controller, which resolves the type + * first and returns 404 for unknown types: here the permission callback runs before + * the route callback, so authentication is always demanded first. + * + * @ticket 35214 + */ + public function test_get_item_edit_context_as_anonymous_returns_401() { + wp_set_current_user( 0 ); + + // A known type. + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); + + // An unknown type: still 401, not 404. + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/unknown' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); + } + + /** + * @ticket 35214 + */ + public function test_get_item_with_fields_limits_response_keys() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $request->set_param( '_fields', 'slug' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( array( 'slug' ), array_keys( $response->get_data() ) ); + } + + /** + * @ticket 35214 + */ + public function test_additional_field_registered_for_comment_type_is_included() { + register_rest_field( + 'comment-type', + 'my_custom_field', + array( + 'get_callback' => static function () { + return 'custom value'; + }, + 'schema' => array( 'type' => 'string' ), + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'my_custom_field', $data ); + $this->assertSame( 'custom value', $data['my_custom_field'] ); + + global $wp_rest_additional_fields; + $wp_rest_additional_fields = array(); + } + + /** + * In the edit context, labels are returned for every exposed type, and the internal + * 'note' type stays hidden even from users who can moderate comments. + * + * @ticket 35214 + */ + public function test_get_items_edit_context_returns_labels_and_still_excludes_note() { + wp_set_current_user( self::$admin_id ); + register_comment_type( + 'review', + array( + 'label' => 'Reviews', + 'public' => true, + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'review', $data ); + $this->assertArrayNotHasKey( 'note', $data ); + + foreach ( $data as $slug => $item ) { + $this->assertArrayHasKey( 'labels', $item, "Labels missing for '{$slug}' in edit context." ); + } + } + + /** + * @ticket 35214 + */ + public function test_get_item_edit_context_returns_labels() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->check_comment_type_object_response( 'edit', $response ); + } + + /** + * @ticket 35214 + */ + public function test_create_item() { + /** Comment types can't be created */ + $request = new WP_REST_Request( 'POST', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 404, $response->get_status() ); + } + + /** + * @ticket 35214 + */ + public function test_update_item() { + /** Comment types can't be updated */ + $request = new WP_REST_Request( 'POST', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 404, $response->get_status() ); + } + + /** + * @ticket 35214 + */ + public function test_delete_item() { + /** Comment types can't be deleted */ + $request = new WP_REST_Request( 'DELETE', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 404, $response->get_status() ); + } + + /** + * @ticket 35214 + */ + public function test_prepare_item() { + $obj = get_comment_type_object( 'comment' ); + $endpoint = new WP_REST_Comment_Types_Controller(); + $request = new WP_REST_Request(); + $request->set_param( 'context', 'view' ); + $response = $endpoint->prepare_item_for_response( $obj, $request ); + $this->check_comment_type_obj( 'view', $obj, $response->get_data(), $response->get_links() ); + } + + /** + * @ticket 35214 + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'labels', $properties ); + } + + /** + * The `api.w.org/items` link should point at the type-filtered comments collection. + * + * @ticket 35214 + */ + public function test_get_item_links_to_filtered_comments_collection() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $links = test_rest_expand_compact_links( $response->get_links() ); + + $this->assertArrayHasKey( 'https://api.w.org/items', $links ); + $this->assertSame( + add_query_arg( 'type', 'comment', rest_url( 'wp/v2/comments' ) ), + $links['https://api.w.org/items'][0]['href'] + ); + } + + /** + * A HEAD request to the collection should succeed without preparing any item data. + * + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_comment_types_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/comment-types' ); + $hook_name = 'rest_prepare_comment_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/comment-types/comment' ); + + $hook_name = 'rest_prepare_comment_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter should be called once.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'slug' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/comment-types/comment' ), + 'get_items request' => array( '/wp/v2/comment-types' ), + ); + } + + protected function check_comment_type_obj( $context, $comment_type_obj, $data, $links ) { + $this->assertSame( $comment_type_obj->label, $data['name'] ); + $this->assertSame( $comment_type_obj->name, $data['slug'] ); + $this->assertSame( $comment_type_obj->description, $data['description'] ); + + $links = test_rest_expand_compact_links( $links ); + $this->assertSame( rest_url( 'wp/v2/comment-types' ), $links['collection'][0]['href'] ); + $this->assertArrayHasKey( 'https://api.w.org/items', $links ); + + if ( 'edit' === $context ) { + $this->assertSame( (array) $comment_type_obj->labels, (array) $data['labels'] ); + } else { + $this->assertArrayNotHasKey( 'labels', $data ); + } + } + + protected function check_comment_type_object_response( $context, $response, $comment_type = 'comment' ) { + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $obj = get_comment_type_object( $comment_type ); + $this->check_comment_type_obj( $context, $obj, $data, $response->get_links() ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 89bf2c481c567..c1aa2977e6829 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -138,6 +138,8 @@ public function test_expected_routes_in_schema() { '/wp/v2/users/(?P(?:[\\d]+|me))/application-passwords/(?P[\\w\\-]+)', '/wp/v2/comments', '/wp/v2/comments/(?P[\\d]+)', + '/wp/v2/comment-types', + '/wp/v2/comment-types/(?P[\\w-]+)', '/wp/v2/global-styles/(?P[\/\d+]+)', '/wp/v2/global-styles/(?P[\d]+)/revisions', '/wp/v2/global-styles/(?P[\d]+)/revisions/(?P[\d]+)', diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index fa03d9751fe99..77dff892703c1 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -22,13 +22,7 @@ mockedApiResponse.Schema = { "wp-block-editor/v1", "wp-abilities/v1" ], - "authentication": { - "application-passwords": { - "endpoints": { - "authorization": "http://example.org/wp-admin/authorize-application.php" - } - } - }, + "authentication": [], "routes": { "/": { "namespace": "", @@ -10823,6 +10817,70 @@ mockedApiResponse.Schema = { } ] }, + "/wp/v2/comment-types": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ], + "_links": { + "self": [ + { + "href": "http://example.org/index.php?rest_route=/wp/v2/comment-types" + } + ] + } + }, + "/wp/v2/comment-types/(?P[\\w-]+)": { + "namespace": "wp/v2", + "methods": [ + "GET" + ], + "endpoints": [ + { + "methods": [ + "GET" + ], + "args": { + "type": { + "description": "An alphanumeric identifier for the comment type.", + "type": "string", + "required": false + }, + "context": { + "description": "Scope under which the request is made; determines fields present in response.", + "type": "string", + "enum": [ + "view", + "embed", + "edit" + ], + "default": "view", + "required": false + } + } + } + ] + }, "/wp/v2/search": { "namespace": "wp/v2", "methods": [