From cce4c8ef6d75050a506035d1ba0be38ecc0e5dbe Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 19 Jun 2019 00:08:44 +0100 Subject: [PATCH 01/15] Fix parent param. --- lib/class-wp-rest-menu-items-controller.php | 28 +++++++-------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index d0422a2..5b7dae7 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -231,7 +231,7 @@ protected function prepare_item_for_database( $request ) { 'menu-item-db-id' => 'db_id', 'menu-item-object-id' => 'object_id', 'menu-item-object' => 'object', - 'menu-item-parent-id' => 'menu_item_parent', + 'menu-item-parent-id' => 'parent', 'menu-item-position' => 'menu_order', 'menu-item-type' => 'type', 'menu-item-url' => 'url', @@ -354,11 +354,7 @@ public function prepare_item_for_response( $post, $request ) { } if ( in_array( 'parent', $fields, true ) ) { - $data['parent'] = absint( $menu_item->post_parent ); // Same as post_parent, expose as integer. - } - - if ( in_array( 'menu_item_parent', $fields, true ) ) { - $data['menu_item_parent'] = absint( $menu_item->menu_item_parent ); // Same as post_parent, expose as integer. + $data['parent'] = absint( $menu_item->menu_item_parent ); // Same as post_parent, expose as integer. } if ( in_array( 'menu_order', $fields, true ) ) { @@ -559,7 +555,7 @@ public function get_item_schema() { $schema['properties']['parent'] = array( 'description' => __( 'The ID for the parent of the object.' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit' , 'embed'), ); $schema['properties']['attr_title'] = array( @@ -588,25 +584,19 @@ public function get_item_schema() { 'type' => 'string', ); - $schema['properties']['menu_item_parent'] = array( - 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ), - 'context' => array( 'view', 'edit' ), - 'type' => 'integer', - ); - $schema['properties']['menu_order'] = array( 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed'), 'type' => 'integer', ); $schema['properties']['object'] = array( 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), ); $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 .' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', ); @@ -618,7 +608,7 @@ public function get_item_schema() { $schema['properties']['type_label'] = array( 'description' => __( 'The singular label used to describe this type of menu item.' ), - 'context' => array( 'view' ), + 'context' => array( 'view' , 'embed'), 'type' => 'string', 'readonly' => true, ); @@ -627,12 +617,12 @@ public function get_item_schema() { 'description' => __( 'The URL to which this menu item points .' ), 'type' => 'string', 'format' => 'uri', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit' , 'embed'), ); $schema['properties']['xfn'] = array( 'description' => __( 'The XFN relationship expressed in the link of this menu item . ' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'array', 'items' => array( 'type' => 'string', From 00e4dd2642d39810f1c73d7b2ba8b112a80eb2d0 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 19 Jun 2019 16:42:52 +0100 Subject: [PATCH 02/15] Improve how id are handled --- lib/class-wp-rest-menu-items-controller.php | 81 +++++++++++++-------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index d0422a2..abd7eaf 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -58,11 +58,8 @@ public function create_item( $request ) { if ( is_wp_error( $prepared_nav_item ) ) { return $prepared_nav_item; } - - $menu_id = (int) $request['menu_id']; - $id = 0; - - $nav_menu_item_id = wp_update_nav_menu_item( $menu_id, $id, $prepared_nav_item ); + $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 ); if ( is_wp_error( $nav_menu_item_id ) ) { if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) { @@ -151,9 +148,9 @@ public function update_item( $request ) { return $prepared_nav_item; } - $menu_id = (int) $request['menu_id']; + $prepared_nav_item = (array) $prepared_nav_item; - $nav_menu_item_id = wp_update_nav_menu_item( $menu_id, $request['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() ) { @@ -210,25 +207,55 @@ public function update_item( $request ) { * @return stdClass */ protected function prepare_item_for_database( $request ) { - $prepared_nav_item = array( - 'menu-item-db-id' => 0, - 'menu-item-object-id' => 0, - 'menu-item-object' => '', - 'menu-item-parent-id' => 0, - 'menu-item-position' => 0, - 'menu-item-type' => 'custom', - 'menu-item-title' => '', - 'menu-item-url' => '', - 'menu-item-description' => '', - 'menu-item-attr-title' => '', - 'menu-item-target' => '', - 'menu-item-classes' => '', - 'menu-item-xfn' => '', - 'menu-item-status' => 'publish', - ); + $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 . + if ( ! is_wp_error( $menu_item_obj ) ) { + // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140 . + $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order; + + $prepared_nav_item = array( + 'menu-item-db-id' => $menu_item_db_id, + 'menu-item-object-id' => $menu_item_obj->object_id, + 'menu-item-object' => $menu_item_obj->object, + 'menu-item-parent-id' => $menu_item_obj->menu_item_parent, + 'menu-item-position' => $position, + 'menu-item-title' => $menu_item_obj->title, + 'menu-item-url' => $menu_item_obj->url, + 'menu-item-description' => $menu_item_obj->description, + 'menu-item-attr-title' => $menu_item_obj->attr_title, + 'menu-item-target' => $menu_item_obj->target, + 'menu-item-classes' => implode( ' ', $menu_item_obj->classes ), // stored in the database as array. + 'menu-item-xfn' => $menu_item_obj->xfn, + 'menu-item-status' => $menu_item_obj->post_status, + ); + $menu_ids = wp_get_post_terms( $menu_item_db_id, 'nav_menu', array( 'fields' => 'ids' ) ); + if ( $menu_ids && ! is_wp_error( $menu_ids ) ) { + $prepared_nav_item['menu-id'] = array_shift( $menu_ids ); + } + } else { + $prepared_nav_item = array( + 'menu-id' => 0, + 'menu-item-db-id' => 0, + 'menu-item-object-id' => 0, + 'menu-item-object' => '', + 'menu-item-parent-id' => 0, + 'menu-item-position' => 0, + 'menu-item-type' => 'custom', + 'menu-item-title' => '', + 'menu-item-url' => '', + 'menu-item-description' => '', + 'menu-item-attr-title' => '', + 'menu-item-target' => '', + 'menu-item-classes' => '', + 'menu-item-xfn' => '', + 'menu-item-status' => 'publish', + ); + } $mapping = array( - 'menu-item-db-id' => 'db_id', + 'menu-id' => 'menu_id', + 'menu-item-db-id' => 'id', 'menu-item-object-id' => 'object_id', 'menu-item-object' => 'object', 'menu-item-parent-id' => 'menu_item_parent', @@ -576,12 +603,6 @@ public function get_item_schema() { ), ); - $schema['properties']['db_id'] = array( - 'description' => __( 'The DB ID of this item as a nav_menu_item object, if it exists( 0 if it doesn\'t exist).' ), - 'context' => array( 'view', 'edit' ), - 'type' => 'integer', - ); - $schema['properties']['description'] = array( 'description' => __( 'The description of this menu item.' ), 'context' => array( 'view', 'edit' ), From 10c471d10204ff301757893eab13cddc22b9e6a6 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 19 Jun 2019 16:51:28 +0100 Subject: [PATCH 03/15] Fix lints --- 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 5b7dae7..23da380 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -555,7 +555,7 @@ public function get_item_schema() { $schema['properties']['parent'] = array( 'description' => __( 'The ID for the parent of the object.' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' , 'embed'), + 'context' => array( 'view', 'edit', 'embed' ), ); $schema['properties']['attr_title'] = array( @@ -586,7 +586,7 @@ public function get_item_schema() { $schema['properties']['menu_order'] = array( 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ), - 'context' => array( 'view', 'edit', 'embed'), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', ); $schema['properties']['object'] = array( @@ -608,7 +608,7 @@ public function get_item_schema() { $schema['properties']['type_label'] = array( 'description' => __( 'The singular label used to describe this type of menu item.' ), - 'context' => array( 'view' , 'embed'), + 'context' => array( 'view', 'embed' ), 'type' => 'string', 'readonly' => true, ); @@ -617,7 +617,7 @@ public function get_item_schema() { 'description' => __( 'The URL to which this menu item points .' ), 'type' => 'string', 'format' => 'uri', - 'context' => array( 'view', 'edit' , 'embed'), + 'context' => array( 'view', 'edit', 'embed' ), ); $schema['properties']['xfn'] = array( From 2320b174aeca38c54087f2929c647eae053331b3 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 24 Jun 2019 01:43:55 +0100 Subject: [PATCH 04/15] Improved validation --- lib/class-wp-rest-menu-items-controller.php | 168 ++++++++++++++++---- 1 file changed, 141 insertions(+), 27 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index abd7eaf..af193e3 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -53,13 +53,14 @@ public function create_item( $request ) { return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) ); } - $prepared_nav_item = (array) $this->prepare_item_for_database( $request ); + $prepared_nav_item = $this->prepare_item_for_database( $request ); if ( is_wp_error( $prepared_nav_item ) ) { return $prepared_nav_item; } $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() ) { @@ -204,7 +205,7 @@ public function update_item( $request ) { * * @param WP_REST_Request $request Request object. * - * @return stdClass + * @return stdClass|WP_Error */ protected function prepare_item_for_database( $request ) { $menu_item_db_id = $request['id']; @@ -228,11 +229,8 @@ protected function prepare_item_for_database( $request ) { 'menu-item-classes' => implode( ' ', $menu_item_obj->classes ), // stored in the database as array. 'menu-item-xfn' => $menu_item_obj->xfn, 'menu-item-status' => $menu_item_obj->post_status, + 'menu-id' => $this->get_menu_id( $menu_item_db_id ), ); - $menu_ids = wp_get_post_terms( $menu_item_db_id, 'nav_menu', array( 'fields' => 'ids' ) ); - if ( $menu_ids && ! is_wp_error( $menu_ids ) ) { - $prepared_nav_item['menu-id'] = array_shift( $menu_ids ); - } } else { $prepared_nav_item = array( 'menu-id' => 0, @@ -287,6 +285,7 @@ protected function prepare_item_for_database( $request ) { } } + // Check if object id existing before saving. if ( ! $prepared_nav_item['menu-item-object'] && $prepared_nav_item['menu-item-object-id'] ) { if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { $original = get_term( (int) $prepared_nav_item['menu-item-object-id'] ); @@ -303,8 +302,89 @@ protected function prepare_item_for_database( $request ) { } } - $prepared_nav_item['menu-item-classes'] = implode( ' ', array_map( 'sanitize_html_class', $prepared_nav_item['menu-item-classes'] ) ); - $prepared_nav_item['menu-item-xfn'] = implode( ' ', array_map( 'sanitize_html_class', $prepared_nav_item['menu-item-xfn'] ) ); + // 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 require if menu item of type custom.' ), array( 'status' => 400 ) ); + } + if ( empty( $prepared_nav_item['menu-item-url'] ) ) { + return new WP_Error( 'rest_url_required', __( 'URL require if menu item of type custom.' ), array( 'status' => 400 ) ); + } + } + + // If menu if is set, valid position and parent. + if ( ! empty( $prepared_nav_item['menu-id'] ) ) { + if ( ! is_nav_menu( $prepared_nav_item['menu-id'] ) ) { + return new WP_Error( 'invalid_menu_id', __( 'Invalid menu ID.' ), array( 'status' => 400 ) ); + } + + $menu_items = (array) wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); + if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { + $last_item = array_pop( $menu_items ); + $prepared_nav_item['menu-item-position'] = ( $last_item && isset( $last_item->menu_order ) ) ? 1 + $last_item->menu_order : count( $menu_items ); + } + + $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.' ), array( 'status' => 400 ) ); + } + } + } + + 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.' ), 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.' ), array( 'status' => 400 ) ); + } + } + } + + foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_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( 'type', 'object', 'target' ) as $key ) { + $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); + } + + foreach ( array( 'xfn', 'classes' ) as $key ) { + $value = $prepared_nav_item[ $key ]; + if ( ! is_array( $value ) ) { + $value = explode( ' ', $value ); + } + $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); + } + + $prepared_nav_item['original_title'] = sanitize_text_field( $prepared_nav_item['original_title'] ); + + // Apply the same filters as when calling wp_insert_post(). + + /** This filter is documented in wp-includes/post.php */ + $prepared_nav_item['title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $prepared_nav_item['title'] ) ) ); + + /** This filter is documented in wp-includes/post.php */ + $prepared_nav_item['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $prepared_nav_item['attr_title'] ) ) ); + + /** This filter is documented in wp-includes/post.php */ + $prepared_nav_item['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $prepared_nav_item['description'] ) ) ); + + if ( '' !== $prepared_nav_item['url'] ) { + $prepared_nav_item['url'] = esc_url_raw( $prepared_nav_item['url'] ); + if ( '' === $prepared_nav_item['url'] ) { + return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) ); // Fail sanitization if URL is invalid. + } + } + if ( 'publish' !== $prepared_nav_item['status'] ) { + $prepared_nav_item['status'] = 'draft'; + } + + $prepared_nav_item['_invalid'] = (bool) $prepared_nav_item['_invalid']; $prepared_nav_item = (object) $prepared_nav_item; @@ -392,6 +472,10 @@ public function prepare_item_for_response( $post, $request ) { $data['menu_order'] = absint( $menu_item->menu_order ); // Same as post_parent, expose as integer. } + if ( in_array( 'menu_id', $fields, true ) ) { + $data['menu_id'] = $this->get_menu_id( $menu_item->ID ); + } + if ( in_array( 'target', $fields, true ) ) { $data['target'] = $menu_item->target; } @@ -401,7 +485,7 @@ public function prepare_item_for_response( $post, $request ) { } if ( in_array( 'xfn', $fields, true ) ) { - $data['xfn'] = (array) $menu_item->xfn; + $data['xfn'] = explode( ' ', $menu_item->xfn ); } if ( in_array( 'meta', $fields, true ) ) { @@ -480,8 +564,6 @@ protected function prepare_links( $menu_item ) { /** * Retrieve Link Description Objects that should be added to the Schema for the posts collection. * - * @since 4.9.8 - * * @return array */ protected function get_schema_links() { @@ -543,6 +625,7 @@ public function get_item_schema() { 'description' => __( 'Unique identifier for the object.' ), 'type' => 'integer', 'default' => 0, + 'minimum' => 0, 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); @@ -550,6 +633,7 @@ public function get_item_schema() { $schema['properties']['menu_id'] = array( 'description' => __( 'Unique identifier for the menu.' ), 'type' => 'integer', + 'minimum' => 0, 'context' => array( 'edit' ), 'default' => 0, ); @@ -557,7 +641,7 @@ public function get_item_schema() { $schema['properties']['type_label'] = array( 'description' => __( 'Name of type.' ), 'type' => 'string', - 'context' => array( 'view', 'embed' ), + 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, ); @@ -565,6 +649,7 @@ public function get_item_schema() { 'description' => __( 'Type of menu item' ), 'type' => 'string', 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ), + 'default' => 'custom', 'context' => array( 'view', 'edit', 'embed' ), ); @@ -572,7 +657,8 @@ public function get_item_schema() { 'description' => __( 'A named status for the object.' ), 'type' => 'string', 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), - 'context' => array( 'view', 'edit' ), + 'default' => 'publish', + 'context' => array( 'view', 'edit', 'embed' ), ); $schema['properties']['link'] = array( @@ -586,17 +672,20 @@ public function get_item_schema() { $schema['properties']['parent'] = array( 'description' => __( 'The ID for the parent of the object.' ), 'type' => 'integer', - 'context' => array( 'view', 'edit' ), + 'minimum' => 0, + 'default' => 0, + 'context' => array( 'view', 'edit', 'embed' ), ); $schema['properties']['attr_title'] = array( 'description' => __( 'The title attribute of the link element for this menu item .' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', ); - $schema['properties']['classes'] = array( + + $schema['properties']['classes'] = array( 'description' => __( 'The array of class attribute values for the link element of this menu item .' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'array', 'items' => array( 'type' => 'string', @@ -605,35 +694,42 @@ public function get_item_schema() { $schema['properties']['description'] = array( 'description' => __( 'The description of this menu item.' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', ); $schema['properties']['menu_item_parent'] = array( 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', + 'minimum' => 0, + 'default' => 0, ); $schema['properties']['menu_order'] = array( 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any . 0 otherwise . ' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', + 'minimum' => 0, + 'default' => 0, ); $schema['properties']['object'] = array( 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), + 'type' => 'string', ); $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 .' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'integer', + 'minimum' => 0, + 'default' => 0, ); $schema['properties']['target'] = array( 'description' => __( 'The target attribute of the link element for this menu item . The family of objects originally represented, such as "post_type" or "taxonomy."' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', ); @@ -648,12 +744,12 @@ public function get_item_schema() { 'description' => __( 'The URL to which this menu item points .' ), 'type' => 'string', 'format' => 'uri', - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), ); $schema['properties']['xfn'] = array( 'description' => __( 'The XFN relationship expressed in the link of this menu item . ' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'array', 'items' => array( 'type' => 'string', @@ -662,8 +758,9 @@ public function get_item_schema() { $schema['properties']['_invalid'] = array( 'description' => __( ' Whether the menu item represents an object that no longer exists .' ), - 'context' => array( 'view', 'edit' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'boolean', + 'readonly' => true, ); $schema['properties']['meta'] = $this->meta->get_field_schema(); @@ -748,4 +845,21 @@ protected function prepare_items_query( $prepared_args = array(), $request = nul return $query_args; } + + /** + * Get menu id of current menu item. + * + * @param int $menu_item_id Menu item id. + * + * @return int + */ + protected function get_menu_id( $menu_item_id ) { + $menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) ); + $menu_id = 0; + if ( $menu_ids && ! is_wp_error( $menu_ids ) ) { + $menu_id = array_shift( $menu_ids ); + } + + return $menu_id; + } } From f037ed9b3735b1d1056754a5438bd2b15e85eb81 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 24 Jun 2019 18:49:23 +0100 Subject: [PATCH 05/15] Add more validation --- lib/class-wp-rest-menu-items-controller.php | 60 +++++++++++++-------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 2110dbc..0baf2ab 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -642,11 +642,14 @@ public function get_item_schema() { ); $schema['properties']['type'] = array( - 'description' => __( 'Type of menu item' ), + 'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ), 'type' => 'string', 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ), - 'default' => 'custom', 'context' => array( 'view', 'edit', 'embed' ), + 'default' => 'custom', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_key', + ), ); $schema['properties']['status'] = array( @@ -657,14 +660,6 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), ); - $schema['properties']['link'] = array( - 'description' => __( 'URL to the object.' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ); - $schema['properties']['parent'] = array( 'description' => __( 'The ID for the parent of the object.' ), 'type' => 'integer', @@ -674,24 +669,35 @@ public function get_item_schema() { ); $schema['properties']['attr_title'] = array( - 'description' => __( 'The title attribute of the link element for this menu item .' ), - 'context' => array( 'view', 'edit', 'embed' ), + 'description' => __( 'Text for the title attribute of the link element for this menu item.' ), 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ); $schema['properties']['classes'] = array( - 'description' => __( 'The array of class attribute values for the link element of this menu item .' ), - 'context' => array( 'view', 'edit', 'embed' ), + 'description' => __( 'Class names for the link element of this menu item.' ), 'type' => 'array', 'items' => array( 'type' => 'string', ), + 'context' => array( 'view', 'edit', 'embed' ), + 'arg_options' => array( + 'sanitize_callback' => function ( $value ) { + return array_map( 'sanitize_html_class', explode( ' ', $value ) ); + }, + ), ); $schema['properties']['description'] = array( 'description' => __( 'The description of this menu item.' ), - 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), ); $schema['properties']['menu_order'] = array( @@ -716,9 +722,13 @@ public function get_item_schema() { ); $schema['properties']['target'] = array( - 'description' => __( 'The target attribute of the link element for this menu item . The family of objects originally represented, such as "post_type" or "taxonomy."' ), - 'context' => array( 'view', 'edit', 'embed' ), + 'description' => __( 'The target attribute of the link element for this menu item.' ), 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'enum' => array( + '_blank', + '', + ), ); $schema['properties']['type_label'] = array( @@ -729,21 +739,27 @@ public function get_item_schema() { ); $schema['properties']['url'] = array( - 'description' => __( 'The URL to which this menu item points .' ), + 'description' => __( 'The URL to which this menu item points.' ), 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), + 'arg_options' => array( + 'sanitize_callback' => 'esc_url_raw', + ), ); $schema['properties']['xfn'] = array( - 'description' => __( 'The XFN relationship expressed in the link of this menu item . ' ), + 'description' => __( 'The XFN relationship expressed in the link of this menu item.' ), + 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => function ( $value ) { + return implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $value ) ) ); + }, ), ); + $schema['properties']['_invalid'] = array( 'description' => __( ' Whether the menu item represents an object that no longer exists .' ), 'context' => array( 'view', 'edit', 'embed' ), From aec9bca7a086183802d6f1a343e03fe4656152ef Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Mon, 24 Jun 2019 19:27:48 +0100 Subject: [PATCH 06/15] Improved handling of menu ids --- lib/class-wp-rest-menu-items-controller.php | 69 ++++++++++++++------- 1 file changed, 46 insertions(+), 23 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 0baf2ab..853822b 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -252,7 +252,6 @@ protected function prepare_item_for_database( $request ) { } $mapping = array( - 'menu-id' => 'menu_id', 'menu-item-db-id' => 'id', 'menu-item-object-id' => 'object_id', 'menu-item-object' => 'object', @@ -275,6 +274,14 @@ protected function prepare_item_for_database( $request ) { $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); } } + $taxonomy = get_taxonomy('nav_menu'); + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) { + if ( count( $request[ $base ] ) > 1 ) { + return new WP_Error( 'rest_single_menu', __( 'Unable to test menu item to multiple menus.' ), array( 'status' => 400 ) ); + } + $prepared_nav_item['menu-id'] = array_shift( $request[ $base ] ); + } // Nav menu title. if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) { @@ -344,16 +351,16 @@ protected function prepare_item_for_database( $request ) { } } - foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) { + foreach ( array( 'menu-item-object-id', '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( 'type', 'object', 'target' ) as $key ) { + foreach ( array( 'menu-item-type', 'menu-item-object', 'menu-item-target' ) as $key ) { $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); } - foreach ( array( 'xfn', 'classes' ) as $key ) { + foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) { $value = $prepared_nav_item[ $key ]; if ( ! is_array( $value ) ) { $value = explode( ' ', $value ); @@ -361,31 +368,28 @@ protected function prepare_item_for_database( $request ) { $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); } - $prepared_nav_item['original_title'] = sanitize_text_field( $prepared_nav_item['original_title'] ); // Apply the same filters as when calling wp_insert_post(). /** This filter is documented in wp-includes/post.php */ - $prepared_nav_item['title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $prepared_nav_item['title'] ) ) ); + $prepared_nav_item['menu-item-title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $prepared_nav_item['menu-item-title'] ) ) ); /** This filter is documented in wp-includes/post.php */ - $prepared_nav_item['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $prepared_nav_item['attr_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['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $prepared_nav_item['description'] ) ) ); + $prepared_nav_item['menu-item-description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $prepared_nav_item['menu-item-description'] ) ) ); - if ( '' !== $prepared_nav_item['url'] ) { - $prepared_nav_item['url'] = esc_url_raw( $prepared_nav_item['url'] ); - if ( '' === $prepared_nav_item['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'] ) { return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) ); // Fail sanitization if URL is invalid. } } - if ( 'publish' !== $prepared_nav_item['status'] ) { - $prepared_nav_item['status'] = 'draft'; + if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) { + $prepared_nav_item['menu-item-status'] = 'draft'; } - $prepared_nav_item['_invalid'] = (bool) $prepared_nav_item['_invalid']; - $prepared_nav_item = (object) $prepared_nav_item; /** @@ -488,6 +492,17 @@ public function prepare_item_for_response( $post, $request ) { $data['meta'] = $this->meta->get_value( $menu_item->ID, $request ); } + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + if ( in_array( $base, $fields, true ) ) { + $terms = get_the_terms( $post, $taxonomy->name ); + $data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array(); + } + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); @@ -626,14 +641,6 @@ public function get_item_schema() { 'readonly' => true, ); - $schema['properties']['menu_id'] = array( - 'description' => __( 'Unique identifier for the menu.' ), - 'type' => 'integer', - 'minimum' => 0, - 'context' => array( 'edit' ), - 'default' => 0, - ); - $schema['properties']['type_label'] = array( 'description' => __( 'Name of type.' ), 'type' => 'string', @@ -767,6 +774,22 @@ public function get_item_schema() { 'readonly' => true, ); + + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); + + foreach ( $taxonomies as $taxonomy ) { + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + $schema['properties'][ $base ] = array( + /* translators: %s: taxonomy name */ + 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ); + } + $schema['properties']['meta'] = $this->meta->get_field_schema(); $schema_links = $this->get_schema_links(); From 966dcb5a6334b119b28a047f9b266f8db72a1347 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 2 Jul 2019 22:45:29 +0100 Subject: [PATCH 07/15] Add some comments --- lib/class-wp-rest-menu-items-controller.php | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 853822b..c2fc233 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -274,8 +274,10 @@ protected function prepare_item_for_database( $request ) { $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); } } - $taxonomy = get_taxonomy('nav_menu'); - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + $taxonomy = get_taxonomy( 'nav_menu' ); + $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + // If menus submitted, then check if 1 menu is given, if more than 1 is given, error out. if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) { if ( count( $request[ $base ] ) > 1 ) { return new WP_Error( 'rest_single_menu', __( 'Unable to test menu item to multiple menus.' ), array( 'status' => 400 ) ); @@ -294,18 +296,25 @@ protected function prepare_item_for_database( $request ) { // Check if object id existing before saving. if ( ! $prepared_nav_item['menu-item-object'] && $prepared_nav_item['menu-item-object-id'] ) { + // If taxonony, check if term exists. if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { $original = get_term( (int) $prepared_nav_item['menu-item-object-id'] ); if ( empty( $original ) ) { return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) ); } $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); - } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { + } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { // If post, check if post object exists. $original = get_post( (int) $prepared_nav_item['menu-item-object-id'] ); if ( empty( $original ) ) { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) ); } $prepared_nav_item['menu-item-object'] = get_post_type( $original ); + } elseif ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { // If post type archive, check if post type exists. + $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.' ), array( 'status' => 400 ) ); + } } } @@ -321,16 +330,19 @@ protected function prepare_item_for_database( $request ) { // If menu if is set, valid position and parent. 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.' ), array( 'status' => 400 ) ); } + // If menu item position is set to 0, when set position to last in existing menu. $menu_items = (array) wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { $last_item = array_pop( $menu_items ); $prepared_nav_item['menu-item-position'] = ( $last_item && isset( $last_item->menu_order ) ) ? 1 + $last_item->menu_order : count( $menu_items ); } + // 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; @@ -341,6 +353,7 @@ 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.' ), array( 'status' => 400 ) ); @@ -360,6 +373,7 @@ protected function prepare_item_for_database( $request ) { $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 ) ) { @@ -368,7 +382,6 @@ protected function prepare_item_for_database( $request ) { $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 */ @@ -380,12 +393,14 @@ protected function prepare_item_for_database( $request ) { /** 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'] ) { return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) ); // Fail sanitization if URL is invalid. } } + // 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'; } @@ -766,7 +781,6 @@ public function get_item_schema() { ), ); - $schema['properties']['_invalid'] = array( 'description' => __( ' Whether the menu item represents an object that no longer exists .' ), 'context' => array( 'view', 'edit', 'embed' ), @@ -774,7 +788,6 @@ public function get_item_schema() { 'readonly' => true, ); - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); foreach ( $taxonomies as $taxonomy ) { From a6ea0907c85b0948687e536db8257b84f038cfd0 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 2 Jul 2019 22:53:41 +0100 Subject: [PATCH 08/15] Add more comments --- lib/class-wp-rest-menu-items-controller.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index c2fc233..201e730 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -309,12 +309,15 @@ protected function prepare_item_for_database( $request ) { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) ); } $prepared_nav_item['menu-item-object'] = get_post_type( $original ); - } elseif ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { // If post type archive, check if post type exists. - $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.' ), array( 'status' => 400 ) ); - } + } + } + + // 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.' ), array( 'status' => 400 ) ); } } From 1c440aec2992996cfcbaae7c3e9662da152df869 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Sat, 17 Aug 2019 12:33:23 +0100 Subject: [PATCH 09/15] Fix comments, styling issues and change callbacks. --- lib/class-wp-rest-menu-items-controller.php | 33 +++++++++++---------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 201e730..1cc8fe3 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -303,7 +303,9 @@ protected function prepare_item_for_database( $request ) { return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) ); } $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); - } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { // If post, check if post object exists. + + // If post, check if post object exists. + } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { $original = get_post( (int) $prepared_nav_item['menu-item-object-id'] ); if ( empty( $original ) ) { return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) ); @@ -331,18 +333,22 @@ protected function prepare_item_for_database( $request ) { } } - // If menu if is set, valid position and parent. + // 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.' ), array( 'status' => 400 ) ); } - // If menu item position is set to 0, when set position to last in existing menu. + // If menu item position is set to 0, insert as the last item in the existing menu. $menu_items = (array) wp_get_nav_menu_items( $prepared_nav_item['menu-id'], array( 'post_status' => 'publish,draft' ) ); if ( 0 === (int) $prepared_nav_item['menu-item-position'] ) { - $last_item = array_pop( $menu_items ); - $prepared_nav_item['menu-item-position'] = ( $last_item && isset( $last_item->menu_order ) ) ? 1 + $last_item->menu_order : count( $menu_items ); + $last_item = array_pop( $menu_items ); + 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 ); + } } // Check if existing menu position is already in use by another menu item. @@ -503,7 +509,7 @@ public function prepare_item_for_response( $post, $request ) { } if ( in_array( 'xfn', $fields, true ) ) { - $data['xfn'] = explode( ' ', $menu_item->xfn ); + $data['xfn'] = (array) $menu_item->xfn; } if ( in_array( 'meta', $fields, true ) ) { @@ -672,9 +678,6 @@ 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' => 'sanitize_key', - ), ); $schema['properties']['status'] = array( @@ -768,24 +771,24 @@ public function get_item_schema() { 'type' => 'string', 'format' => 'uri', 'context' => array( 'view', 'edit', 'embed' ), - 'arg_options' => array( - 'sanitize_callback' => 'esc_url_raw', - ), ); $schema['properties']['xfn'] = array( 'description' => __( 'The XFN relationship expressed in the link of this menu item.' ), - 'type' => 'string', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => function ( $value ) { - return implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $value ) ) ); + return array_map( 'sanitize_html_class', explode( ' ', $value ) ); }, ), ); $schema['properties']['_invalid'] = array( - 'description' => __( ' Whether the menu item represents an object that no longer exists .' ), + 'description' => __( 'Whether the menu item represents an object that no longer exists .' ), 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'boolean', 'readonly' => true, From baa5f7c81a827e7d438b3c3b294d0f2ca61dfa5c Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 20 Nov 2019 09:42:59 +0000 Subject: [PATCH 10/15] Add type label in edit. --- lib/class-wp-rest-menu-items-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 1cc8fe3..f13ef03 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -761,7 +761,7 @@ public function get_item_schema() { $schema['properties']['type_label'] = array( 'description' => __( 'The singular label used to describe this type of menu item.' ), - 'context' => array( 'view', 'embed' ), + 'context' => array( 'view', 'edit', 'embed' ), 'type' => 'string', 'readonly' => true, ); From 1cec7b9519a711ff295778c726f99f6538a4930f Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Wed, 20 Nov 2019 16:42:29 +0000 Subject: [PATCH 11/15] Add maxItems param in schema. --- lib/class-wp-rest-menu-items-controller.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index f13ef03..08d219f 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -279,8 +279,10 @@ protected function prepare_item_for_database( $request ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; // If menus submitted, then check if 1 menu is given, if more than 1 is given, error out. if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) { - if ( count( $request[ $base ] ) > 1 ) { - return new WP_Error( 'rest_single_menu', __( 'Unable to test menu item to multiple menus.' ), array( 'status' => 400 ) ); + $maxItems = ( int ) $schema['properties'][ $base ]['maxItems']; + $nav_menus = count( $request[ $base ] ); + if ( $maxItems && $nav_menus > $maxItems ) { + return new WP_Error( 'rest_invalid_nav_menu_assignmentss', sprintf( __( 'Unable to menu item to %d menus. Max allowed is %d' ), $nav_menus, $maxItems ), array( 'status' => 400 ) ); } $prepared_nav_item['menu-id'] = array_shift( $request[ $base ] ); } @@ -807,6 +809,10 @@ public function get_item_schema() { ), 'context' => array( 'view', 'edit' ), ); + + if ( 'nav_menu' === $taxonomy ) { + $schema['properties'][ $base ]['maxItems'] = 1; + } } $schema['properties']['meta'] = $this->meta->get_field_schema(); From d0d25bfa92f22046943c346cf81e603a9afe7b56 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Fri, 22 Nov 2019 22:15:17 +0000 Subject: [PATCH 12/15] Make nav_menu into an integer. --- lib/class-wp-rest-menu-items-controller.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 08d219f..6a3c00a 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -279,12 +279,7 @@ protected function prepare_item_for_database( $request ) { $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; // If menus submitted, then check if 1 menu is given, if more than 1 is given, error out. if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) { - $maxItems = ( int ) $schema['properties'][ $base ]['maxItems']; - $nav_menus = count( $request[ $base ] ); - if ( $maxItems && $nav_menus > $maxItems ) { - return new WP_Error( 'rest_invalid_nav_menu_assignmentss', sprintf( __( 'Unable to menu item to %d menus. Max allowed is %d' ), $nav_menus, $maxItems ), array( 'status' => 400 ) ); - } - $prepared_nav_item['menu-id'] = array_shift( $request[ $base ] ); + $prepared_nav_item['menu-id'] = (int) $request[ $base ]; } // Nav menu title. @@ -811,7 +806,8 @@ public function get_item_schema() { ); if ( 'nav_menu' === $taxonomy ) { - $schema['properties'][ $base ]['maxItems'] = 1; + $schema['properties'][ $base ]['type'] = 'integer'; + unset( $schema['properties'][ $base ]['items'] ); } } From cbc17732475ae99893696091debfa3d59dc41e18 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 26 Nov 2019 14:26:17 +0000 Subject: [PATCH 13/15] Fix comments. --- lib/class-wp-rest-menu-items-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 6a3c00a..bee9b1a 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -277,7 +277,7 @@ 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, then check if 1 menu is given, if more than 1 is given, error out. + // If menus submitted, cast to int. if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) { $prepared_nav_item['menu-id'] = (int) $request[ $base ]; } @@ -291,7 +291,7 @@ protected function prepare_item_for_database( $request ) { } } - // Check if object id existing before saving. + // Check if object id exists before saving. if ( ! $prepared_nav_item['menu-item-object'] && $prepared_nav_item['menu-item-object-id'] ) { // If taxonony, check if term exists. if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { From f1ee75a2b9c09db7a5b7b738a6cc2c2023eb29fb Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 26 Nov 2019 14:29:48 +0000 Subject: [PATCH 14/15] Use absint. --- lib/class-wp-rest-menu-items-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index bee9b1a..11e9542 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -279,7 +279,7 @@ protected function prepare_item_for_database( $request ) { $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'] = (int) $request[ $base ]; + $prepared_nav_item['menu-id'] = absint( $request[ $base ] ); } // Nav menu title. @@ -295,7 +295,7 @@ protected function prepare_item_for_database( $request ) { if ( ! $prepared_nav_item['menu-item-object'] && $prepared_nav_item['menu-item-object-id'] ) { // If taxonony, check if term exists. if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { - $original = get_term( (int) $prepared_nav_item['menu-item-object-id'] ); + $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); if ( empty( $original ) ) { return new WP_Error( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) ); } @@ -303,7 +303,7 @@ protected function prepare_item_for_database( $request ) { // If post, check if post object exists. } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { - $original = get_post( (int) $prepared_nav_item['menu-item-object-id'] ); + $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.' ), array( 'status' => 400 ) ); } From aa5242a53ada635a5be893fe7928bd0d2dc26cc0 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 26 Nov 2019 14:33:57 +0000 Subject: [PATCH 15/15] Use wp_parse_list over explode. --- lib/class-wp-rest-menu-items-controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-rest-menu-items-controller.php b/lib/class-wp-rest-menu-items-controller.php index 11e9542..6feac30 100644 --- a/lib/class-wp-rest-menu-items-controller.php +++ b/lib/class-wp-rest-menu-items-controller.php @@ -383,7 +383,7 @@ protected function prepare_item_for_database( $request ) { foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) { $value = $prepared_nav_item[ $key ]; if ( ! is_array( $value ) ) { - $value = explode( ' ', $value ); + $value = wp_parse_list( $value ); } $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); } @@ -711,7 +711,7 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => function ( $value ) { - return array_map( 'sanitize_html_class', explode( ' ', $value ) ); + return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); }, ), ); @@ -779,7 +779,7 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), 'arg_options' => array( 'sanitize_callback' => function ( $value ) { - return array_map( 'sanitize_html_class', explode( ' ', $value ) ); + return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); }, ), );