diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 26f462e4f3..5854539a37 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -368,10 +368,11 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { $data = $this->filter_response_by_context( $data, $context ); + $links = $response->get_links(); + // Wrap the data in a response object. $response = rest_ensure_response( $data ); - - $response->add_links( $this->prepare_links( $post ) ); + $response->add_links( $links ); /** * Filters an attachment returned from the REST API. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index b7b2dd79f6..7cd6497c0b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -1590,7 +1590,18 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { // Wrap the data in a response object. $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $post ) ); + $links = $this->prepare_links( $post ); + $response->add_links( $links ); + + if ( ! empty( $links['self']['href'] ) ) { + $actions = $this->get_available_actions( $post, $request ); + + $self = $links['self']['href']; + + foreach ( $actions as $rel ) { + $response->add_link( $rel, $self ); + } + } /** * Filters the post data for a response. @@ -1729,6 +1740,60 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { return $links; } + /** + * Get the link relations available for the post and current user. + * + * @since 4.9.7 + * + * @param WP_Post $post Post object. + * @param WP_REST_Request Request object. + * + * @return array List of link relations. + */ + protected function get_available_actions( $post, $request ) { + + if ( 'edit' !== $request['context'] ) { + return array(); + } + + $rels = array(); + + $post_type = get_post_type_object( $post->post_type ); + + if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) { + $rels[] = 'https://api.w.org/action-publish'; + } + + if ( 'post' === $post_type->name ) { + if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) { + $rels[] = 'https://api.w.org/action-sticky'; + } + } + + if ( post_type_supports( $post_type->name, 'author' ) ) { + if ( current_user_can( $post_type->cap->edit_others_posts ) ) { + $rels[] = 'https://api.w.org/action-assign-author'; + } + } + + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $tax ) { + $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; + $create_cap = is_taxonomy_hierarchical( $tax->name ) ? $tax->cap->edit_terms : $tax->cap->assign_terms; + + if ( current_user_can( $create_cap ) ) { + $rels[] = 'https://api.w.org/action-create-' . $tax_base; + } + + if ( current_user_can( $tax->cap->assign_terms ) ) { + $rels[] = 'https://api.w.org/action-assign-' . $tax_base; + } + } + + return $rels; + } + /** * Retrieves the post's schema, conforming to JSON Schema. * @@ -2069,9 +2134,125 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { ); } + $schema_links = $this->get_schema_links(); + + if ( $schema_links ) { + $schema['links'] = $schema_links; + } + return $this->add_additional_fields_schema( $schema ); } + /** + * Retrieve Link Description Objects that should be added to the Schema for the posts collection. + * + * @since 4.9.7 + * + * @return array + */ + protected function get_schema_links() { + + $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" ); + + $links = array(); + + if ( 'attachment' !== $this->post_type ) { + $links[] = array( + 'rel' => 'https://api.w.org/action-publish', + 'title' => __( 'The current user can publish this post.' ), + 'href' => $href, + 'targetSchema' => array( + 'type' => 'object', + 'properties' => array( + 'status' => array( + 'type' => 'string', + 'enum' => array( 'publish', 'future' ), + ), + ), + ), + ); + } + + if ( 'post' === $this->post_type ) { + $links[] = array( + 'rel' => 'https://api.w.org/action-sticky', + 'title' => __( 'The current user can sticky this post.' ), + 'href' => $href, + 'targetSchema' => array( + 'type' => 'object', + 'properties' => array( + 'sticky' => array( + 'type' => 'boolean', + ), + ), + ), + ); + } + + if ( post_type_supports( $this->post_type, 'author' ) ) { + $links[] = array( + 'rel' => 'https://api.w.org/action-assign-author', + 'title' => __( 'The current user can change the author on this post.' ), + 'href' => $href, + 'targetSchema' => array( + 'type' => 'object', + 'properties' => array( + 'author' => array( + 'type' => 'integer', + ), + ), + ), + ); + } + + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $tax ) { + $tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name; + + /* translators: %s: taxonomy name */ + $assign_title = sprintf( __( 'The current user can assign terms in the %s taxonomy.' ), $tax->name ); + /* translators: %s: taxonomy name */ + $create_title = sprintf( __( 'The current user can create terms in the %s taxonomy.' ), $tax->name ); + + $links[] = array( + 'rel' => 'https://api.w.org/action-assign-' . $tax_base, + 'title' => $assign_title, + 'href' => $href, + 'targetSchema' => array( + 'type' => 'object', + 'properties' => array( + $tax_base => array( + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ), + ), + ), + ); + + $links[] = array( + 'rel' => 'https://api.w.org/action-create-' . $tax_base, + 'title' => $create_title, + 'href' => $href, + 'targetSchema' => array( + 'type' => 'object', + 'properties' => array( + $tax_base => array( + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ), + ), + ), + ); + } + + return $links; + } + /** * Retrieves the query params for the posts collection. * diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index c42e40f194..8803906f38 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -1350,6 +1350,50 @@ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Control } } + public function test_links_exist() { + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->attachment->create( array( 'post_author' => self::$editor_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'self', $links ); + } + + public function test_publish_action_ldo_not_registered() { + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-publish' ) ); + + $this->assertCount( 0, $publish, 'LDO not found on schema.' ); + } + + public function test_publish_action_link_does_not_exists() { + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->attachment->create( array( 'post_author' => self::$editor_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links ); + } + public function tearDown() { parent::tearDown(); if ( file_exists( $this->test_file ) ) { diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 721b08292a..c01ca6cd51 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -3640,6 +3640,312 @@ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te update_post_meta( $post->ID, 'my_custom_int', $value ); } + public function test_publish_action_ldo_registered() { + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-publish' ) ); + + $this->assertCount( 1, $publish, 'LDO found on schema.' ); + } + + public function test_sticky_action_ldo_registered_for_posts() { + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-sticky' ) ); + + $this->assertCount( 1, $publish, 'LDO found on schema.' ); + } + + public function test_sticky_action_ldo_not_registered_for_non_posts() { + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-sticky' ) ); + + $this->assertCount( 0, $publish, 'LDO found on schema.' ); + } + + public function test_author_action_ldo_registered_for_post_types_with_author_support() { + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-assign-author' ) ); + + $this->assertCount( 1, $publish, 'LDO found on schema.' ); + } + + public function test_author_action_ldo_not_registered_for_post_types_without_author_support() { + + remove_post_type_support( 'post', 'author' ); + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $publish = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-assign-author' ) ); + + $this->assertCount( 0, $publish, 'LDO found on schema.' ); + } + + public function test_term_action_ldos_registered() { + + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $rels = array_flip( wp_list_pluck( $schema['links'], 'rel' ) ); + + $this->assertArrayHasKey( 'https://api.w.org/action-assign-categories', $rels ); + $this->assertArrayHasKey( 'https://api.w.org/action-create-categories', $rels ); + $this->assertArrayHasKey( 'https://api.w.org/action-assign-tags', $rels ); + $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $rels ); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-post_format', $rels ); + $this->assertArrayNotHasKey( 'https://api.w.org/action-create-post_format', $rels ); + $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-nav_menu', $rels ); + $this->assertArrayNotHasKey( 'https://api.w.org/action-create-nav_menu', $rels ); + } + + public function test_action_links_only_available_in_edit_context() { + + wp_set_current_user( self::$author_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'view' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links ); + } + + public function test_publish_action_link_exists_for_author() { + + wp_set_current_user( self::$author_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'https://api.w.org/action-publish', $links ); + } + + public function test_publish_action_link_does_not_exist_for_contributor() { + + wp_set_current_user( self::$contributor_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$contributor_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links ); + } + + public function test_sticky_action_exists_for_editor() { + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'https://api.w.org/action-sticky', $links ); + } + + public function test_sticky_action_does_not_exist_for_author() { + + wp_set_current_user( self::$author_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-sticky', $links ); + } + + public function test_sticky_action_does_not_exist_for_non_post_posts() { + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->post->create( + array( + 'post_author' => self::$author_id, + 'post_type' => 'page', + ) + ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-sticky', $links ); + } + + + public function test_assign_author_action_exists_for_editor() { + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'https://api.w.org/action-assign-author', $links ); + } + + public function test_assign_author_action_does_not_exist_for_author() { + + wp_set_current_user( self::$author_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-author', $links ); + } + + public function test_assign_author_action_does_not_exist_for_post_types_without_author_support() { + + remove_post_type_support( 'post', 'author' ); + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->post->create(); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-assign-author', $links ); + } + + public function test_create_term_action_exists_for_editor() { + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'https://api.w.org/action-create-categories', $links ); + $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $links ); + $this->assertArrayNotHasKey( 'https://api.w.org/action-create-post_format', $links ); + } + + public function test_create_term_action_non_hierarchical_exists_for_author() { + + wp_set_current_user( self::$author_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'https://api.w.org/action-create-tags', $links ); + } + + public function test_create_term_action_hierarchical_does_not_exists_for_author() { + + wp_set_current_user( self::$author_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-create-categories', $links ); + } + + public function test_assign_term_action_exists_for_contributor() { + + wp_set_current_user( self::$contributor_id ); + + $post = self::factory()->post->create( + array( + 'post_author' => self::$contributor_id, + 'post_status' => 'draft', + ) + ); + $this->assertGreaterThan( 0, $post ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'https://api.w.org/action-assign-categories', $links ); + $this->assertArrayHasKey( 'https://api.w.org/action-assign-tags', $links ); + } + public function tearDown() { _unregister_post_type( 'youseeeme' ); if ( isset( $this->attachment_id ) ) {