From bdf5335bb291efa64623069ad11aa642dad6e1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 24 Jun 2020 17:13:21 +0200 Subject: [PATCH 01/26] Restore schema-based title validation (and disable broken tests for now) --- lib/class-wp-rest-menu-items-controller.php | 4 +-- ...ss-rest-nav-menu-items-controller-test.php | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 1e1b84975abeb0..3c8d9059c5a7d8 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -749,9 +749,9 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( // Note: sanitization implemented in self::prepare_item_for_database(). - 'sanitize_callback' => null, +// 'sanitize_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). - 'validate_callback' => null, +// 'validate_callback' => null, ), 'properties' => array( 'raw' => array( diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index a9c5ff444a7790..0b9ba3d994469e 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -178,7 +178,7 @@ public function test_get_item() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item() { wp_set_current_user( self::$admin_id ); @@ -193,7 +193,7 @@ public function test_create_item() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_term() { wp_set_current_user( self::$admin_id ); @@ -212,7 +212,7 @@ public function test_create_item_invalid_term() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_change_position() { wp_set_current_user( self::$admin_id ); @@ -234,7 +234,7 @@ public function test_create_item_change_position() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_position() { wp_set_current_user( self::$admin_id ); @@ -321,7 +321,7 @@ public function test_create_item_invalid_parent() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_parent_menu_item() { wp_set_current_user( self::$admin_id ); @@ -340,7 +340,7 @@ public function test_create_item_invalid_parent_menu_item() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_parent_post() { wp_set_current_user( self::$admin_id ); @@ -358,7 +358,7 @@ public function test_create_item_invalid_parent_post() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_menu() { wp_set_current_user( self::$admin_id ); @@ -375,7 +375,7 @@ public function test_create_item_invalid_menu() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_post() { wp_set_current_user( self::$admin_id ); @@ -394,7 +394,7 @@ public function test_create_item_invalid_post() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_post_type() { wp_set_current_user( self::$admin_id ); @@ -432,7 +432,7 @@ public function test_create_item_invalid_custom_link() { } /** - * + * @requires PHP <= 5.3 */ public function test_create_item_invalid_custom_link_url() { wp_set_current_user( self::$admin_id ); @@ -451,7 +451,7 @@ public function test_create_item_invalid_custom_link_url() { } /** - * + * @requires PHP <= 5.3 */ public function test_update_item() { wp_set_current_user( self::$admin_id ); @@ -480,7 +480,7 @@ public function test_update_item() { } /** - * + * @requires PHP <= 5.3 */ public function test_update_item_clean_xfn() { wp_set_current_user( self::$admin_id ); @@ -513,7 +513,7 @@ public function test_update_item_clean_xfn() { /** - * + * @requires PHP <= 5.3 */ public function test_update_item_invalid() { wp_set_current_user( self::$admin_id ); From fe37af300f69011837b90aba79dbfd0cab2708b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 25 Jun 2020 16:59:16 +0200 Subject: [PATCH 02/26] Schema-based title property --- lib/class-wp-rest-menu-items-controller.php | 125 +++++++++++------- ...ss-rest-nav-menu-items-controller-test.php | 29 ++-- 2 files changed, 92 insertions(+), 62 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 3c8d9059c5a7d8..d0879e21449309 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -54,6 +54,7 @@ protected function get_nav_menu_item( $id ) { * Checks if a given request has access to read a menu item if they have access to edit them. * * @param WP_REST_Request $request Full details about the request. + * * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise. */ public function get_item_permissions_check( $request ) { @@ -62,7 +63,9 @@ public function get_item_permissions_check( $request ) { return $post; } if ( $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'rest_cannot_view', + __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) ); } return parent::get_item_permissions_check( $request ); @@ -72,16 +75,23 @@ public function get_item_permissions_check( $request ) { * Checks if a given request has access to read menu items if they have access to edit them. * * @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 ) { $post_type = get_post_type_object( $this->post_type ); if ( ! current_user_can( $post_type->cap->edit_posts ) ) { if ( 'edit' === $request['context'] ) { - return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'rest_forbidden_context', + __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) ); } - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); + + return new WP_Error( 'rest_cannot_view', + __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) ); } + return true; } @@ -104,7 +114,8 @@ public function create_item( $request ) { } $prepared_nav_item = (array) $prepared_nav_item; - $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item ); + $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], + $prepared_nav_item ); if ( is_wp_error( $nav_menu_item_id ) ) { if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) { $nav_menu_item_id->add_data( array( 'status' => 500 ) ); @@ -127,9 +138,9 @@ public function create_item( $request ) { * * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param object $nav_menu_item Inserted or updated nav item object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a post, false when updating. + * @param object $nav_menu_item Inserted or updated nav item object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating a post, false when updating. * SA */ do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, true ); @@ -158,9 +169,9 @@ public function create_item( $request ) { * * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param object $nav_menu_item Inserted or updated nav item object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a post, false when updating. + * @param object $nav_menu_item Inserted or updated nav item object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating a post, false when updating. */ do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, true ); @@ -194,7 +205,8 @@ public function update_item( $request ) { $prepared_nav_item = (array) $prepared_nav_item; - $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item ); + $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], + $prepared_nav_item ); if ( is_wp_error( $nav_menu_item_id ) ) { if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) { @@ -247,6 +259,7 @@ public function update_item( $request ) { * Deletes a single menu item. * * @param WP_REST_Request $request Full details about the request. + * * @return true|WP_Error True on success, or WP_Error object on failure. */ public function delete_item( $request ) { @@ -260,7 +273,9 @@ public function delete_item( $request ) { // We don't support trashing for menu items. if ( ! $force ) { /* translators: %s: force=true */ - return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) ); + return new WP_Error( 'rest_trash_not_supported', + sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), + array( 'status' => 501 ) ); } $previous = $this->prepare_item_for_response( $menu_item, $request ); @@ -284,9 +299,9 @@ public function delete_item( $request ) { * * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param Object $menu_item The deleted or trashed menu item. + * @param Object $menu_item The deleted or trashed menu item. * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. + * @param WP_REST_Request $request The request sent to the API. */ do_action( "rest_delete_{$this->post_type}", $menu_item, $response, $request ); @@ -356,6 +371,7 @@ protected function prepare_item_for_database( $request ) { 'menu-item-description' => 'description', 'menu-item-attr-title' => 'attr_title', 'menu-item-target' => 'target', + 'menu-item-title' => 'title', 'menu-item-classes' => 'classes', 'menu-item-xfn' => 'xfn', 'menu-item-status' => 'status', @@ -368,9 +384,11 @@ protected function prepare_item_for_database( $request ) { $check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); if ( is_wp_error( $check ) ) { $check->add_data( array( 'status' => 400 ) ); + return $check; } - $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); + $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], + $schema['properties'][ $api_request ] ); } } @@ -381,15 +399,6 @@ protected function prepare_item_for_database( $request ) { $prepared_nav_item['menu-id'] = absint( $request[ $base ] ); } - // Nav menu title. - if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) { - if ( is_string( $request['title'] ) ) { - $prepared_nav_item['menu-item-title'] = $request['title']; - } elseif ( ! empty( $request['title']['raw'] ) ) { - $prepared_nav_item['menu-item-title'] = $request['title']['raw']; - } - } - // Check if object id exists before saving. if ( ! $prepared_nav_item['menu-item-object'] ) { // If taxonony, check if term exists. @@ -399,7 +408,6 @@ protected function prepare_item_for_database( $request ) { return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.', 'gutenberg' ), array( 'status' => 400 ) ); } $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); - // If post, check if post object exists. } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); @@ -421,11 +429,9 @@ protected function prepare_item_for_database( $request ) { // Check if menu item is type custom, then title and url are required. if ( 'custom' === $prepared_nav_item['menu-item-type'] ) { - if ( '' === $prepared_nav_item['menu-item-title'] ) { - return new WP_Error( 'rest_title_required', __( 'Title required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) ); - } if ( empty( $prepared_nav_item['menu-item-url'] ) ) { - return new WP_Error( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) ); + return new WP_Error( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), + array( 'status' => 400 ) ); } } @@ -466,7 +472,8 @@ protected function prepare_item_for_database( $request ) { // Check if valid parent id is valid nav menu item in menu. if ( $prepared_nav_item['menu-item-parent-id'] ) { if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) { - return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) ); + return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), + array( 'status' => 400 ) ); } if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) { return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) ); @@ -495,13 +502,12 @@ protected function prepare_item_for_database( $request ) { // Apply the same filters as when calling wp_insert_post(). /** This filter is documented in wp-includes/post.php */ - $prepared_nav_item['menu-item-title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $prepared_nav_item['menu-item-title'] ) ) ); + $prepared_nav_item['menu-item-attr-title'] = wp_unslash( apply_filters( 'excerpt_save_pre', + wp_slash( $prepared_nav_item['menu-item-attr-title'] ) ) ); /** This filter is documented in wp-includes/post.php */ - $prepared_nav_item['menu-item-attr-title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $prepared_nav_item['menu-item-attr-title'] ) ) ); - - /** This filter is documented in wp-includes/post.php */ - $prepared_nav_item['menu-item-description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $prepared_nav_item['menu-item-description'] ) ) ); + $prepared_nav_item['menu-item-description'] = wp_unslash( apply_filters( 'content_save_pre', + wp_slash( $prepared_nav_item['menu-item-description'] ) ) ); // Valid url. if ( '' !== $prepared_nav_item['menu-item-url'] ) { @@ -523,9 +529,9 @@ protected function prepare_item_for_database( $request ) { * * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param stdClass $prepared_post An object representing a single post prepared + * @param stdClass $prepared_post An object representing a single post prepared * for inserting or updating the database. - * @param WP_REST_Request $request Request object. + * @param WP_REST_Request $request Request object. */ return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request ); } @@ -533,7 +539,7 @@ protected function prepare_item_for_database( $request ) { /** * Prepares a single post output for response. * - * @param object $post Post object. + * @param object $post Post object. * @param WP_REST_Request $request Request object. * * @return WP_REST_Response Response object. @@ -663,8 +669,8 @@ public function prepare_item_for_response( $post, $request ) { * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * * @param WP_REST_Response $response The response object. - * @param object $post Post object. - * @param WP_REST_Request $request Request object. + * @param object $post Post object. + * @param WP_REST_Request $request Request object. */ return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request ); } @@ -745,13 +751,35 @@ public function get_item_schema() { $schema['properties']['title'] = array( 'description' => __( 'The title for the object.', 'gutenberg' ), - 'type' => 'object', + 'type' => array( 'object', 'string' ), 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( - // Note: sanitization implemented in self::prepare_item_for_database(). -// 'sanitize_callback' => null, - // Note: validation implemented in self::prepare_item_for_database(). -// 'validate_callback' => null, + 'validate_callback' => static function( $value, $request ) { + if ( 'custom' !== $request['type'] ) { + return true; + } + if ( '' === $value ) { + return new WP_Error( + 'rest_title_required', + __( 'Title required if menu item of type custom.', 'gutenberg' ) + ); + } + }, + 'sanitize_callback' => static function( $value ) { + if ( ! $value) { + return ""; + } + + $title = ""; + if ( is_string( $value ) ) { + $title = $value; + } elseif ( ! empty( $value['raw'] ) ) { + $title = $value['raw']; + } + + $title = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $title ) ) ); + return $title; + }, ), 'properties' => array( 'raw' => array( @@ -854,7 +882,8 @@ public function get_item_schema() { ); $schema['properties']['object_id'] = array( - 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', 'gutenberg' ), + 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', + 'gutenberg' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', 'minimum' => 0, @@ -983,8 +1012,8 @@ public function get_collection_params() { * Determines the allowed query_vars for a get_items() response and prepares * them for WP_Query. * - * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. - * @param WP_REST_Request $request Optional. Full details about the request. + * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. + * @param WP_REST_Request $request Optional. Full details about the request. * * @return array Items query arguments. */ diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index 0b9ba3d994469e..8158d8f4aa5970 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -178,7 +178,7 @@ public function test_get_item() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item() { wp_set_current_user( self::$admin_id ); @@ -193,7 +193,7 @@ public function test_create_item() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_term() { wp_set_current_user( self::$admin_id ); @@ -212,7 +212,7 @@ public function test_create_item_invalid_term() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_change_position() { wp_set_current_user( self::$admin_id ); @@ -234,7 +234,7 @@ public function test_create_item_change_position() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_position() { wp_set_current_user( self::$admin_id ); @@ -321,7 +321,7 @@ public function test_create_item_invalid_parent() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_parent_menu_item() { wp_set_current_user( self::$admin_id ); @@ -340,7 +340,7 @@ public function test_create_item_invalid_parent_menu_item() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_parent_post() { wp_set_current_user( self::$admin_id ); @@ -358,7 +358,7 @@ public function test_create_item_invalid_parent_post() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_menu() { wp_set_current_user( self::$admin_id ); @@ -375,7 +375,7 @@ public function test_create_item_invalid_menu() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_post() { wp_set_current_user( self::$admin_id ); @@ -394,7 +394,7 @@ public function test_create_item_invalid_post() { } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_post_type() { wp_set_current_user( self::$admin_id ); @@ -428,11 +428,12 @@ public function test_create_item_invalid_custom_link() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_title_required', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Title required if menu item of type custom.', $response->get_data()['data']['params']['title'] ); } /** - * @requires PHP <= 5.3 + * */ public function test_create_item_invalid_custom_link_url() { wp_set_current_user( self::$admin_id ); @@ -451,7 +452,7 @@ public function test_create_item_invalid_custom_link_url() { } /** - * @requires PHP <= 5.3 + * */ public function test_update_item() { wp_set_current_user( self::$admin_id ); @@ -480,7 +481,7 @@ public function test_update_item() { } /** - * @requires PHP <= 5.3 + * */ public function test_update_item_clean_xfn() { wp_set_current_user( self::$admin_id ); @@ -513,7 +514,7 @@ public function test_update_item_clean_xfn() { /** - * @requires PHP <= 5.3 + * */ public function test_update_item_invalid() { wp_set_current_user( self::$admin_id ); From 6de74d170f6be74347263a323ca8500ddf73dc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 10:05:48 +0200 Subject: [PATCH 03/26] Schema-based object property --- lib/class-wp-rest-menu-items-controller.php | 52 ++++++++++++------- ...ss-rest-nav-menu-items-controller-test.php | 6 ++- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index d0879e21449309..871211a5b90411 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -399,25 +399,6 @@ protected function prepare_item_for_database( $request ) { $prepared_nav_item['menu-id'] = absint( $request[ $base ] ); } - // Check if object id exists before saving. - if ( ! $prepared_nav_item['menu-item-object'] ) { - // If taxonony, check if term exists. - if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { - $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); - if ( empty( $original ) || is_wp_error( $original ) ) { - return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.', 'gutenberg' ), array( 'status' => 400 ) ); - } - $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); - // If post, check if post object exists. - } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { - $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); - if ( empty( $original ) ) { - return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'gutenberg' ), array( 'status' => 400 ) ); - } - $prepared_nav_item['menu-item-object'] = get_post_type( $original ); - } - } - // If post type archive, check if post type exists. if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; @@ -879,6 +860,37 @@ public function get_item_schema() { 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."', 'gutenberg' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', + 'default' => '', + 'arg_options' => array( + 'validate_callback' => static function ( $value, $request ) { + // If taxonony, check if term exists. + if ( 'taxonomy' === $request['type'] ) { + $original = get_term( absint( $request['object_id'] ) ); + if ( empty( $original ) || is_wp_error( $original ) ) { + return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.', 'gutenberg' ), array( 'status' => 400 ) ); + } + // If post, check if post object exists. + } elseif ( 'post_type' === $request['type'] ) { + $original = get_post( absint( $request['object_id'] ) ); + if ( empty( $original ) ) { + return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'gutenberg' ), array( 'status' => 400 ) ); + } + } + return true; + }, + + 'sanitize_callback' => static function ( $value, $request ) { + // If taxonony, check if term exists. + if ( 'taxonomy' === $request['type'] ) { + $original = get_term( absint( $request['object_id'] ) ); + return get_term_field( 'taxonomy', $original ); + // If post, check if post object exists. + } elseif ( 'post_type' === $request['type'] ) { + $original = get_post( absint( $request['object_id'] ) ); + return get_post_type( $original ); + } + } + ) ); $schema['properties']['object_id'] = array( @@ -887,7 +899,7 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', 'minimum' => 0, - 'default' => 0, + 'default' => 0 ); $schema['properties']['target'] = array( diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index 8158d8f4aa5970..3956d267d523d0 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -208,7 +208,8 @@ public function test_create_item_invalid_term() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_term_invalid_id', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid term ID.', $response->get_data()['data']['params']['object'] ); } /** @@ -390,7 +391,8 @@ public function test_create_item_invalid_post() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_post_invalid_id', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid post ID.', $response->get_data()['data']['params']['object'] ); } /** From a8a49ed9bc01031259cc2df1658f8964b7ae2478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 12:47:25 +0200 Subject: [PATCH 04/26] More schema-based validation --- lib/class-wp-rest-menu-items-controller.php | 129 +++++++++++------- ...ss-rest-nav-menu-items-controller-test.php | 3 +- 2 files changed, 85 insertions(+), 47 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 871211a5b90411..c77c614032035c 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -12,6 +12,7 @@ * @see WP_REST_Posts_Controller */ class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller { + /** * Constructor. * @@ -204,8 +205,7 @@ public function update_item( $request ) { } $prepared_nav_item = (array) $prepared_nav_item; - - $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], + $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item ); if ( is_wp_error( $nav_menu_item_id ) ) { @@ -360,6 +360,11 @@ protected function prepare_item_for_database( $request ) { ); } + // Check if object id exists before saving. + if ( ! $prepared_nav_item['menu-item-object'] ) { + // Only ever populate $prepared_nav_item['menu-item-object'] if it's missing here! + } + $mapping = array( 'menu-item-db-id' => 'id', 'menu-item-object-id' => 'object_id', @@ -392,6 +397,12 @@ protected function prepare_item_for_database( $request ) { } } + foreach ( array( "menu-item-xfn", "menu-item-classes" ) as $key ) { + if ( is_array( $prepared_nav_item[ $key ] ) ) { + $prepared_nav_item[ $key ] = implode( " ", $prepared_nav_item[ $key ] ); + } + } + $taxonomy = get_taxonomy( 'nav_menu' ); $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; // If menus submitted, cast to int. @@ -462,47 +473,17 @@ protected function prepare_item_for_database( $request ) { } } - foreach ( array( 'menu-item-object-id', 'menu-item-parent-id' ) as $key ) { + foreach ( array( 'menu-item-parent-id' ) as $key ) { // Note we need to allow negative-integer IDs for previewed objects not inserted yet. $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] ); } - foreach ( array( 'menu-item-type', 'menu-item-object', 'menu-item-target' ) as $key ) { + foreach ( array( 'menu-item-type', ) as $key ) { $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); } - // Valid xfn and classes are an array. - foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) { - $value = $prepared_nav_item[ $key ]; - if ( ! is_array( $value ) ) { - $value = wp_parse_list( $value ); - } - $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); - } - // Apply the same filters as when calling wp_insert_post(). - /** This filter is documented in wp-includes/post.php */ - $prepared_nav_item['menu-item-attr-title'] = wp_unslash( apply_filters( 'excerpt_save_pre', - wp_slash( $prepared_nav_item['menu-item-attr-title'] ) ) ); - - /** This filter is documented in wp-includes/post.php */ - $prepared_nav_item['menu-item-description'] = wp_unslash( apply_filters( 'content_save_pre', - wp_slash( $prepared_nav_item['menu-item-description'] ) ) ); - - // Valid url. - if ( '' !== $prepared_nav_item['menu-item-url'] ) { - $prepared_nav_item['menu-item-url'] = esc_url_raw( $prepared_nav_item['menu-item-url'] ); - if ( '' === $prepared_nav_item['menu-item-url'] ) { - // Fail sanitization if URL is invalid. - return new WP_Error( 'invalid_url', __( 'Invalid URL.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - // Only draft / publish are valid post status for menu items. - if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) { - $prepared_nav_item['menu-item-status'] = 'draft'; - } - $prepared_nav_item = (object) $prepared_nav_item; /** @@ -735,7 +716,7 @@ public function get_item_schema() { 'type' => array( 'object', 'string' ), 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( - 'validate_callback' => static function( $value, $request ) { + 'validate_callback' => static function ( $value, $request ) { if ( 'custom' !== $request['type'] ) { return true; } @@ -746,8 +727,8 @@ public function get_item_schema() { ); } }, - 'sanitize_callback' => static function( $value ) { - if ( ! $value) { + 'sanitize_callback' => static function ( $value ) { + if ( ! $value ) { return ""; } @@ -759,6 +740,7 @@ public function get_item_schema() { } $title = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $title ) ) ); + return $title; }, ), @@ -807,6 +789,15 @@ public function get_item_schema() { 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), 'default' => 'publish', 'context' => array( 'view', 'edit', 'embed' ), + 'arg_options' => array( + 'sanitize_callback' => static function ( $value, $request ) { + if ( 'publish' !== $value ) { + return 'draft'; + } + + return $value; + }, + ), ); $schema['properties']['parent'] = array( @@ -822,7 +813,14 @@ public function get_item_schema() { 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', + 'sanitize_callback' => static function ( $value, $request ) { + $sanitized = sanitize_text_field( $value ); + /** This filter is documented in wp-includes/post.php */ + $sanitized = wp_unslash( apply_filters( 'excerpt_save_pre', + wp_slash( $sanitized ) ) ); + + return $sanitized; + }, ), ); @@ -835,7 +833,10 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => function ( $value ) { - return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); + $sanitized = $value; + $sanitized = array_map( 'sanitize_html_class', wp_parse_list( $sanitized ) ); + + return array_map( 'sanitize_html_class', $sanitized ); }, ), ); @@ -845,7 +846,14 @@ public function get_item_schema() { 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', + 'sanitize_callback' => static function ( $value, $request ) { + $sanitized = sanitize_text_field( $value ); + /** This filter is documented in wp-includes/post.php */ + $sanitized = wp_unslash( apply_filters( 'content_save_pre', + wp_slash( $sanitized ) ) ); + + return $sanitized; + }, ), ); @@ -876,21 +884,27 @@ public function get_item_schema() { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'gutenberg' ), array( 'status' => 400 ) ); } } + return true; }, 'sanitize_callback' => static function ( $value, $request ) { + $sanitized = ""; // If taxonony, check if term exists. if ( 'taxonomy' === $request['type'] ) { $original = get_term( absint( $request['object_id'] ) ); - return get_term_field( 'taxonomy', $original ); + + $sanitized = get_term_field( 'taxonomy', $original ); // If post, check if post object exists. } elseif ( 'post_type' === $request['type'] ) { $original = get_post( absint( $request['object_id'] ) ); - return get_post_type( $original ); + + $sanitized = get_post_type( $original ); } - } - ) + + return sanitize_key( $sanitized ); + }, + ), ); $schema['properties']['object_id'] = array( @@ -899,7 +913,12 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', 'minimum' => 0, - 'default' => 0 + 'default' => 0, + 'arg_options' => array( + 'sanitize_callback' => static function ( $value ) { + return intval( $value ); + }, + ), ); $schema['properties']['target'] = array( @@ -910,6 +929,9 @@ public function get_item_schema() { '_blank', '', ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_key', + ), ); $schema['properties']['type_label'] = array( @@ -924,6 +946,18 @@ public function get_item_schema() { 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), + 'arg_options' => array( + 'validate_callback' => static function ( $value, $request ) { + $validated = esc_url_raw( $value ); + if ( '' === $validated ) { + // Fail sanitization if URL is invalid. + return new WP_Error( 'invalid_url', __( 'Invalid URL.', 'gutenberg' ), array( 'status' => 400 ) ); + } + }, + 'sanitize_callback' => static function ( $value, $request ) { + return esc_url_raw( $value ); + }, + ), ); $schema['properties']['xfn'] = array( @@ -935,7 +969,10 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => function ( $value ) { - return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); + $sanitized = $value; + $sanitized = array_map( 'sanitize_html_class', wp_parse_list( $sanitized ) ); + + return array_map( 'sanitize_html_class', $sanitized ); }, ), ); diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index 3956d267d523d0..5ca76cfbff1fd6 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -450,7 +450,8 @@ public function test_create_item_invalid_custom_link_url() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_url_required', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid URL.', $response->get_data()['data']['params']['url'] ); } /** From 23899e36366b08fa352c45730fc5c5e8d67f3924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 14:31:38 +0200 Subject: [PATCH 05/26] Restore object id sanitization logic to prepare_item_for_db --- lib/class-wp-rest-menu-items-controller.php | 60 ++++++++----------- ...ss-rest-nav-menu-items-controller-test.php | 4 +- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index c77c614032035c..3922f710a17d15 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -389,11 +389,23 @@ protected function prepare_item_for_database( $request ) { $check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); if ( is_wp_error( $check ) ) { $check->add_data( array( 'status' => 400 ) ); - return $check; } - $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], - $schema['properties'][ $api_request ] ); + $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); + } + } + + // Check if object id exists before saving. + if ( ! $prepared_nav_item['menu-item-object'] ) { + // If taxonony, check if term exists. + if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { + $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); + $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); + + // If post, check if post object exists. + } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { + $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); + $prepared_nav_item['menu-item-object'] = get_post_type( $original ); } } @@ -868,18 +880,26 @@ public function get_item_schema() { 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."', 'gutenberg' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', - 'default' => '', + ); + + $schema['properties']['object_id'] = array( + 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', + 'gutenberg' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'type' => 'integer', + 'minimum' => 0, + 'default' => 0, 'arg_options' => array( 'validate_callback' => static function ( $value, $request ) { // If taxonony, check if term exists. if ( 'taxonomy' === $request['type'] ) { - $original = get_term( absint( $request['object_id'] ) ); + $original = get_term( absint( $value ) ); if ( empty( $original ) || is_wp_error( $original ) ) { return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.', 'gutenberg' ), array( 'status' => 400 ) ); } // If post, check if post object exists. } elseif ( 'post_type' === $request['type'] ) { - $original = get_post( absint( $request['object_id'] ) ); + $original = get_post( absint( $value ) ); if ( empty( $original ) ) { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'gutenberg' ), array( 'status' => 400 ) ); } @@ -887,34 +907,6 @@ public function get_item_schema() { return true; }, - - 'sanitize_callback' => static function ( $value, $request ) { - $sanitized = ""; - // If taxonony, check if term exists. - if ( 'taxonomy' === $request['type'] ) { - $original = get_term( absint( $request['object_id'] ) ); - - $sanitized = get_term_field( 'taxonomy', $original ); - // If post, check if post object exists. - } elseif ( 'post_type' === $request['type'] ) { - $original = get_post( absint( $request['object_id'] ) ); - - $sanitized = get_post_type( $original ); - } - - return sanitize_key( $sanitized ); - }, - ), - ); - - $schema['properties']['object_id'] = array( - 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', - 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'integer', - 'minimum' => 0, - 'default' => 0, - 'arg_options' => array( 'sanitize_callback' => static function ( $value ) { return intval( $value ); }, diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index 5ca76cfbff1fd6..7f1cd812901d66 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -209,7 +209,7 @@ public function test_create_item_invalid_term() { $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - $this->assertEquals( 'Invalid term ID.', $response->get_data()['data']['params']['object'] ); + $this->assertEquals( 'Invalid term ID.', $response->get_data()['data']['params']['object_id'] ); } /** @@ -392,7 +392,7 @@ public function test_create_item_invalid_post() { $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - $this->assertEquals( 'Invalid post ID.', $response->get_data()['data']['params']['object'] ); + $this->assertEquals( 'Invalid post ID.', $response->get_data()['data']['params']['object_id'] ); } /** From e6f4bc99e9fb00a6dac36158d20edfb5a07ead47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 15:14:45 +0200 Subject: [PATCH 06/26] Add comments --- lib/class-wp-rest-menu-items-controller.php | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 3922f710a17d15..f8196d24fd8668 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -422,6 +422,12 @@ protected function prepare_item_for_database( $request ) { $prepared_nav_item['menu-id'] = absint( $request[ $base ] ); } + // The section below depends on: + // 1. Stored data (on update) + // 2. Request data and schema validation/sanitization + // 3. The section above "Check if object id exists before saving." + // And should be executed even when $request['type'] is null + // If post type archive, check if post type exists. if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; @@ -431,6 +437,11 @@ protected function prepare_item_for_database( $request ) { } } + // The section below depends on: + // 1. Stored data (on update) + // 2. Request data and schema validation/sanitization of "type" and "url" properties + // And should be executed even when $request['url'] is null + // Check if menu item is type custom, then title and url are required. if ( 'custom' === $prepared_nav_item['menu-item-type'] ) { if ( empty( $prepared_nav_item['menu-item-url'] ) ) { @@ -439,6 +450,11 @@ protected function prepare_item_for_database( $request ) { } } + // The section below depends on: + // 1. Stored data (on update) + // 2. $prepared_nav_item['menu-id'] which is based on $request[ ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name ] + // Moving it to validate_callback is not straightforward + // If menu id is set, valid the value of menu item position and parent id. if ( ! empty( $prepared_nav_item['menu-id'] ) ) { // Check if nav menu is valid. @@ -510,6 +526,8 @@ protected function prepare_item_for_database( $request ) { return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request ); } + + /** * Prepares a single post output for response. * @@ -715,6 +733,14 @@ protected function get_schema_links() { * Retrieves the term's schema, conforming to JSON Schema. * * @return array Item schema data. + * + * 1. get_item_schema + * 2. validation + * 3. sanitization + * 4. prepare_item_for_database + * * fetch an object and assign it's data to $prepared_nav_item + * * assign sanitized data to $prepared_nav_item + * * further validation and sanitization based on fetched object and preceeding operations */ public function get_item_schema() { $schema = array( From e69ef47ab06defda8795dc61b64a50807499db47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 15:18:33 +0200 Subject: [PATCH 07/26] Schema-based menu id validation --- lib/class-wp-rest-menu-items-controller.php | 23 ++++++++++++++------- phpunit.xml.dist | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index f8196d24fd8668..cd2c0d7b3c98a5 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -365,7 +365,10 @@ protected function prepare_item_for_database( $request ) { // Only ever populate $prepared_nav_item['menu-item-object'] if it's missing here! } + $taxonomy = get_taxonomy( 'nav_menu' ); + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; $mapping = array( + 'menu-id' => $base, 'menu-item-db-id' => 'id', 'menu-item-object-id' => 'object_id', 'menu-item-object' => 'object', @@ -415,13 +418,6 @@ protected function prepare_item_for_database( $request ) { } } - $taxonomy = get_taxonomy( 'nav_menu' ); - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - // If menus submitted, cast to int. - if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) { - $prepared_nav_item['menu-id'] = absint( $request[ $base ] ); - } - // The section below depends on: // 1. Stored data (on update) // 2. Request data and schema validation/sanitization @@ -939,6 +935,19 @@ public function get_item_schema() { ), ); + $taxonomy = get_taxonomy( 'nav_menu' ); + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + $schema['properties'][$base] = array( + 'description' => __( 'Related menu ID.', 'gutenberg' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'type' => 'integer', + 'arg_options' => array( + 'sanitize_callback' => static function ( $value ) { + return absint( $value ); + }, + ), + ); + $schema['properties']['target'] = array( 'description' => __( 'The target attribute of the link element for this menu item.', 'gutenberg' ), 'type' => 'string', diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 75b7e4bfeb52e1..5190f4724b4825 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,7 @@ > - ./phpunit/ + ./phpunit/class-rest-nav-menu-items-controller-test.php From 894cc5f5d55d5a3f6bda7ac65976e503997dc9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 15:34:26 +0200 Subject: [PATCH 08/26] Schema-based menu id validation --- lib/class-wp-rest-menu-items-controller.php | 38 ++++++++++--------- ...ss-rest-nav-menu-items-controller-test.php | 3 +- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index cd2c0d7b3c98a5..71a74064a0523f 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -453,11 +453,6 @@ protected function prepare_item_for_database( $request ) { // If menu id is set, valid the value of menu item position and parent id. if ( ! empty( $prepared_nav_item['menu-id'] ) ) { - // Check if nav menu is valid. - if ( ! is_nav_menu( $prepared_nav_item['menu-id'] ) ) { - return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) ); - } - // If menu item position is set to 0, insert as the last item in the existing menu. $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { @@ -935,19 +930,6 @@ public function get_item_schema() { ), ); - $taxonomy = get_taxonomy( 'nav_menu' ); - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - $schema['properties'][$base] = array( - 'description' => __( 'Related menu ID.', 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'integer', - 'arg_options' => array( - 'sanitize_callback' => static function ( $value ) { - return absint( $value ); - }, - ), - ); - $schema['properties']['target'] = array( 'description' => __( 'The target attribute of the link element for this menu item.', 'gutenberg' ), 'type' => 'string', @@ -1031,6 +1013,26 @@ public function get_item_schema() { } } + $taxonomy = get_taxonomy( 'nav_menu' ); + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + $schema['properties'][ $base ] = array( + 'description' => __( 'Related menu ID.', 'gutenberg' ), + 'context' => array( 'view', 'edit' ), + 'type' => 'integer', + 'arg_options' => array( + 'validate_callback' => static function ( $value ) { + // Check if nav menu is valid. + if ( ! is_nav_menu( absint( $value ) ) ) { + return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) ); + } + return true; + }, + 'sanitize_callback' => static function ( $value ) { + return absint( $value ); + }, + ), + ); + $schema['properties']['meta'] = $this->meta->get_field_schema(); $schema_links = $this->get_schema_links(); diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index 7f1cd812901d66..d626ec843088c2 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -372,7 +372,8 @@ public function test_create_item_invalid_menu() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'invalid_menu_id', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid menu ID.', $response->get_data()['data']['params']['menus'] ); } /** From e284c0e0c60b8760f35e0193f8ca02937a392121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 15:35:17 +0200 Subject: [PATCH 09/26] Schema-based menu id validation --- lib/class-wp-rest-menu-items-controller.php | 32 ++++++++------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 71a74064a0523f..38f2195812b0bd 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -1010,29 +1010,21 @@ public function get_item_schema() { if ( 'nav_menu' === $taxonomy->name ) { $schema['properties'][ $base ]['type'] = 'integer'; unset( $schema['properties'][ $base ]['items'] ); + $schema['properties'][ $base ]['arg_options'] = array( + 'validate_callback' => static function ( $value ) { + // Check if nav menu is valid. + if ( ! is_nav_menu( absint( $value ) ) ) { + return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) ); + } + return true; + }, + 'sanitize_callback' => static function ( $value ) { + return absint( $value ); + }, + ); } } - $taxonomy = get_taxonomy( 'nav_menu' ); - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - $schema['properties'][ $base ] = array( - 'description' => __( 'Related menu ID.', 'gutenberg' ), - 'context' => array( 'view', 'edit' ), - 'type' => 'integer', - 'arg_options' => array( - 'validate_callback' => static function ( $value ) { - // Check if nav menu is valid. - if ( ! is_nav_menu( absint( $value ) ) ) { - return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) ); - } - return true; - }, - 'sanitize_callback' => static function ( $value ) { - return absint( $value ); - }, - ), - ); - $schema['properties']['meta'] = $this->meta->get_field_schema(); $schema_links = $this->get_schema_links(); From c0b04ad9dea7a9b4643bdcb3238a9c96050a78d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:11:42 +0200 Subject: [PATCH 10/26] Rename prepare_item_for_database -> prepare_item_for_sanitization --- lib/class-wp-rest-menu-items-controller.php | 273 ++++++++++-------- ...ss-rest-nav-menu-items-controller-test.php | 9 +- 2 files changed, 153 insertions(+), 129 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 38f2195812b0bd..d121bc667e8e86 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -316,6 +316,81 @@ public function delete_item( $request ) { * @return stdClass|WP_Error */ protected function prepare_item_for_database( $request ) { + $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); + + // Check if object id exists before saving. + if ( ! $prepared_nav_item['menu-item-object'] ) { + // If taxonony, check if term exists. + if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { + $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); + $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); + // If post, check if post object exists. + } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { + $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); + $prepared_nav_item['menu-item-object'] = get_post_type( $original ); + } + } + + foreach ( array( "menu-item-xfn", "menu-item-classes" ) as $key ) { + if ( is_array( $prepared_nav_item[ $key ] ) ) { + $prepared_nav_item[ $key ] = implode( " ", $prepared_nav_item[ $key ] ); + } + } + + // The section below depends on: + // 1. Stored data (on update) + // 2. Request data and schema validation/sanitization + // 3. The section above "Check if object id exists before saving." + // And should be executed even when $request['type'] is null + + // If post type archive, check if post type exists. + if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { + $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; + $original = get_post_type_object( $post_type ); + if ( empty( $original ) ) { + return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) ); + } + } + + // The section below depends on: + // 1. Stored data (on update) + // 2. Request data and schema validation/sanitization of "type" and "url" properties + // And should be executed even when $request['url'] is null + + // Check if menu item is type custom, then title and url are required. + if ( 'custom' === $prepared_nav_item['menu-item-type'] ) { + if ( empty( $prepared_nav_item['menu-item-url'] ) ) { + return new WP_Error( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), + array( 'status' => 400 ) ); + } + } + + foreach ( array( 'menu-item-parent-id' ) as $key ) { + // Note we need to allow negative-integer IDs for previewed objects not inserted yet. + $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] ); + } + + foreach ( array( 'menu-item-type', ) as $key ) { + $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); + } + + // Apply the same filters as when calling wp_insert_post(). + + $prepared_nav_item = (object) $prepared_nav_item; + + /** + * Filters a post before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @param stdClass $prepared_post An object representing a single post prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request ); + } + + protected function prepare_item_for_sanitization( $request ) { $menu_item_db_id = $request['id']; $menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id ); // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 . @@ -360,14 +435,9 @@ protected function prepare_item_for_database( $request ) { ); } - // Check if object id exists before saving. - if ( ! $prepared_nav_item['menu-item-object'] ) { - // Only ever populate $prepared_nav_item['menu-item-object'] if it's missing here! - } - $taxonomy = get_taxonomy( 'nav_menu' ); $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - $mapping = array( + $mapping = array( 'menu-id' => $base, 'menu-item-db-id' => 'id', 'menu-item-object-id' => 'object_id', @@ -392,133 +462,17 @@ protected function prepare_item_for_database( $request ) { $check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); if ( is_wp_error( $check ) ) { $check->add_data( array( 'status' => 400 ) ); - return $check; - } - $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); - } - } - - // Check if object id exists before saving. - if ( ! $prepared_nav_item['menu-item-object'] ) { - // If taxonony, check if term exists. - if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { - $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); - $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); - - // If post, check if post object exists. - } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { - $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); - $prepared_nav_item['menu-item-object'] = get_post_type( $original ); - } - } - - foreach ( array( "menu-item-xfn", "menu-item-classes" ) as $key ) { - if ( is_array( $prepared_nav_item[ $key ] ) ) { - $prepared_nav_item[ $key ] = implode( " ", $prepared_nav_item[ $key ] ); - } - } - - // The section below depends on: - // 1. Stored data (on update) - // 2. Request data and schema validation/sanitization - // 3. The section above "Check if object id exists before saving." - // And should be executed even when $request['type'] is null - - // If post type archive, check if post type exists. - if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { - $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; - $original = get_post_type_object( $post_type ); - if ( empty( $original ) ) { - return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - - // The section below depends on: - // 1. Stored data (on update) - // 2. Request data and schema validation/sanitization of "type" and "url" properties - // And should be executed even when $request['url'] is null - // Check if menu item is type custom, then title and url are required. - if ( 'custom' === $prepared_nav_item['menu-item-type'] ) { - if ( empty( $prepared_nav_item['menu-item-url'] ) ) { - return new WP_Error( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), - array( 'status' => 400 ) ); - } - } - - // The section below depends on: - // 1. Stored data (on update) - // 2. $prepared_nav_item['menu-id'] which is based on $request[ ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name ] - // Moving it to validate_callback is not straightforward - - // If menu id is set, valid the value of menu item position and parent id. - if ( ! empty( $prepared_nav_item['menu-id'] ) ) { - // If menu item position is set to 0, insert as the last item in the existing menu. - $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); - if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { - if ( $menu_items ) { - $last_item = $menu_items[ count( $menu_items ) - 1 ]; - if ( $last_item && isset( $last_item->menu_order ) ) { - $prepared_nav_item['menu-item-position'] = $last_item->menu_order + 1; - } else { - $prepared_nav_item['menu-item-position'] = count( $menu_items ) - 1; - } - array_push( $menu_items, $last_item ); - } else { - $prepared_nav_item['menu-item-position'] = 1; - } - } - - // Check if existing menu position is already in use by another menu item. - $menu_item_ids = array(); - foreach ( $menu_items as $menu_item ) { - $menu_item_ids[] = $menu_item->ID; - if ( $menu_item->ID !== (int) $menu_item_db_id ) { - if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) { - return new WP_Error( 'invalid_menu_order', __( 'Invalid menu position.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - } - - // Check if valid parent id is valid nav menu item in menu. - if ( $prepared_nav_item['menu-item-parent-id'] ) { - if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) { - return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), - array( 'status' => 400 ) ); - } - if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) { - return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) ); + return $check; } + $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], + $schema['properties'][ $api_request ] ); } } - foreach ( array( 'menu-item-parent-id' ) as $key ) { - // Note we need to allow negative-integer IDs for previewed objects not inserted yet. - $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] ); - } - - foreach ( array( 'menu-item-type', ) as $key ) { - $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); - } - - // Apply the same filters as when calling wp_insert_post(). - - $prepared_nav_item = (object) $prepared_nav_item; - - /** - * Filters a post before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. - * - * @param stdClass $prepared_post An object representing a single post prepared - * for inserting or updating the database. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request ); + return $prepared_nav_item; } - - /** * Prepares a single post output for response. * @@ -835,6 +789,34 @@ public function get_item_schema() { 'minimum' => 0, 'default' => 0, 'context' => array( 'view', 'edit', 'embed' ), + + 'arg_options' => array( + 'sanitize_callback' => function ( $value, $request ) { + $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); + if ( empty( $prepared_nav_item['menu-id'] ) ) { + return $value; + } + + $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); + $menu_item_ids = array(); + foreach ( $menu_items as $menu_item ) { + $menu_item_ids[] = $menu_item->ID; + } + + // Check if valid parent id is valid nav menu item in menu. + if ( $prepared_nav_item['menu-item-parent-id'] ) { + if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) { + return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), + array( 'status' => 400 ) ); + } + if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) { + return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), + array( 'status' => 400 ) ); + } + } + return $value; + }, + ), ); $schema['properties']['attr_title'] = array( @@ -892,6 +874,44 @@ public function get_item_schema() { 'type' => 'integer', 'minimum' => 0, 'default' => 0, + 'arg_options' => array( + 'sanitize_callback' => function ( $value, $request ) { + $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); + if ( empty( $prepared_nav_item['menu-id'] ) ) { + return $value; + } + + // If menu item position is set to 0, insert as the last item in the existing menu. + $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); + if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { + if ( $menu_items ) { + $last_item = $menu_items[ count( $menu_items ) - 1 ]; + if ( $last_item && isset( $last_item->menu_order ) ) { + $prepared_nav_item['menu-item-position'] = $last_item->menu_order + 1; + } else { + $prepared_nav_item['menu-item-position'] = count( $menu_items ) - 1; + } + array_push( $menu_items, $last_item ); + } else { + $prepared_nav_item['menu-item-position'] = 1; + } + } + + // Check if existing menu position is already in use by another menu item. + $menu_item_ids = array(); + foreach ( $menu_items as $menu_item ) { + $menu_item_ids[] = $menu_item->ID; + if ( $menu_item->ID !== (int) $request['id'] ) { + if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) { + return new WP_Error( 'invalid_menu_order', __( 'Invalid menu position.', 'gutenberg' ), + array( 'status' => 400 ) ); + } + } + } + + return $prepared_nav_item['menu-item-position']; + }, + ), ); $schema['properties']['object'] = array( 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."', 'gutenberg' ), @@ -1016,6 +1036,7 @@ public function get_item_schema() { if ( ! is_nav_menu( absint( $value ) ) ) { return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) ); } + return true; }, 'sanitize_callback' => static function ( $value ) { diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index d626ec843088c2..522313262064ca 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -262,7 +262,8 @@ public function test_create_item_invalid_position() { $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'invalid_menu_order', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid menu position.', $response->get_data()['data']['params']['menu_order'] ); } /** @@ -337,7 +338,8 @@ public function test_create_item_invalid_parent_menu_item() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'invalid_item_parent', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid menu item parent.', $response->get_data()['data']['params']['parent'] ); } /** @@ -355,7 +357,8 @@ public function test_create_item_invalid_parent_post() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'invalid_menu_item_parent', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid menu item parent.', $response->get_data()['data']['params']['parent'] ); } /** From 65f121d4414fd205e3d6407affc2514271622c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:24:25 +0200 Subject: [PATCH 11/26] Post type validation moved to sanitize_callback --- lib/class-wp-rest-menu-items-controller.php | 29 +++++++++---------- ...ss-rest-nav-menu-items-controller-test.php | 3 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index d121bc667e8e86..cd064d8a9d7aca 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -337,21 +337,6 @@ protected function prepare_item_for_database( $request ) { } } - // The section below depends on: - // 1. Stored data (on update) - // 2. Request data and schema validation/sanitization - // 3. The section above "Check if object id exists before saving." - // And should be executed even when $request['type'] is null - - // If post type archive, check if post type exists. - if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { - $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; - $original = get_post_type_object( $post_type ); - if ( empty( $original ) ) { - return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - // The section below depends on: // 1. Stored data (on update) // 2. Request data and schema validation/sanitization of "type" and "url" properties @@ -764,6 +749,20 @@ public function get_item_schema() { 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ), 'context' => array( 'view', 'edit', 'embed' ), 'default' => 'custom', + 'arg_options' => array( + 'sanitize_callback' => function ( $value, $request ) { + if ( 'post_type_archive' === $value ) { + $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); + $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; + $original = get_post_type_object( $post_type ); + if ( empty( $original ) ) { + return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) ); + } + } + + return $value; + }, + ), ); $schema['properties']['status'] = array( diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index 522313262064ca..80303002fbb25e 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -415,7 +415,8 @@ public function test_create_item_invalid_post_type() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_post_invalid_type', $response, 400 ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertEquals( 'Invalid post type.', $response->get_data()['data']['params']['type'] ); } /** From fcc147b69653e107d6ea767e974bfb3c4bfe8dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:27:22 +0200 Subject: [PATCH 12/26] Clean up dev artifacts --- lib/class-wp-rest-menu-items-controller.php | 31 ++++----------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index cd064d8a9d7aca..96a8e45ab861da 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -337,19 +337,6 @@ protected function prepare_item_for_database( $request ) { } } - // The section below depends on: - // 1. Stored data (on update) - // 2. Request data and schema validation/sanitization of "type" and "url" properties - // And should be executed even when $request['url'] is null - - // Check if menu item is type custom, then title and url are required. - if ( 'custom' === $prepared_nav_item['menu-item-type'] ) { - if ( empty( $prepared_nav_item['menu-item-url'] ) ) { - return new WP_Error( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), - array( 'status' => 400 ) ); - } - } - foreach ( array( 'menu-item-parent-id' ) as $key ) { // Note we need to allow negative-integer IDs for previewed objects not inserted yet. $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] ); @@ -359,8 +346,6 @@ protected function prepare_item_for_database( $request ) { $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); } - // Apply the same filters as when calling wp_insert_post(). - $prepared_nav_item = (object) $prepared_nav_item; /** @@ -663,14 +648,6 @@ protected function get_schema_links() { * Retrieves the term's schema, conforming to JSON Schema. * * @return array Item schema data. - * - * 1. get_item_schema - * 2. validation - * 3. sanitization - * 4. prepare_item_for_database - * * fetch an object and assign it's data to $prepared_nav_item - * * assign sanitized data to $prepared_nav_item - * * further validation and sanitization based on fetched object and preceeding operations */ public function get_item_schema() { $schema = array( @@ -753,10 +730,11 @@ public function get_item_schema() { 'sanitize_callback' => function ( $value, $request ) { if ( 'post_type_archive' === $value ) { $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); - $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; - $original = get_post_type_object( $post_type ); + $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; + $original = get_post_type_object( $post_type ); if ( empty( $original ) ) { - return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) ); + return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), + array( 'status' => 400 ) ); } } @@ -813,6 +791,7 @@ public function get_item_schema() { array( 'status' => 400 ) ); } } + return $value; }, ), From 8e95458dfc6c290f209e94844419a8ef049a570a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:35:20 +0200 Subject: [PATCH 13/26] Get rid of prepare_item_for_sanitization --- lib/class-wp-rest-menu-items-controller.php | 94 ++++++++++----------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 96a8e45ab861da..67044548db59f8 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -109,6 +109,16 @@ public function create_item( $request ) { } $prepared_nav_item = $this->prepare_item_for_database( $request ); + /** + * Filters a post before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @param stdClass $prepared_post An object representing a single post prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + $prepared_nav_item = apply_filters( "rest_pre_insert_{$this->post_type}", (object) $prepared_nav_item, $request ); if ( is_wp_error( $prepared_nav_item ) ) { return $prepared_nav_item; @@ -199,6 +209,16 @@ public function update_item( $request ) { } $prepared_nav_item = $this->prepare_item_for_database( $request ); + /** + * Filters a post before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. + * + * @param stdClass $prepared_post An object representing a single post prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + $prepared_nav_item = apply_filters( "rest_pre_insert_{$this->post_type}", (object) $prepared_nav_item, $request ); if ( is_wp_error( $prepared_nav_item ) ) { return $prepared_nav_item; @@ -313,54 +333,9 @@ public function delete_item( $request ) { * * @param WP_REST_Request $request Request object. * - * @return stdClass|WP_Error + * @return array|WP_Error */ protected function prepare_item_for_database( $request ) { - $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); - - // Check if object id exists before saving. - if ( ! $prepared_nav_item['menu-item-object'] ) { - // If taxonony, check if term exists. - if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { - $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); - $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); - // If post, check if post object exists. - } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { - $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); - $prepared_nav_item['menu-item-object'] = get_post_type( $original ); - } - } - - foreach ( array( "menu-item-xfn", "menu-item-classes" ) as $key ) { - if ( is_array( $prepared_nav_item[ $key ] ) ) { - $prepared_nav_item[ $key ] = implode( " ", $prepared_nav_item[ $key ] ); - } - } - - foreach ( array( 'menu-item-parent-id' ) as $key ) { - // Note we need to allow negative-integer IDs for previewed objects not inserted yet. - $prepared_nav_item[ $key ] = intval( $prepared_nav_item[ $key ] ); - } - - foreach ( array( 'menu-item-type', ) as $key ) { - $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); - } - - $prepared_nav_item = (object) $prepared_nav_item; - - /** - * Filters a post before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. - * - * @param stdClass $prepared_post An object representing a single post prepared - * for inserting or updating the database. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request ); - } - - protected function prepare_item_for_sanitization( $request ) { $menu_item_db_id = $request['id']; $menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id ); // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 . @@ -440,6 +415,25 @@ protected function prepare_item_for_sanitization( $request ) { } } + // Check if object id exists before saving. + if ( ! $prepared_nav_item['menu-item-object'] ) { + // If taxonony, check if term exists. + if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { + $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); + $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); + // If post, check if post object exists. + } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { + $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); + $prepared_nav_item['menu-item-object'] = get_post_type( $original ); + } + } + + foreach ( array( "menu-item-xfn", "menu-item-classes" ) as $key ) { + if ( is_array( $prepared_nav_item[ $key ] ) ) { + $prepared_nav_item[ $key ] = implode( " ", $prepared_nav_item[ $key ] ); + } + } + return $prepared_nav_item; } @@ -729,7 +723,7 @@ public function get_item_schema() { 'arg_options' => array( 'sanitize_callback' => function ( $value, $request ) { if ( 'post_type_archive' === $value ) { - $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); + $prepared_nav_item = $this->prepare_item_for_database( $request ); $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; $original = get_post_type_object( $post_type ); if ( empty( $original ) ) { @@ -738,7 +732,7 @@ public function get_item_schema() { } } - return $value; + return sanitize_key( $value ); }, ), ); @@ -769,7 +763,7 @@ public function get_item_schema() { 'arg_options' => array( 'sanitize_callback' => function ( $value, $request ) { - $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); + $prepared_nav_item = $this->prepare_item_for_database( $request, true ); if ( empty( $prepared_nav_item['menu-id'] ) ) { return $value; } @@ -854,7 +848,7 @@ public function get_item_schema() { 'default' => 0, 'arg_options' => array( 'sanitize_callback' => function ( $value, $request ) { - $prepared_nav_item = $this->prepare_item_for_sanitization( $request ); + $prepared_nav_item = $this->prepare_item_for_database( $request ); if ( empty( $prepared_nav_item['menu-id'] ) ) { return $value; } From ffaf72f82b947c70813847916aec56fd829b54e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:41:43 +0200 Subject: [PATCH 14/26] Remove dev artifact --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5190f4724b4825..f5a8c10d283307 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,7 @@ > - ./phpunit/class-rest-nav-menu-items-controller-test.php + ./phpunit From b8f7ac6f574ccd4eb24e14b6017ea3b6af92cc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:45:06 +0200 Subject: [PATCH 15/26] Lint and remove unused parameters --- lib/class-wp-rest-menu-items-controller.php | 115 +++++++++++++------- phpunit.xml.dist | 2 +- 2 files changed, 77 insertions(+), 40 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 67044548db59f8..26ad56f36d783c 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -12,7 +12,6 @@ * @see WP_REST_Posts_Controller */ class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller { - /** * Constructor. * @@ -64,9 +63,11 @@ public function get_item_permissions_check( $request ) { return $post; } if ( $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( 'rest_cannot_view', + return new WP_Error( + 'rest_cannot_view', __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) ); + array( 'status' => rest_authorization_required_code() ) + ); } return parent::get_item_permissions_check( $request ); @@ -83,14 +84,18 @@ public function get_items_permissions_check( $request ) { $post_type = get_post_type_object( $this->post_type ); if ( ! current_user_can( $post_type->cap->edit_posts ) ) { if ( 'edit' === $request['context'] ) { - return new WP_Error( 'rest_forbidden_context', + return new WP_Error( + 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) ); + array( 'status' => rest_authorization_required_code() ) + ); } - return new WP_Error( 'rest_cannot_view', + return new WP_Error( + 'rest_cannot_view', __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) ); + array( 'status' => rest_authorization_required_code() ) + ); } return true; @@ -125,8 +130,11 @@ public function create_item( $request ) { } $prepared_nav_item = (array) $prepared_nav_item; - $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], - $prepared_nav_item ); + $nav_menu_item_id = wp_update_nav_menu_item( + $prepared_nav_item['menu-id'], + $prepared_nav_item['menu-item-db-id'], + $prepared_nav_item + ); if ( is_wp_error( $nav_menu_item_id ) ) { if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) { $nav_menu_item_id->add_data( array( 'status' => 500 ) ); @@ -225,8 +233,11 @@ public function update_item( $request ) { } $prepared_nav_item = (array) $prepared_nav_item; - $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], - $prepared_nav_item ); + $nav_menu_item_id = wp_update_nav_menu_item( + $prepared_nav_item['menu-id'], + $prepared_nav_item['menu-item-db-id'], + $prepared_nav_item + ); if ( is_wp_error( $nav_menu_item_id ) ) { if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) { @@ -293,9 +304,11 @@ public function delete_item( $request ) { // We don't support trashing for menu items. if ( ! $force ) { /* translators: %s: force=true */ - return new WP_Error( 'rest_trash_not_supported', + return new WP_Error( + 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), - array( 'status' => 501 ) ); + array( 'status' => 501 ) + ); } $previous = $this->prepare_item_for_response( $menu_item, $request ); @@ -410,8 +423,10 @@ protected function prepare_item_for_database( $request ) { return $check; } - $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], - $schema['properties'][ $api_request ] ); + $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( + $request[ $api_request ], + $schema['properties'][ $api_request ] + ); } } @@ -428,9 +443,9 @@ protected function prepare_item_for_database( $request ) { } } - foreach ( array( "menu-item-xfn", "menu-item-classes" ) as $key ) { + foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) { if ( is_array( $prepared_nav_item[ $key ] ) ) { - $prepared_nav_item[ $key ] = implode( " ", $prepared_nav_item[ $key ] ); + $prepared_nav_item[ $key ] = implode( ' ', $prepared_nav_item[ $key ] ); } } @@ -440,7 +455,7 @@ protected function prepare_item_for_database( $request ) { /** * Prepares a single post output for response. * - * @param object $post Post object. + * @param object $post Post object. * @param WP_REST_Request $request Request object. * * @return WP_REST_Response Response object. @@ -668,10 +683,10 @@ public function get_item_schema() { }, 'sanitize_callback' => static function ( $value ) { if ( ! $value ) { - return ""; + return ''; } - $title = ""; + $title = ''; if ( is_string( $value ) ) { $title = $value; } elseif ( ! empty( $value['raw'] ) ) { @@ -725,10 +740,13 @@ public function get_item_schema() { if ( 'post_type_archive' === $value ) { $prepared_nav_item = $this->prepare_item_for_database( $request ); $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; - $original = get_post_type_object( $post_type ); + $original = get_post_type_object( $post_type ); if ( empty( $original ) ) { - return new WP_Error( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), - array( 'status' => 400 ) ); + return new WP_Error( + 'rest_post_invalid_type', + __( 'Invalid post type.', 'gutenberg' ), + array( 'status' => 400 ) + ); } } @@ -744,7 +762,7 @@ public function get_item_schema() { 'default' => 'publish', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( - 'sanitize_callback' => static function ( $value, $request ) { + 'sanitize_callback' => static function ( $value ) { if ( 'publish' !== $value ) { return 'draft'; } @@ -763,7 +781,7 @@ public function get_item_schema() { 'arg_options' => array( 'sanitize_callback' => function ( $value, $request ) { - $prepared_nav_item = $this->prepare_item_for_database( $request, true ); + $prepared_nav_item = $this->prepare_item_for_database( $request ); if ( empty( $prepared_nav_item['menu-id'] ) ) { return $value; } @@ -777,12 +795,18 @@ public function get_item_schema() { // Check if valid parent id is valid nav menu item in menu. if ( $prepared_nav_item['menu-item-parent-id'] ) { if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) { - return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), - array( 'status' => 400 ) ); + return new WP_Error( + 'invalid_menu_item_parent', + __( 'Invalid menu item parent.', 'gutenberg' ), + array( 'status' => 400 ) + ); } if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) { - return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), - array( 'status' => 400 ) ); + return new WP_Error( + 'invalid_item_parent', + __( 'Invalid menu item parent.', 'gutenberg' ), + array( 'status' => 400 ) + ); } } @@ -796,11 +820,15 @@ public function get_item_schema() { 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( - 'sanitize_callback' => static function ( $value, $request ) { + 'sanitize_callback' => static function ( $value ) { $sanitized = sanitize_text_field( $value ); /** This filter is documented in wp-includes/post.php */ - $sanitized = wp_unslash( apply_filters( 'excerpt_save_pre', - wp_slash( $sanitized ) ) ); + $sanitized = wp_unslash( + apply_filters( + 'excerpt_save_pre', + wp_slash( $sanitized ) + ) + ); return $sanitized; }, @@ -832,8 +860,12 @@ public function get_item_schema() { 'sanitize_callback' => static function ( $value, $request ) { $sanitized = sanitize_text_field( $value ); /** This filter is documented in wp-includes/post.php */ - $sanitized = wp_unslash( apply_filters( 'content_save_pre', - wp_slash( $sanitized ) ) ); + $sanitized = wp_unslash( + apply_filters( + 'content_save_pre', + wp_slash( $sanitized ) + ) + ); return $sanitized; }, @@ -875,8 +907,11 @@ public function get_item_schema() { $menu_item_ids[] = $menu_item->ID; if ( $menu_item->ID !== (int) $request['id'] ) { if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) { - return new WP_Error( 'invalid_menu_order', __( 'Invalid menu position.', 'gutenberg' ), - array( 'status' => 400 ) ); + return new WP_Error( + 'invalid_menu_order', + __( 'Invalid menu position.', 'gutenberg' ), + array( 'status' => 400 ) + ); } } } @@ -892,8 +927,10 @@ public function get_item_schema() { ); $schema['properties']['object_id'] = array( - 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', - 'gutenberg' ), + 'description' => __( + 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', + 'gutenberg' + ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', 'minimum' => 0, @@ -1075,7 +1112,7 @@ public function get_collection_params() { * Determines the allowed query_vars for a get_items() response and prepares * them for WP_Query. * - * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. + * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. * @param WP_REST_Request $request Optional. Full details about the request. * * @return array Items query arguments. diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f5a8c10d283307..aa4b0e943ec1f6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,7 @@ > - ./phpunit + ./phpunitr From ac11aaacc992fa256c7492ebc224559363ca9fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:46:06 +0200 Subject: [PATCH 16/26] Rollback phpunit.xml.dist changes --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index aa4b0e943ec1f6..75b7e4bfeb52e1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,7 @@ > - ./phpunitr + ./phpunit/ From e8cd0f2a14bddd3109e1b63bfdbf0c68bece3f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 16:54:51 +0200 Subject: [PATCH 17/26] Simplify validate_callback of title parameter --- lib/class-wp-rest-menu-items-controller.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 26ad56f36d783c..08b874fc7870dc 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -671,10 +671,7 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'validate_callback' => static function ( $value, $request ) { - if ( 'custom' !== $request['type'] ) { - return true; - } - if ( '' === $value ) { + if ( 'custom' === $request['type'] && '' === $value ) { return new WP_Error( 'rest_title_required', __( 'Title required if menu item of type custom.', 'gutenberg' ) From 0077dd523a063c184cb72e755949c8c70741d172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 26 Jun 2020 17:33:50 +0200 Subject: [PATCH 18/26] First step towards rebasing on top of batch validation branch --- lib/rest-api.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rest-api.php b/lib/rest-api.php index 7f1d5886c783be..c451c0c526e844 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -173,6 +173,7 @@ function wp_api_nav_menus_post_type_args( $args, $post_type ) { $args['show_in_rest'] = true; $args['rest_base'] = 'menu-items'; $args['rest_controller_class'] = 'WP_REST_Menu_Items_Controller'; + $args['validate_callback'] = array('WP_REST_Menu_Items_Controller', 'validate'); } return $args; From a55840efec08cb6ea2db0a2b56af83b49844ae90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:12:15 +0200 Subject: [PATCH 19/26] Move the "global" sanitization logic into the sanitize function --- lib/class-wp-rest-menu-items-controller.php | 250 +++++++++++------- ...ss-rest-nav-menu-items-controller-test.php | 12 +- 2 files changed, 158 insertions(+), 104 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 08b874fc7870dc..43304bc574f34b 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -22,6 +22,87 @@ public function __construct( $post_type ) { $this->namespace = '__experimental'; } + /** + * Registers the routes for the objects of the controller. + * + * @since 4.7.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(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + 'sanitize_callback' => array( $this, 'sanitize' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + 'allow_batch' => true, + ) + ); + + $schema = $this->get_item_schema(); + $get_item_args = array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + if ( isset( $schema['properties']['password'] ) ) { + $get_item_args['password'] = array( + 'description' => __( 'The password for the post if it is password protected.' ), + 'type' => 'string', + ); + } + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => $get_item_args, + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + 'sanitize_callback' => array( $this, 'sanitize' ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'type' => 'boolean', + 'default' => false, + 'description' => __( 'Whether to bypass Trash and force deletion.' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + 'allow_batch' => true, + ) + ); + } + /** * Get the post, if the ID is valid. * @@ -113,7 +194,7 @@ public function create_item( $request ) { return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.', 'gutenberg' ), array( 'status' => 400 ) ); } - $prepared_nav_item = $this->prepare_item_for_database( $request ); + $prepared_nav_item = $request['prepared_nav_item']; /** * Filters a post before it is inserted via the REST API. * @@ -216,7 +297,7 @@ public function update_item( $request ) { return $valid_check; } - $prepared_nav_item = $this->prepare_item_for_database( $request ); + $prepared_nav_item = $request['prepared_nav_item']; /** * Filters a post before it is inserted via the REST API. * @@ -348,7 +429,7 @@ public function delete_item( $request ) { * * @return array|WP_Error */ - protected function prepare_item_for_database( $request ) { + public function sanitize( $params, $request ) { $menu_item_db_id = $request['id']; $menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id ); // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 . @@ -449,13 +530,79 @@ protected function prepare_item_for_database( $request ) { } } - return $prepared_nav_item; + $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); + $menu_item_ids = array(); + foreach ( $menu_items as $menu_item ) { + $menu_item_ids[] = $menu_item->ID; + } + + // If post type archive, check if post type exists. + if ( 'post_type_archive' === $request['type'] ) { + $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; + $original = get_post_type_object( $post_type ); + if ( empty( $original ) ) { + return new WP_Error( + 'rest_post_invalid_type', + __( 'Invalid post type.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + } + + // If menu id is set, valid the value of menu item position and parent id. + if ( ! empty( $prepared_nav_item['menu-id'] ) ) { + // Check if nav menu is valid. + if ( ! is_nav_menu( $prepared_nav_item['menu-id'] ) ) { + return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.', 'gutenberg' ), array( 'status' => 400 ) ); + } + + // If menu item position is set to 0, insert as the last item in the existing menu. + $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); + if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { + if ( $menu_items ) { + $last_item = $menu_items[ count( $menu_items ) - 1 ]; + if ( $last_item && isset( $last_item->menu_order ) ) { + $prepared_nav_item['menu-item-position'] = $last_item->menu_order + 1; + } else { + $prepared_nav_item['menu-item-position'] = count( $menu_items ) - 1; + } + array_push( $menu_items, $last_item ); + } else { + $prepared_nav_item['menu-item-position'] = 1; + } + } + + // Check if existing menu position is already in use by another menu item. + $menu_item_ids = array(); + foreach ( $menu_items as $menu_item ) { + $menu_item_ids[] = $menu_item->ID; + if ( $menu_item->ID !== (int) $menu_item_db_id ) { + if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) { + return new WP_Error( 'invalid_menu_order', __( 'Invalid menu position.', 'gutenberg' ), array( 'status' => 400 ) ); + } + } + } + + // Check if valid parent id is valid nav menu item in menu. + if ( $prepared_nav_item['menu-item-parent-id'] ) { + if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) { + return new WP_Error( 'invalid_menu_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) ); + } + if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) { + return new WP_Error( 'invalid_item_parent', __( 'Invalid menu item parent.', 'gutenberg' ), array( 'status' => 400 ) ); + } + } + } + + $params['POST']['prepared_nav_item'] = $prepared_nav_item; + + return $params; } /** * Prepares a single post output for response. * - * @param object $post Post object. + * @param object $post Post object. * @param WP_REST_Request $request Request object. * * @return WP_REST_Response Response object. @@ -733,20 +880,7 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'default' => 'custom', 'arg_options' => array( - 'sanitize_callback' => function ( $value, $request ) { - if ( 'post_type_archive' === $value ) { - $prepared_nav_item = $this->prepare_item_for_database( $request ); - $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; - $original = get_post_type_object( $post_type ); - if ( empty( $original ) ) { - return new WP_Error( - 'rest_post_invalid_type', - __( 'Invalid post type.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } - } - + 'sanitize_callback' => function ( $value ) { return sanitize_key( $value ); }, ), @@ -775,41 +909,6 @@ public function get_item_schema() { 'minimum' => 0, 'default' => 0, 'context' => array( 'view', 'edit', 'embed' ), - - 'arg_options' => array( - 'sanitize_callback' => function ( $value, $request ) { - $prepared_nav_item = $this->prepare_item_for_database( $request ); - if ( empty( $prepared_nav_item['menu-id'] ) ) { - return $value; - } - - $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); - $menu_item_ids = array(); - foreach ( $menu_items as $menu_item ) { - $menu_item_ids[] = $menu_item->ID; - } - - // Check if valid parent id is valid nav menu item in menu. - if ( $prepared_nav_item['menu-item-parent-id'] ) { - if ( ! is_nav_menu_item( $prepared_nav_item['menu-item-parent-id'] ) ) { - return new WP_Error( - 'invalid_menu_item_parent', - __( 'Invalid menu item parent.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } - if ( ! $menu_item_ids || ! in_array( $prepared_nav_item['menu-item-parent-id'], $menu_item_ids, true ) ) { - return new WP_Error( - 'invalid_item_parent', - __( 'Invalid menu item parent.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } - } - - return $value; - }, - ), ); $schema['properties']['attr_title'] = array( @@ -875,47 +974,6 @@ public function get_item_schema() { 'type' => 'integer', 'minimum' => 0, 'default' => 0, - 'arg_options' => array( - 'sanitize_callback' => function ( $value, $request ) { - $prepared_nav_item = $this->prepare_item_for_database( $request ); - if ( empty( $prepared_nav_item['menu-id'] ) ) { - return $value; - } - - // If menu item position is set to 0, insert as the last item in the existing menu. - $menu_items = wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); - if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { - if ( $menu_items ) { - $last_item = $menu_items[ count( $menu_items ) - 1 ]; - if ( $last_item && isset( $last_item->menu_order ) ) { - $prepared_nav_item['menu-item-position'] = $last_item->menu_order + 1; - } else { - $prepared_nav_item['menu-item-position'] = count( $menu_items ) - 1; - } - array_push( $menu_items, $last_item ); - } else { - $prepared_nav_item['menu-item-position'] = 1; - } - } - - // Check if existing menu position is already in use by another menu item. - $menu_item_ids = array(); - foreach ( $menu_items as $menu_item ) { - $menu_item_ids[] = $menu_item->ID; - if ( $menu_item->ID !== (int) $request['id'] ) { - if ( (int) $prepared_nav_item['menu-item-position'] === (int) $menu_item->menu_order ) { - return new WP_Error( - 'invalid_menu_order', - __( 'Invalid menu position.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } - } - } - - return $prepared_nav_item['menu-item-position']; - }, - ), ); $schema['properties']['object'] = array( 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."', 'gutenberg' ), @@ -1109,7 +1167,7 @@ public function get_collection_params() { * Determines the allowed query_vars for a get_items() response and prepares * them for WP_Query. * - * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. + * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. * @param WP_REST_Request $request Optional. Full details about the request. * * @return array Items query arguments. diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index 80303002fbb25e..d626ec843088c2 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -262,8 +262,7 @@ public function test_create_item_invalid_position() { $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - $this->assertEquals( 'Invalid menu position.', $response->get_data()['data']['params']['menu_order'] ); + $this->assertErrorResponse( 'invalid_menu_order', $response, 400 ); } /** @@ -338,8 +337,7 @@ public function test_create_item_invalid_parent_menu_item() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - $this->assertEquals( 'Invalid menu item parent.', $response->get_data()['data']['params']['parent'] ); + $this->assertErrorResponse( 'invalid_item_parent', $response, 400 ); } /** @@ -357,8 +355,7 @@ public function test_create_item_invalid_parent_post() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - $this->assertEquals( 'Invalid menu item parent.', $response->get_data()['data']['params']['parent'] ); + $this->assertErrorResponse( 'invalid_menu_item_parent', $response, 400 ); } /** @@ -415,8 +412,7 @@ public function test_create_item_invalid_post_type() { ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); - $this->assertEquals( 'Invalid post type.', $response->get_data()['data']['params']['type'] ); + $this->assertErrorResponse( 'rest_post_invalid_type', $response, 400 ); } /** From 2a86560b5f531e44a9e2a97cb2844c1f7191fb0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:23:03 +0200 Subject: [PATCH 20/26] Lint --- lib/class-wp-rest-menu-items-controller.php | 22 ++++++++++----------- lib/rest-api.php | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 43304bc574f34b..38a01e4200ef38 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -58,7 +58,7 @@ public function register_routes() { ); if ( isset( $schema['properties']['password'] ) ) { $get_item_args['password'] = array( - 'description' => __( 'The password for the post if it is password protected.' ), + 'description' => __( 'The password for the post if it is password protected.', 'default' ), 'type' => 'string', ); } @@ -68,7 +68,7 @@ public function register_routes() { array( 'args' => array( 'id' => array( - 'description' => __( 'Unique identifier for the object.' ), + 'description' => __( 'Unique identifier for the object.', 'default' ), 'type' => 'integer', ), ), @@ -93,7 +93,7 @@ public function register_routes() { 'force' => array( 'type' => 'boolean', 'default' => false, - 'description' => __( 'Whether to bypass Trash and force deletion.' ), + 'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ), ), ), ), @@ -135,7 +135,6 @@ protected function get_nav_menu_item( $id ) { * Checks if a given request has access to read a menu item if they have access to edit them. * * @param WP_REST_Request $request Full details about the request. - * * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise. */ public function get_item_permissions_check( $request ) { @@ -384,9 +383,9 @@ public function delete_item( $request ) { // We don't support trashing for menu items. if ( ! $force ) { - /* translators: %s: force=true */ return new WP_Error( 'rest_trash_not_supported', + /* translators: %s: force=true */ sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) ); @@ -425,6 +424,7 @@ public function delete_item( $request ) { /** * Prepares a single post for create or update. * + * @param array $params List of params to sanitize. * @param WP_REST_Request $request Request object. * * @return array|WP_Error @@ -538,8 +538,8 @@ public function sanitize( $params, $request ) { // If post type archive, check if post type exists. if ( 'post_type_archive' === $request['type'] ) { - $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; - $original = get_post_type_object( $post_type ); + $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; + $original = get_post_type_object( $post_type ); if ( empty( $original ) ) { return new WP_Error( 'rest_post_invalid_type', @@ -602,7 +602,7 @@ public function sanitize( $params, $request ) { /** * Prepares a single post output for response. * - * @param object $post Post object. + * @param object $post Post object. * @param WP_REST_Request $request Request object. * * @return WP_REST_Response Response object. @@ -1040,14 +1040,14 @@ public function get_item_schema() { 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( - 'validate_callback' => static function ( $value, $request ) { + 'validate_callback' => static function ( $value ) { $validated = esc_url_raw( $value ); if ( '' === $validated ) { // Fail sanitization if URL is invalid. return new WP_Error( 'invalid_url', __( 'Invalid URL.', 'gutenberg' ), array( 'status' => 400 ) ); } }, - 'sanitize_callback' => static function ( $value, $request ) { + 'sanitize_callback' => static function ( $value ) { return esc_url_raw( $value ); }, ), @@ -1167,7 +1167,7 @@ public function get_collection_params() { * Determines the allowed query_vars for a get_items() response and prepares * them for WP_Query. * - * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. + * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. * @param WP_REST_Request $request Optional. Full details about the request. * * @return array Items query arguments. diff --git a/lib/rest-api.php b/lib/rest-api.php index c451c0c526e844..8c3d90c739d25d 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -173,7 +173,7 @@ function wp_api_nav_menus_post_type_args( $args, $post_type ) { $args['show_in_rest'] = true; $args['rest_base'] = 'menu-items'; $args['rest_controller_class'] = 'WP_REST_Menu_Items_Controller'; - $args['validate_callback'] = array('WP_REST_Menu_Items_Controller', 'validate'); + $args['validate_callback'] = array( 'WP_REST_Menu_Items_Controller', 'validate' ); } return $args; From fb835b688106bdd4a0cd6d60a5e12bd7c6c89044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:23:52 +0200 Subject: [PATCH 21/26] Remove dev artifact --- lib/rest-api.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rest-api.php b/lib/rest-api.php index 8c3d90c739d25d..7f1d5886c783be 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -173,7 +173,6 @@ function wp_api_nav_menus_post_type_args( $args, $post_type ) { $args['show_in_rest'] = true; $args['rest_base'] = 'menu-items'; $args['rest_controller_class'] = 'WP_REST_Menu_Items_Controller'; - $args['validate_callback'] = array( 'WP_REST_Menu_Items_Controller', 'validate' ); } return $args; From 1c5ce927cd86ea2b23fb714941043239b49a5f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:29:09 +0200 Subject: [PATCH 22/26] Sanitize the request itself, not the params --- lib/class-wp-rest-menu-items-controller.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 38a01e4200ef38..84ee1d24780e20 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -424,12 +424,11 @@ public function delete_item( $request ) { /** * Prepares a single post for create or update. * - * @param array $params List of params to sanitize. * @param WP_REST_Request $request Request object. * * @return array|WP_Error */ - public function sanitize( $params, $request ) { + public function sanitize( $request ) { $menu_item_db_id = $request['id']; $menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id ); // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 . @@ -594,9 +593,7 @@ public function sanitize( $params, $request ) { } } - $params['POST']['prepared_nav_item'] = $prepared_nav_item; - - return $params; + $request['prepared_nav_item'] = $prepared_nav_item; } /** From 70a07cdba2e04f9537683a833142fe3e6f2f7b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:35:54 +0200 Subject: [PATCH 23/26] Formatting --- lib/class-wp-rest-menu-items-controller.php | 56 ++++++--------------- 1 file changed, 16 insertions(+), 40 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 84ee1d24780e20..5391cddfba3254 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -143,11 +143,7 @@ public function get_item_permissions_check( $request ) { return $post; } if ( $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( - 'rest_cannot_view', - __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); + return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); } return parent::get_item_permissions_check( $request ); @@ -164,18 +160,10 @@ public function get_items_permissions_check( $request ) { $post_type = get_post_type_object( $this->post_type ); if ( ! current_user_can( $post_type->cap->edit_posts ) ) { if ( 'edit' === $request['context'] ) { - return new WP_Error( - 'rest_forbidden_context', - __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); + return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); } - return new WP_Error( - 'rest_cannot_view', - __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); + return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); } return true; @@ -210,11 +198,7 @@ public function create_item( $request ) { } $prepared_nav_item = (array) $prepared_nav_item; - $nav_menu_item_id = wp_update_nav_menu_item( - $prepared_nav_item['menu-id'], - $prepared_nav_item['menu-item-db-id'], - $prepared_nav_item - ); + $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item ); if ( is_wp_error( $nav_menu_item_id ) ) { if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) { $nav_menu_item_id->add_data( array( 'status' => 500 ) ); @@ -237,9 +221,9 @@ public function create_item( $request ) { * * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param object $nav_menu_item Inserted or updated nav item object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a post, false when updating. + * @param object $nav_menu_item Inserted or updated nav item object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating a post, false when updating. * SA */ do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, true ); @@ -268,9 +252,9 @@ public function create_item( $request ) { * * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param object $nav_menu_item Inserted or updated nav item object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a post, false when updating. + * @param object $nav_menu_item Inserted or updated nav item object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating a post, false when updating. */ do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, true ); @@ -313,11 +297,7 @@ public function update_item( $request ) { } $prepared_nav_item = (array) $prepared_nav_item; - $nav_menu_item_id = wp_update_nav_menu_item( - $prepared_nav_item['menu-id'], - $prepared_nav_item['menu-item-db-id'], - $prepared_nav_item - ); + $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], $prepared_nav_item ); if ( is_wp_error( $nav_menu_item_id ) ) { if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) { @@ -383,12 +363,8 @@ public function delete_item( $request ) { // We don't support trashing for menu items. if ( ! $force ) { - return new WP_Error( - 'rest_trash_not_supported', - /* translators: %s: force=true */ - sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), - array( 'status' => 501 ) - ); + /* translators: %s: force=true */ + return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) ); } $previous = $this->prepare_item_for_response( $menu_item, $request ); @@ -412,9 +388,9 @@ public function delete_item( $request ) { * * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param Object $menu_item The deleted or trashed menu item. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. + * @param Object $menu_item The deleted or trashed menu item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. */ do_action( "rest_delete_{$this->post_type}", $menu_item, $response, $request ); From 0a18888dcdd412e6e6ef0a57194441cf4f45d0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:37:24 +0200 Subject: [PATCH 24/26] Formatting --- lib/class-wp-rest-menu-items-controller.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 5391cddfba3254..9b5bf110159755 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -182,6 +182,7 @@ public function create_item( $request ) { } $prepared_nav_item = $request['prepared_nav_item']; + /** * Filters a post before it is inserted via the REST API. * @@ -281,6 +282,7 @@ public function update_item( $request ) { } $prepared_nav_item = $request['prepared_nav_item']; + /** * Filters a post before it is inserted via the REST API. * From b1a07dc0118b707bc58dc5ecae7d09c8e3cb3dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:38:27 +0200 Subject: [PATCH 25/26] Formatting --- lib/class-wp-rest-menu-items-controller.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 9b5bf110159755..f6eac32be76f2e 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -390,9 +390,9 @@ public function delete_item( $request ) { * * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. * - * @param Object $menu_item The deleted or trashed menu item. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. + * @param Object $menu_item The deleted or trashed menu item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. */ do_action( "rest_delete_{$this->post_type}", $menu_item, $response, $request ); @@ -404,7 +404,7 @@ public function delete_item( $request ) { * * @param WP_REST_Request $request Request object. * - * @return array|WP_Error + * @return null */ public function sanitize( $request ) { $menu_item_db_id = $request['id']; From b1ffaf373e6b2604fcaded300e8f8b445d4237df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Mon, 29 Jun 2020 13:53:17 +0200 Subject: [PATCH 26/26] Tighten the title validation and sanitization --- lib/class-wp-rest-menu-items-controller.php | 2 +- ...ss-rest-nav-menu-items-controller-test.php | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index f6eac32be76f2e..6ebec7cf4db59a 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -789,7 +789,7 @@ public function get_item_schema() { $schema['properties']['title'] = array( 'description' => __( 'The title for the object.', 'gutenberg' ), - 'type' => array( 'object', 'string' ), + 'type' => array( 'string', 'object' ), 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'validate_callback' => static function ( $value, $request ) { diff --git a/phpunit/class-rest-nav-menu-items-controller-test.php b/phpunit/class-rest-nav-menu-items-controller-test.php index d626ec843088c2..8f59ee0c5e5570 100644 --- a/phpunit/class-rest-nav-menu-items-controller-test.php +++ b/phpunit/class-rest-nav-menu-items-controller-test.php @@ -192,6 +192,52 @@ public function test_create_item() { $this->check_create_menu_item_response( $response ); } + /** + * + */ + public function test_create_item_title_array() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/__experimental/menu-items' ); + $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'title' => array( + 'raw' => 'A raw title', + 'rendered' => 'A rendered title', + ), + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_create_menu_item_response( $response ); + $this->assertEquals( 'A raw title', $response->get_data()['title']['raw'] ); + } + + /** + * + */ + public function test_create_item_title_empty() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/__experimental/menu-items' ); + $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'title' => array( + 'raw' => '', + 'rendered' => 'A rendered title', + ), + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_create_menu_item_response( $response ); + $this->assertEquals( '', $response->get_data()['title']['raw'] ); + } + /** * */