From ecf1d6a158c70dd7f10cc42c4b26ecb0fb22bb59 Mon Sep 17 00:00:00 2001 From: Timothy Jacobs <timothyblynjacobs@git.wordpress.org> Date: Tue, 9 Nov 2021 01:57:48 +0000 Subject: [PATCH] REST API: Add batch support for posts and terms controllers. This also exposes the value of `allow_batch` in `OPTIONS` requests to a route. A future commit will add batch support to more resources. Props spacedmonkey, chrisvanpatten. See #53063. git-svn-id: https://develop.svn.wordpress.org/trunk@52068 602fd350-edb4-49c9-b593-d223f7449a82 --- .../rest-api/class-wp-rest-server.php | 10 +++ .../class-wp-rest-attachments-controller.php | 8 ++ .../class-wp-rest-posts-controller.php | 16 +++- .../class-wp-rest-terms-controller.php | 16 +++- .../class-wp-rest-widgets-controller.php | 12 ++- .../rest-api/rest-attachments-controller.php | 2 + .../rest-api/rest-categories-controller.php | 2 + .../tests/rest-api/rest-pages-controller.php | 3 +- .../tests/rest-api/rest-posts-controller.php | 2 + tests/phpunit/tests/rest-api/rest-server.php | 32 +++++++ .../tests/rest-api/rest-tags-controller.php | 2 + .../rest-api/rest-widgets-controller.php | 2 + tests/qunit/fixtures/wp-api-generated.js | 90 +++++++++++++++++++ 13 files changed, 188 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 3585aa7656b5e..b7451ffc18f90 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1403,6 +1403,8 @@ public function get_data_for_route( $route, $callbacks, $context = 'view' ) { 'endpoints' => array(), ); + $allow_batch = false; + if ( isset( $this->route_options[ $route ] ) ) { $options = $this->route_options[ $route ]; @@ -1410,6 +1412,8 @@ public function get_data_for_route( $route, $callbacks, $context = 'view' ) { $data['namespace'] = $options['namespace']; } + $allow_batch = isset( $options['allow_batch'] ) ? $options['allow_batch'] : false; + if ( isset( $options['schema'] ) && 'help' === $context ) { $data['schema'] = call_user_func( $options['schema'] ); } @@ -1430,6 +1434,12 @@ public function get_data_for_route( $route, $callbacks, $context = 'view' ) { 'methods' => array_keys( $callback['methods'] ), ); + $callback_batch = isset( $callback['allow_batch'] ) ? $callback['allow_batch'] : $allow_batch; + + if ( $callback_batch ) { + $endpoint_data['allow_batch'] = $callback_batch; + } + if ( isset( $callback['args'] ) ) { $endpoint_data['args'] = array(); 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 ad6a89b234c8f..423347a721a07 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 @@ -16,6 +16,14 @@ */ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { + /** + * Whether the controller supports batching. + * + * @since 5.9.0 + * @var false + */ + protected $allow_batch = false; + /** * Registers the routes for attachments. * 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 ffb4466892340..fd7fa22b4c8bc 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 @@ -39,6 +39,14 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { */ protected $password_check_passed = array(); + /** + * Whether the controller supports batching. + * + * @since 5.9.0 + * @var array + */ + protected $allow_batch = array( 'v1' => true ); + /** * Constructor. * @@ -80,7 +88,8 @@ public function register_routes() { 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), - 'schema' => array( $this, 'get_public_item_schema' ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -98,7 +107,7 @@ public function register_routes() { $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( - 'args' => array( + 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the post.' ), 'type' => 'integer', @@ -128,7 +137,8 @@ public function register_routes() { ), ), ), - 'schema' => array( $this, 'get_public_item_schema' ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), ) ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php index 3a92b774f0fd2..8856609375ddd 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php @@ -48,6 +48,14 @@ class WP_REST_Terms_Controller extends WP_REST_Controller { */ protected $total_terms; + /** + * Whether the controller supports batching. + * + * @since 5.9.0 + * @var array + */ + protected $allow_batch = array( 'v1' => true ); + /** * Constructor. * @@ -89,7 +97,8 @@ public function register_routes() { 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), ), - 'schema' => array( $this, 'get_public_item_schema' ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -97,7 +106,7 @@ public function register_routes() { $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( - 'args' => array( + 'args' => array( 'id' => array( 'description' => __( 'Unique identifier for the term.' ), 'type' => 'integer', @@ -129,7 +138,8 @@ public function register_routes() { ), ), ), - 'schema' => array( $this, 'get_public_item_schema' ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), ) ); } diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php index 6385be8ef81dd..8cf7039d1fadb 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php @@ -24,6 +24,14 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { */ protected $widgets_retrieved = false; + /** + * Whether the controller supports batching. + * + * @since 5.9.0 + * @var array + */ + protected $allow_batch = array( 'v1' => true ); + /** * Widgets controller constructor. * @@ -56,7 +64,7 @@ public function register_routes() { 'permission_callback' => array( $this, 'create_item_permissions_check' ), 'args' => $this->get_endpoint_args_for_item_schema(), ), - 'allow_batch' => array( 'v1' => true ), + 'allow_batch' => $this->allow_batch, 'schema' => array( $this, 'get_public_item_schema' ), ) ); @@ -90,7 +98,7 @@ public function register_routes() { ), ), ), - 'allow_batch' => array( 'v1' => true ), + 'allow_batch' => $this->allow_batch, 'schema' => array( $this, 'get_public_item_schema' ), ) ); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 14c1f080751a1..fae3bbb8b397e 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -160,6 +160,7 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. @@ -174,6 +175,7 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media/' . $attachment_id ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } diff --git a/tests/phpunit/tests/rest-api/rest-categories-controller.php b/tests/phpunit/tests/rest-api/rest-categories-controller.php index 311137159927a..01bddd3738567 100644 --- a/tests/phpunit/tests/rest-api/rest-categories-controller.php +++ b/tests/phpunit/tests/rest-api/rest-categories-controller.php @@ -117,6 +117,7 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. @@ -124,6 +125,7 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories/' . $category1 ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } diff --git a/tests/phpunit/tests/rest-api/rest-pages-controller.php b/tests/phpunit/tests/rest-api/rest-pages-controller.php index 00047cce80e08..f4548f3fc8a41 100644 --- a/tests/phpunit/tests/rest-api/rest-pages-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pages-controller.php @@ -64,7 +64,8 @@ public function test_registered_query_params() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $keys = array_keys( $data['endpoints'][0]['args'] ); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + $keys = array_keys( $data['endpoints'][0]['args'] ); sort( $keys ); $this->assertSame( array( diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index b35921a73db19..2b214d184ac7a 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -154,12 +154,14 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . self::$post_id ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 30977958fa911..57951463ae539 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -370,6 +370,38 @@ public function test_allow_header_send_only_permitted_methods() { $this->assertSame( $sent_headers['Allow'], 'POST' ); } + /** + * @ticket 53063 + */ + public function test_batched_options() { + register_rest_route( + 'test-ns', + '/test', + array( + array( + 'methods' => array( 'GET' ), + 'callback' => '__return_null', + 'permission_callback' => '__return_true', + ), + array( + 'methods' => array( 'POST' ), + 'callback' => '__return_null', + 'permission_callback' => '__return_null', + 'allow_batch' => false, + ), + 'allow_batch' => array( 'v1' => true ), + ) + ); + + $request = new WP_REST_Request( 'OPTIONS', '/test-ns/test' ); + $response = rest_get_server()->dispatch( $request ); + + $data = $response->get_data(); + + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][1] ); + } + public function test_allow_header_sent_on_options_request() { register_rest_route( 'test-ns', diff --git a/tests/phpunit/tests/rest-api/rest-tags-controller.php b/tests/phpunit/tests/rest-api/rest-tags-controller.php index 6ec4bbb689cbe..b9ac20cf71d84 100644 --- a/tests/phpunit/tests/rest-api/rest-tags-controller.php +++ b/tests/phpunit/tests/rest-api/rest-tags-controller.php @@ -135,6 +135,7 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. @@ -142,6 +143,7 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags/' . $tag1 ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } diff --git a/tests/phpunit/tests/rest-api/rest-widgets-controller.php b/tests/phpunit/tests/rest-api/rest-widgets-controller.php index d50172f2b3326..a548ab79e1ad2 100644 --- a/tests/phpunit/tests/rest-api/rest-widgets-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widgets-controller.php @@ -1523,6 +1523,8 @@ public function test_get_item_schema() { $data = $response->get_data(); $properties = $data['schema']['properties']; + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + $this->assertCount( 7, $properties ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'id_base', $properties ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index cd327ec468e3d..f61e16362e782 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -261,6 +261,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -602,6 +605,9 @@ mockedApiResponse.Schema = { "methods": [ "POST" ], + "allow_batch": { + "v1": true + }, "args": { "date": { "description": "The date the post was published, in the site's timezone.", @@ -840,6 +846,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -870,6 +879,9 @@ mockedApiResponse.Schema = { "PUT", "PATCH" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -1098,6 +1110,9 @@ mockedApiResponse.Schema = { "methods": [ "DELETE" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -1579,6 +1594,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -1766,6 +1784,9 @@ mockedApiResponse.Schema = { "methods": [ "POST" ], + "allow_batch": { + "v1": true + }, "args": { "date": { "description": "The date the post was published, in the site's timezone.", @@ -1976,6 +1997,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -2006,6 +2030,9 @@ mockedApiResponse.Schema = { "PUT", "PATCH" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -2206,6 +2233,9 @@ mockedApiResponse.Schema = { "methods": [ "DELETE" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -3396,6 +3426,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -3541,6 +3574,9 @@ mockedApiResponse.Schema = { "methods": [ "POST" ], + "allow_batch": { + "v1": true + }, "args": { "date": { "description": "The date the post was published, in the site's timezone.", @@ -3656,6 +3692,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -3686,6 +3725,9 @@ mockedApiResponse.Schema = { "PUT", "PATCH" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -3791,6 +3833,9 @@ mockedApiResponse.Schema = { "methods": [ "DELETE" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the post.", @@ -4606,6 +4651,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -5370,6 +5418,9 @@ mockedApiResponse.Schema = { "methods": [ "POST" ], + "allow_batch": { + "v1": true + }, "args": { "description": { "description": "HTML description of the term.", @@ -5418,6 +5469,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the term.", @@ -5443,6 +5497,9 @@ mockedApiResponse.Schema = { "PUT", "PATCH" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the term.", @@ -5481,6 +5538,9 @@ mockedApiResponse.Schema = { "methods": [ "DELETE" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the term.", @@ -5508,6 +5568,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -5615,6 +5678,9 @@ mockedApiResponse.Schema = { "methods": [ "POST" ], + "allow_batch": { + "v1": true + }, "args": { "description": { "description": "HTML description of the term.", @@ -5658,6 +5724,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the term.", @@ -5683,6 +5752,9 @@ mockedApiResponse.Schema = { "PUT", "PATCH" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the term.", @@ -5716,6 +5788,9 @@ mockedApiResponse.Schema = { "methods": [ "DELETE" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the term.", @@ -7664,6 +7739,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -7687,6 +7765,9 @@ mockedApiResponse.Schema = { "methods": [ "POST" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the widget.", @@ -7762,6 +7843,9 @@ mockedApiResponse.Schema = { "methods": [ "GET" ], + "allow_batch": { + "v1": true + }, "args": { "context": { "description": "Scope under which the request is made; determines fields present in response.", @@ -7782,6 +7866,9 @@ mockedApiResponse.Schema = { "PUT", "PATCH" ], + "allow_batch": { + "v1": true + }, "args": { "id": { "description": "Unique identifier for the widget.", @@ -7837,6 +7924,9 @@ mockedApiResponse.Schema = { "methods": [ "DELETE" ], + "allow_batch": { + "v1": true + }, "args": { "force": { "description": "Whether to force removal of the widget, or move it to the inactive sidebar.",