WordPress.org

Make WordPress Core

Ticket #40878: 40878.4.diff

File 40878.4.diff, 35.2 KB (added by wpscholar, 17 months ago)

Implemented REST API endpoints for menus, menu items, menu locations, and menu settings.

  • src/wp-includes/post.php

    From 52818fda83f9dd47a2e1a5e7aa349537369317c6 Mon Sep 17 00:00:00 2001
    From: Micah Wood <micah@wpscholar.com>
    Date: Mon, 25 Mar 2019 17:46:55 -0400
    Subject: [PATCH] Implemented REST API endpoints for menus, menu items, menu
     locations, and menu settings.
    
    ---
     src/wp-includes/post.php                      |  17 +-
     src/wp-includes/rest-api.php                  |  18 +
     .../class-wp-rest-menu-items-controller.php   | 292 ++++++++++++++
     ...lass-wp-rest-menu-locations-controller.php | 369 ++++++++++++++++++
     ...class-wp-rest-menu-settings-controller.php | 289 ++++++++++++++
     .../class-wp-rest-menus-controller.php        |  49 +++
     src/wp-includes/taxonomy.php                  |  19 +-
     src/wp-settings.php                           |   4 +
     8 files changed, 1042 insertions(+), 15 deletions(-)
     create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php
     create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-menu-locations-controller.php
     create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-menu-settings-controller.php
     create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-menus-controller.php
    
    diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php
    index eb69ac727b..7bd770b55a 100644
    a b function create_initial_post_types() { 
    123123        register_post_type(
    124124                'nav_menu_item',
    125125                array(
    126                         'labels'           => array(
     126                        'labels'                => array(
    127127                                'name'          => __( 'Navigation Menu Items' ),
    128128                                'singular_name' => __( 'Navigation Menu Item' ),
    129129                        ),
    130                         'public'           => false,
    131                         '_builtin'         => true, /* internal use only. don't use this when registering your own post type. */
    132                         'hierarchical'     => false,
    133                         'rewrite'          => false,
    134                         'delete_with_user' => false,
    135                         'query_var'        => false,
     130                        'public'                => false,
     131                        '_builtin'              => true, /* internal use only. don't use this when registering your own post type. */
     132                        'hierarchical'          => false,
     133                        'rewrite'               => false,
     134                        'delete_with_user'      => false,
     135                        'query_var'             => false,
     136                        'show_in_rest'          => true,
     137                        'rest_base'             => 'menu-items',
     138                        'rest_controller_class' => 'WP_REST_Posts_Controller'
    136139                )
    137140        );
    138141
  • src/wp-includes/rest-api.php

    diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php
    index 1c73b97824..c4f83e87b6 100644
    a b function create_initial_rest_routes() { 
    276276        $controller = new WP_REST_Themes_Controller;
    277277        $controller->register_routes();
    278278
     279        // Menu Items.
     280        $controller = new WP_REST_Menu_Items_Controller( 'nav_menu_item' );
     281        $controller->register_routes();
     282
     283        // Menus.
     284        $controller = new WP_REST_Menus_Controller( 'nav_menu' );
     285        $controller->register_routes();
     286
     287        // Menu Locations.
     288        $controller = new WP_REST_Menu_Locations_Controller();
     289        $controller->register_routes();
     290
     291        // Menu Settings.
     292        $controller = new WP_REST_Menu_Settings_Controller();
     293        $controller->register_routes();
     294
     295
     296
    279297}
    280298
    281299/**
  • new file src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php

    diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php
    new file mode 100644
    index 0000000000..699da2ab73
    - +  
     1<?php
     2/**
     3 * REST API: WP_REST_Menu_Items_Controller class
     4 *
     5 * @package WordPress
     6 * @subpackage REST_API
     7 * @since 5.x
     8 */
     9
     10/**
     11 * Core controller used to access menu items via the REST API.
     12 *
     13 * @since 5.x
     14 *
     15 * @see WP_REST_Posts_Controller
     16 */
     17class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller {
     18
     19        /**
     20         * Constructor
     21         *
     22         * @since 5.x
     23         *
     24         * @param string $post_type Post type name
     25         */
     26        public function __construct( $post_type ) {
     27
     28                parent::__construct( $post_type );
     29
     30                add_filter( "rest_pre_insert_{$post_type}", array( $this, 'pre_insert_item' ), 0, 2 );
     31                add_action( "rest_insert_{$post_type}", array( $this, 'post_insert_item' ), 0, 2 );
     32                add_action( "rest_after_insert_{$post_type}", array( $this, 'post_insert_item' ), 0, 2 );
     33                add_filter( "rest_prepare_{$post_type}", array( $this, 'prepare_item_response' ), 0, 3 );
     34
     35        }
     36
     37        /**
     38         * Sets the menu item properties before storing the post in the database.
     39         *
     40         * @since 5.x
     41         *
     42         * @param stdClass        $prepared_post Post
     43         * @param WP_REST_Request $request Request
     44         *
     45         * @return stdClass|WP_Error
     46         */
     47        public function pre_insert_item( $prepared_post, $request ) {
     48
     49                $schema = $this->get_item_schema();
     50
     51                // Set the menu item type (stored as meta)
     52                if ( ! empty( $schema['properties']['item_type'] ) && isset( $request['item_type'] ) ) {
     53                        $prepared_post->meta_input['_menu_item_type'] = $request['item_type'];
     54                }
     55
     56                // Set the menu item attr_title (stored as post excerpt)
     57                if ( ! empty( $schema['properties']['attr_title'] ) && isset( $request['attr_title'] ) ) {
     58                        $prepared_post->post_excerpt = $request['attr_title'];
     59                }
     60
     61                // Set the menu item classes (stored as meta)
     62                if ( ! empty( $schema['properties']['classes'] ) && isset( $request['classes'] ) ) {
     63                        $prepared_post->meta_input['_menu_item_classes'] = $request['classes'];
     64                }
     65
     66                // Set the menu item description (stored as post content)
     67                if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
     68                        $prepared_post->post_content = $request['description'];
     69                }
     70
     71                if ( ! empty( $schema['properties']['menus'] ) ) {
     72                        if ( empty( $request['menus'] ) ) {
     73                                // If no menu is set, go ahead and mark as orphaned.
     74                                $prepared_post->meta_input['_menu_item_orphaned'] = (string) time();
     75                        }
     76                }
     77
     78                // Set the menu item object type (stored as meta)
     79                if ( ! empty( $schema['properties']['object'] ) && isset( $request['object'] ) ) {
     80                        $prepared_post->meta_input['_menu_item_object'] = $request['object'];
     81                }
     82
     83                // Set the menu item object id (stored as meta)
     84                if ( ! empty( $schema['properties']['object_id'] ) && isset( $request['object_id'] ) ) {
     85                        $prepared_post->meta_input['_menu_item_object_id'] = $request['object_id'];
     86                }
     87
     88                // Set the menu item parent (stored as meta)
     89                if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) {
     90                        $prepared_post->meta_input['_menu_item_menu_item_parent'] = $request['parent'];
     91                }
     92
     93                // Set the menu item parent (stored as meta)
     94                if ( ! empty( $schema['properties']['target'] ) && isset( $request['target'] ) ) {
     95                        $prepared_post->meta_input['_menu_item_target'] = $request['target'];
     96                }
     97
     98                // Set the menu item URL (stored as meta)
     99                if ( ! empty( $schema['properties']['url'] ) && isset( $request['url'] ) ) {
     100                        $prepared_post->meta_input['_menu_item_url'] = $request['url'];
     101                }
     102
     103                // Set the menu item xfn (stored as meta)
     104                if ( ! empty( $schema['properties']['xfn'] ) && isset( $request['xfn'] ) ) {
     105                        $prepared_post->meta_input['_menu_item_xfn'] = $request['xfn'];
     106                }
     107
     108                return $prepared_post;
     109        }
     110
     111        /**
     112         * Handles special cases after a post has been updated.
     113         *
     114         * @since 5.x
     115         *
     116         * @param WP_Post         $post Post object.
     117         * @param WP_REST_Request $request Request object.
     118         */
     119        public function post_insert_item( $post, $request ) {
     120                // If a menu is set, make sure we remove the orphaned marker.
     121                if ( ! empty( $request['menus'] ) ) {
     122                        delete_post_meta( $post->ID, '_menu_item_orphaned' );
     123                }
     124        }
     125
     126        /**
     127         * Prepares a single item response.
     128         *
     129         * @since 5.x
     130         *
     131         * @param WP_REST_Response $response Response object.
     132         * @param WP_Post          $post Post object.
     133         * @param WP_REST_Request  $request Request object.
     134         *
     135         * @return WP_REST_Response Response object.
     136         */
     137        public function prepare_item_response( $response, $post, $request ) {
     138
     139                $menu_item = wp_setup_nav_menu_item( $post );
     140
     141                $response->data['attr_title']        = $menu_item->attr_title; // Same as post_excerpt
     142                $response->data['classes']           = $menu_item->classes;
     143                $response->data['description']       = $menu_item->description; // Same as post_content
     144                $response->data['item_type']         = $menu_item->type; // Using 'item_type' since 'type' already exists.
     145                $response->data['item_type_label']   = $menu_item->type_label; // Using 'item_type_label' to match up with 'item_type' - IS READ ONLY!
     146                $response->data['object']            = $menu_item->object;
     147                $response->data['object_id']         = absint( $menu_item->object_id ); // Usually is a string, but lets expose as an integer.
     148                $response->data['parent']            = absint( $menu_item->menu_item_parent ); // Same as post_parent, expose as integer
     149                $response->data['target']            = $menu_item->target;
     150                $response->data['title']['rendered'] = $menu_item->title; // Overwrites 'title' (should be same as post_title)
     151                $response->data['url']               = $menu_item->url;
     152                $response->data['xfn']               = $menu_item->xfn;
     153
     154                return $response;
     155        }
     156
     157        /**
     158         * Retrieves the menu item's schema, conforming to JSON Schema.
     159         *
     160         * @since 5.x
     161         *
     162         * @return array Item schema as an array.
     163         */
     164        public function get_item_schema() {
     165
     166                $schema = parent::get_item_schema();
     167
     168                $schema['properties']['attr_title'] = array(
     169                        'description' => __( 'Text for the title attribute of the link element for this menu item.' ),
     170                        'type'        => 'string',
     171                        'context'     => array( 'view', 'edit', 'embed' ),
     172                        'arg_options' => array(
     173                                'sanitize_callback' => 'sanitize_text_field',
     174                        ),
     175                );
     176
     177                $schema['properties']['classes'] = array(
     178                        'description' => __( 'Class names for the link element of this menu item.' ),
     179                        'type'        => 'array',
     180                        'context'     => array( 'view', 'edit', 'embed' ),
     181                        'arg_options' => array(
     182                                'sanitize_callback' => function ( $value ) {
     183                                        return array_map( 'sanitize_html_class', explode( ' ', $value ) );
     184                                },
     185                        ),
     186                );
     187
     188                $schema['properties']['description'] = array(
     189                        'description' => __( 'The description of this menu item.' ),
     190                        'type'        => 'string',
     191                        'context'     => array( 'view', 'edit', 'embed' ),
     192                        'arg_options' => array(
     193                                'sanitize_callback' => 'sanitize_text_field',
     194                        ),
     195                );
     196
     197                $schema['properties']['item_type'] = array(
     198                        'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ),
     199                        'type'        => 'string',
     200                        'context'     => array( 'view', 'edit', 'embed' ),
     201                        'default'     => 'custom',
     202                        'arg_options' => array(
     203                                'sanitize_callback' => 'sanitize_key',
     204                        ),
     205                        'required'    => true,
     206                );
     207
     208                $schema['properties']['item_type_label'] = array(
     209                        'description' => __( 'The singular label used to describe this type of menu item.' ),
     210                        'type'        => 'string',
     211                        'context'     => array( 'view', 'edit', 'embed' ),
     212                        'readonly'    => true,
     213                );
     214
     215                $schema['properties']['menu_order'] = array(
     216                        'description' => __( 'The order of the object in relation to other objects of its type.' ),
     217                        'type'        => 'integer',
     218                        'context'     => array( 'view', 'edit', 'embed' ),
     219                );
     220
     221                $schema['properties']['menus'] = array(
     222                        'description' => __( 'The IDs representing the menus to which this menu item should be added.' ),
     223                        'type'        => 'array',
     224                        'context'     => array( 'view', 'edit', 'embed' ),
     225                );
     226
     227                $schema['properties']['object'] = array(
     228                        'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."' ),
     229                        'type'        => 'string',
     230                        'context'     => array( 'view', 'edit' ),
     231                        'default'     => 'custom',
     232                        'arg_options' => array(
     233                                'sanitize_callback' => 'sanitize_key',
     234                        ),
     235                        'required'    => true,
     236                );
     237
     238                $schema['properties']['object_id'] = array(
     239                        'description' => __( 'The DB ID of the original object this menu item represents, e.g. ID for posts and term_id for categories.' ),
     240                        'type'        => 'integer',
     241                        'context'     => array( 'view', 'edit' ),
     242                        'default'     => 0,
     243                        'required'    => true,
     244                );
     245
     246                $schema['properties']['parent'] = array(
     247                        'description' => __( "The DB ID of the nav_menu_item that is this item's menu parent, if any. 0 otherwise." ),
     248                        'type'        => 'integer',
     249                        'context'     => array( 'view', 'edit' ),
     250                );
     251
     252                $schema['properties']['target'] = array(
     253                        'description' => __( 'The target attribute of the link element for this menu item.' ),
     254                        'type'        => 'string',
     255                        'context'     => array( 'view', 'edit', 'embed' ),
     256                        'enum'        => array(
     257                                '_blank',
     258                                '',
     259                        ),
     260                );
     261
     262                $schema['properties']['title']['required'] = true;
     263
     264                $schema['properties']['url'] = array(
     265                        'description' => __( 'The URL to which this menu item points.' ),
     266                        'type'        => 'string',
     267                        'format'      => 'uri',
     268                        'context'     => array( 'view', 'edit', 'embed' ),
     269                        'arg_options' => array(
     270                                'sanitize_callback' => 'esc_url_raw',
     271                        ),
     272                );
     273
     274                $schema['properties']['xfn'] = array(
     275                        'description' => __( 'The XFN relationship expressed in the link of this menu item.' ),
     276                        'type'        => 'string',
     277                        'context'     => array( 'view', 'edit', 'embed' ),
     278                        'arg_options' => array(
     279                                'sanitize_callback' => function ( $value ) {
     280                                        return implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $value ) ) );
     281                                },
     282                        ),
     283                );
     284
     285                unset(
     286                        $schema['properties']['link'],
     287                        $schema['properties']['password']
     288                );
     289
     290                return $schema;
     291        }
     292}
  • new file src/wp-includes/rest-api/endpoints/class-wp-rest-menu-locations-controller.php

    diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-locations-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-locations-controller.php
    new file mode 100644
    index 0000000000..4ebd8beee0
    - +  
     1<?php
     2/**
     3 * REST API: WP_REST_Menu_Locations_Controller class
     4 *
     5 * @package WordPress
     6 * @subpackage REST_API
     7 * @since 5.x
     8 */
     9
     10/**
     11 * Core controller used to access menu locations via the REST API.
     12 *
     13 * @since 5.x
     14 *
     15 * @see WP_REST_Controller
     16 */
     17class WP_REST_Menu_Locations_Controller extends WP_REST_Controller {
     18
     19        /**
     20         * The namespace of this controller's route.
     21         *
     22         * @since 5.x
     23         *
     24         * @var string
     25         */
     26        protected $namespace = 'wp/v2';
     27
     28        /**
     29         * The base of this controller's route.
     30         *
     31         * @since 5.x
     32         *
     33         * @var string
     34         */
     35        protected $rest_base = 'menu-locations';
     36
     37        /**
     38         * Registers the routes for the objects of the controller.
     39         *
     40         * @since 5.x
     41         *
     42         * @see register_rest_route()
     43         */
     44        public function register_routes() {
     45
     46                register_rest_route(
     47                        $this->namespace,
     48                        '/' . $this->rest_base,
     49                        array(
     50                                array(
     51                                        'methods'             => WP_REST_Server::READABLE,
     52                                        'callback'            => array( $this, 'get_items' ),
     53                                        'permission_callback' => array( $this, 'get_items_permissions_check' ),
     54                                        'args'                => $this->get_collection_params(),
     55                                ),
     56                                'schema' => array( $this, 'get_public_item_schema' ),
     57                        )
     58                );
     59
     60                register_rest_route(
     61                        $this->namespace,
     62                        '/' . $this->rest_base . '/(?P<location>[\w-]+)',
     63                        array(
     64                                'args'   => array(
     65                                        'location' => array(
     66                                                'description' => __( 'An alphanumeric identifier for the menu location.' ),
     67                                                'type'        => 'string',
     68                                        ),
     69                                ),
     70                                array(
     71                                        'methods'             => WP_REST_Server::READABLE,
     72                                        'callback'            => array( $this, 'get_item' ),
     73                                        'permission_callback' => array( $this, 'get_item_permissions_check' ),
     74                                        'args'                => array(
     75                                                'context' => $this->get_context_param( array( 'default' => 'view' ) ),
     76                                        ),
     77                                ),
     78                                array(
     79                                        'methods'             => WP_REST_Server::EDITABLE,
     80                                        'callback'            => array( $this, 'update_item' ),
     81                                        'permission_callback' => array( $this, 'update_item_permissions_check' ),
     82                                        'args'                => array(
     83                                                'menu' => array(
     84                                                        'validate_callback' => function( $id ) {
     85                                                                return 0 === $id || false !== wp_get_nav_menu_object( $id );
     86                                                        },
     87                                                        'required'          => true,
     88                                                ),
     89                                        ),
     90                                ),
     91                                'schema' => array( $this, 'get_public_item_schema' ),
     92                        )
     93                );
     94        }
     95
     96        /**
     97         * Checks whether a given request has permission to read menu locations.
     98         *
     99         * @since 5.x
     100         *
     101         * @param WP_REST_Request $request Full details about the request.
     102         *
     103         * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
     104         */
     105        public function get_items_permissions_check( $request ) {
     106                if ( ! current_user_can( 'edit_theme_options' ) ) {
     107                        return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
     108                }
     109
     110                return true;
     111        }
     112
     113        /**
     114         * Retrieves all menu locations, depending on user context.
     115         *
     116         * @since 5.x
     117         *
     118         * @param WP_REST_Request $request Full details about the request.
     119         *
     120         * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
     121         */
     122        public function get_items( $request ) {
     123
     124                $data = array();
     125
     126                $locations = $this->get_locations();
     127
     128                foreach ( $locations as $location ) {
     129                        $item                    = $this->prepare_item_for_response( $location, $request );
     130                        $data[ $location->name ] = $this->prepare_response_for_collection( $item );
     131                }
     132
     133                return rest_ensure_response( $data );
     134        }
     135
     136        /**
     137         * Checks if a given request has access to read a menu location.
     138         *
     139         * @since 5.x
     140         *
     141         * @param WP_REST_Request $request Full details about the request.
     142         *
     143         * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
     144         */
     145        public function get_item_permissions_check( $request ) {
     146
     147                if ( ! current_user_can( 'edit_theme_options' ) ) {
     148                        return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
     149                }
     150
     151                if ( ! array_key_exists( $request['location'], get_registered_nav_menus() ) ) {
     152                        return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
     153                }
     154
     155                return true;
     156        }
     157
     158        /**
     159         * Retrieves a specific menu location.
     160         *
     161         * @since 5.x
     162         *
     163         * @param WP_REST_Request $request Full details about the request.
     164         *
     165         * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
     166         */
     167        public function get_item( $request ) {
     168
     169                $locations = $this->get_locations();
     170                if ( ! array_key_exists( $request['location'], $locations ) ) {
     171                        return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
     172                }
     173
     174                $item = $locations[ $request['location'] ];
     175
     176                return rest_ensure_response( $this->prepare_item_for_response( $item, $request ) );
     177        }
     178
     179        /**
     180         * Checks if a given request has access to update a menu location.
     181         *
     182         * @since 5.x
     183         *
     184         * @param WP_REST_Request $request Full details about the request.
     185         *
     186         * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
     187         */
     188        public function update_item_permissions_check( $request ) {
     189
     190                if ( ! current_user_can( 'edit_theme_options' ) ) {
     191                        return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to manage menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
     192                }
     193
     194                if ( ! array_key_exists( $request['location'], get_registered_nav_menus() ) ) {
     195                        return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
     196                }
     197
     198                return true;
     199        }
     200
     201        /**
     202         * Updates one item from the collection.
     203         *
     204         * @since 5.x
     205         *
     206         * @param WP_REST_Request $request REST request
     207         *
     208         * @return WP_REST_Response|WP_Error
     209         */
     210        public function update_item( $request ) {
     211
     212                $locations      = $this->get_locations();
     213                $location       = $locations[ $request['location'] ];
     214                $location->menu = $request['menu'];
     215
     216                // Update theme mod
     217                $theme_mod                    = get_nav_menu_locations();
     218                $theme_mod[ $location->name ] = $request['menu'];
     219                set_theme_mod( 'nav_menu_locations', $theme_mod );
     220
     221                return rest_ensure_response( $this->prepare_item_for_response( $location, $request ) );
     222        }
     223
     224        /**
     225         * Prepares a menu location object for serialization.
     226         *
     227         * @since 5.x
     228         *
     229         * @param stdClass        $location  Menu location data.
     230         * @param WP_REST_Request $request Full details about the request.
     231         *
     232         * @return WP_REST_Response Response object.
     233         */
     234        public function prepare_item_for_response( $location, $request ) {
     235
     236                $item = (array) $location;
     237
     238                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     239                $data    = $this->add_additional_fields_to_object( $item, $request );
     240                $data    = $this->filter_response_by_context( $data, $context );
     241
     242                $response = rest_ensure_response( $data );
     243
     244                $response->add_links( $this->prepare_links( $item ) );
     245
     246                /**
     247                 * Filters a menu location returned from the REST API.
     248                 *
     249                 * Allows modification of the menu location data right before it is
     250                 * returned.
     251                 *
     252                 * @since 5.x
     253                 *
     254                 * @param WP_REST_Response $response The response object.
     255                 * @param stdClass        $location The original location object.
     256                 * @param WP_REST_Request  $request  Request used to generate the response.
     257                 */
     258                return apply_filters( 'rest_prepare_nav_menu_location', $response, $location, $request );
     259        }
     260
     261        /**
     262         * Retrieves the menu location's schema, conforming to JSON Schema.
     263         *
     264         * @since 5.x
     265         *
     266         * @return array Item schema data.
     267         */
     268        public function get_item_schema() {
     269
     270                $schema = array(
     271                        '$schema'    => 'http://json-schema.org/draft-04/schema#',
     272                        'title'      => 'menu-location',
     273                        'type'       => 'object',
     274                        'properties' => array(
     275                                'name'        => array(
     276                                        'description' => __( 'The name of the menu location.' ),
     277                                        'type'        => 'string',
     278                                        'context'     => array( 'view', 'edit' ),
     279                                        'readonly'    => true,
     280                                ),
     281                                'description' => array(
     282                                        'description' => __( 'The description of the menu location.' ),
     283                                        'type'        => 'string',
     284                                        'context'     => array( 'view', 'edit' ),
     285                                        'readonly'    => true,
     286                                ),
     287                                'menu'        => array(
     288                                        'description' => __( 'The ID of the assigned menu.' ),
     289                                        'type'        => 'int',
     290                                        'context'     => array( 'embed', 'view', 'edit' ),
     291                                ),
     292                        ),
     293                );
     294
     295                return $this->add_additional_fields_schema( $schema );
     296        }
     297
     298        /**
     299         * Retrieves the query params for collections.
     300         *
     301         * @since 5.x
     302         *
     303         * @return array Collection parameters.
     304         */
     305        public function get_collection_params() {
     306                return array(
     307                        'context' => $this->get_context_param( array( 'default' => 'view' ) ),
     308                );
     309        }
     310
     311        /**
     312         * Prepares links for the request.
     313         *
     314         * @since 5.x
     315         *
     316         * @param array $location Menu location.
     317         *
     318         * @return array Links for the given menu location.
     319         */
     320        protected function prepare_links( $location ) {
     321                $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
     322
     323                // Entity meta.
     324                $links = array(
     325                        'self'       => array(
     326                                'href' => rest_url( trailingslashit( $base ) . $location['name'] ),
     327                        ),
     328                        'collection' => array(
     329                                'href' => rest_url( $base ),
     330                        ),
     331                );
     332
     333                if ( $location['menu'] > 0 ) {
     334                        $links['menu'] = array(
     335                                'href'       => rest_url( sprintf( '%s/menus/%d', $this->namespace, $location['menu'] ) ),
     336                                'embeddable' => true,
     337                        );
     338                }
     339
     340                return $links;
     341        }
     342
     343        /**
     344         * Get all nav menu locations as well as assigned menu IDs.
     345         *
     346         * @since 5.x
     347         *
     348         * @return stdClass[]
     349         */
     350        protected function get_locations() {
     351
     352                $locations = array();
     353
     354                $registered = get_registered_nav_menus();
     355                $assigned   = get_nav_menu_locations();
     356
     357                foreach ( $registered as $name => $description ) {
     358                        $locations[ $name ] = (object) array(
     359                                'name'        => $name,
     360                                'description' => $description,
     361                                'menu'        => isset( $assigned[ $name ] ) ? absint( $assigned[ $name ] ) : 0,
     362                        );
     363                }
     364
     365                return $locations;
     366
     367        }
     368
     369}
  • new file src/wp-includes/rest-api/endpoints/class-wp-rest-menu-settings-controller.php

    diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-settings-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-settings-controller.php
    new file mode 100644
    index 0000000000..a860efbd70
    - +  
     1<?php
     2/**
     3 * REST API: WP_REST_Menu_Settings_Controller class
     4 *
     5 * @package WordPress
     6 * @subpackage REST_API
     7 * @since 5.x
     8 */
     9
     10/**
     11 * Core controller used to access menu settings via the REST API.
     12 *
     13 * @since 5.x
     14 *
     15 * @see WP_REST_Posts_Controller
     16 */
     17class WP_REST_Menu_Settings_Controller extends WP_REST_Controller {
     18
     19        /**
     20         * The namespace of this controller's route.
     21         *
     22         * @since 5.x
     23         *
     24         * @var string
     25         */
     26        protected $namespace = 'wp/v2';
     27
     28        /**
     29         * The base of this controller's route.
     30         *
     31         * @since 5.x
     32         *
     33         * @var string
     34         */
     35        protected $rest_base = 'menus';
     36
     37        /**
     38         * Registers the routes for the objects of the controller.
     39         *
     40         * @since 5.x
     41         */
     42        public function register_routes() {
     43
     44                register_rest_route(
     45                        $this->namespace,
     46                        "/{$this->rest_base}/(?P<id>[\d]+)/settings",
     47                        array(
     48                                'args'   => array(
     49                                        'id' => array(
     50                                                'description' => __( 'Unique identifier for the menu.' ),
     51                                                'type'        => 'integer',
     52                                        ),
     53                                ),
     54                                array(
     55                                        'methods'             => WP_REST_Server::READABLE,
     56                                        'callback'            => array( $this, 'get_item' ),
     57                                        'permission_callback' => array( $this, 'get_item_permissions_check' ),
     58                                        'args'                => array(
     59                                                'context' => $this->get_context_param( array( 'default' => 'view' ) ),
     60                                        ),
     61                                ),
     62                                array(
     63                                        'methods'             => WP_REST_Server::EDITABLE,
     64                                        'callback'            => array( $this, 'update_item' ),
     65                                        'permission_callback' => array( $this, 'update_item_permissions_check' ),
     66                                        'args'                => array(
     67                                                'auto_add' => array(
     68                                                        'sanitize_callback' => 'wp_validate_boolean',
     69                                                        'validate_callback' => 'rest_is_boolean',
     70                                                        'required'          => true,
     71                                                ),
     72                                        ),
     73                                ),
     74                                'schema' => array( $this, 'get_public_item_schema' ),
     75                        )
     76                );
     77
     78        }
     79
     80        /**
     81         * Retrieves one item from the collection.
     82         *
     83         * @since 5.x
     84         *
     85         * @param WP_REST_Request $request REST request
     86         *
     87         * @return WP_REST_Response|WP_Error
     88         */
     89        public function get_item( $request ) {
     90
     91                $menu_id = $request['id'];
     92                $menu    = wp_get_nav_menu_object( $menu_id );
     93
     94                if ( ! $menu ) {
     95                        return new WP_Error( 'rest_invalid_menu_id', __( 'Invalid menu ID' ), array( 'status' => 404 ) );
     96                }
     97
     98                $item = array(
     99                        'id'       => $menu_id,
     100                        'auto_add' => false,
     101                );
     102
     103                $nav_menu_options = (array) get_option( 'nav_menu_options' );
     104
     105                if ( isset( $nav_menu_options['auto_add'] ) ) {
     106                        if ( in_array( $menu_id, $nav_menu_options['auto_add'], true ) ) {
     107                                $item['auto_add'] = (bool) $nav_menu_options['auto_add'];
     108                        }
     109                }
     110
     111                return $this->prepare_item_for_response( $item, $request );
     112        }
     113
     114        /**
     115         * Checks if a given request has access to get a specific item.
     116         *
     117         * @since 5.x
     118         *
     119         * @param WP_REST_Request $request REST request
     120         *
     121         * @return bool|WP_Error
     122         */
     123        public function get_item_permissions_check( $request ) {
     124                if ( ! current_user_can( 'edit_theme_options' ) ) {
     125                        return new WP_Error( 'rest_forbidden_context', __( "Sorry, you are not allowed to view this menu's settings." ), array( 'status' => rest_authorization_required_code() ) );
     126                }
     127
     128                return true;
     129        }
     130
     131        /**
     132         * Updates one item from the collection.
     133         *
     134         * @since 5.x
     135         *
     136         * @param WP_REST_Request $request REST request
     137         *
     138         * @return WP_REST_Response|WP_Error
     139         */
     140        public function update_item( $request ) {
     141
     142                $menu_id = $request['id'];
     143                $menu    = wp_get_nav_menu_object( $menu_id );
     144
     145                if ( ! $menu ) {
     146                        return new WP_Error( 'rest_invalid_menu_id', __( 'Invalid menu ID' ), array( 'status' => 404 ) );
     147                }
     148
     149                $item = array(
     150                        'id'       => $menu_id,
     151                        'auto_add' => false,
     152                );
     153
     154                $nav_menu_options = (array) get_option( 'nav_menu_options' );
     155
     156                if ( isset( $nav_menu_options['auto_add'] ) ) {
     157                        if ( in_array( $menu_id, $nav_menu_options['auto_add'], true ) ) {
     158                                $item['auto_add'] = true;
     159                        }
     160                } else {
     161                        $nav_menu_options['auto_add'] = array();
     162                }
     163
     164                // Update auto add pages setting
     165                $item['auto_add'] = $request->get_param( 'auto_add' );
     166                if ( $item['auto_add'] ) {
     167                        $nav_menu_options['auto_add'][] = $menu_id;
     168                } else {
     169                        $key = array_search( $menu_id, $nav_menu_options['auto_add'], true );
     170                        if ( false !== $key ) {
     171                                unset( $nav_menu_options['auto_add'][ $key ] );
     172                        }
     173                }
     174                update_option( 'nav_menu_options', $nav_menu_options );
     175
     176                return $this->prepare_item_for_response( $item, $request );
     177        }
     178
     179        /**
     180         * Checks if a given request has access to update a specific item.
     181         *
     182         * @since 5.x
     183         *
     184         * @param WP_REST_Request $request REST request
     185         *
     186         * @return bool|WP_Error
     187         */
     188        public function update_item_permissions_check( $request ) {
     189                if ( ! current_user_can( 'edit_theme_options' ) ) {
     190                        return new WP_Error( 'rest_cannot_update', __( "Sorry, you are not allowed to edit this menu's settings." ), array( 'status' => rest_authorization_required_code() ) );
     191                }
     192
     193                return true;
     194        }
     195
     196        /**
     197         * Prepares the item for the REST response.
     198         *
     199         * @since 5.x
     200         *
     201         * @param array            $item Menu settings
     202         * @param WP_REST_Request $request REST request
     203         *
     204         * @return WP_REST_Response|WP_Error
     205         */
     206        public function prepare_item_for_response( $item, $request ) {
     207
     208                $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
     209                $data    = $this->add_additional_fields_to_object( $item, $request );
     210                $data    = $this->filter_response_by_context( $data, $context );
     211
     212                $response = rest_ensure_response( $data );
     213
     214                $response->add_links( $this->prepare_links( $item ) );
     215
     216                /**
     217                 * Filters a menu location returned from the REST API.
     218                 *
     219                 * Allows modification of the menu's settings data right before it is
     220                 * returned.
     221                 *
     222                 * @since 5.x
     223                 *
     224                 * @param WP_REST_Response $response The response object.
     225                 * @param array            $item The original status object.
     226                 * @param WP_REST_Request  $request  Request used to generate the response.
     227                 */
     228                return apply_filters( 'rest_prepare_nav_menu_settings', $response, $item, $request );
     229        }
     230
     231        /**
     232         * Prepares links for the request.
     233         *
     234         * @since 5.x
     235         *
     236         * @param array $item Menu settings.
     237         *
     238         * @return array Links for the given menu settings.
     239         */
     240        protected function prepare_links( $item ) {
     241
     242                $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
     243
     244                // Entity meta.
     245                $links = array(
     246                        'self' => array(
     247                                'href' => rest_url( trailingslashit( $base ) . $item['id'] . '/settings' ),
     248                        ),
     249                        'menu' => array(
     250                                'href'       => rest_url( trailingslashit( $base ) . $item['id'] ),
     251                                'embeddable' => true,
     252                        ),
     253                );
     254
     255                return $links;
     256        }
     257
     258        /**
     259         * Retrieves the menu settings schema, conforming to JSON Schema.
     260         *
     261         * @since 5.x
     262         *
     263         * @return array Item schema data.
     264         */
     265        public function get_item_schema() {
     266
     267                $schema = array(
     268                        '$schema'    => 'http://json-schema.org/draft-04/schema#',
     269                        'title'      => 'menu-settings',
     270                        'type'       => 'object',
     271                        'properties' => array(
     272                                'id'       => array(
     273                                        'description' => 'Unique identifier for the menu.',
     274                                        'type'        => 'integer',
     275                                        'context'     => array( 'view', 'edit' ),
     276                                        'readonly'    => true,
     277                                ),
     278                                'auto_add' => array(
     279                                        'description' => __( 'Whether or not to automatically add top level pages to the menu.' ),
     280                                        'type'        => 'boolean',
     281                                        'context'     => array( 'view', 'edit' ),
     282                                ),
     283                        ),
     284                );
     285
     286                return $this->add_additional_fields_schema( $schema );
     287        }
     288
     289}
  • new file src/wp-includes/rest-api/endpoints/class-wp-rest-menus-controller.php

    diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menus-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menus-controller.php
    new file mode 100644
    index 0000000000..a3433291bc
    - +  
     1<?php
     2/**
     3 * REST API: WP_REST_Menus_Controller class
     4 *
     5 * @package WordPress
     6 * @subpackage REST_API
     7 * @since 5.x
     8 */
     9
     10/**
     11 * Core controller used to access menus via the REST API.
     12 *
     13 * @since 5.x
     14 *
     15 * @see WP_REST_Posts_Controller
     16 */
     17class WP_REST_Menus_Controller extends WP_REST_Terms_Controller {
     18
     19        /**
     20         * Prepares links for the request.
     21         *
     22         * @since 5.x
     23         *
     24         * @param object $term Term object.
     25         * @return array Links for the given term.
     26         */
     27        protected function prepare_links( $term ) {
     28
     29                $links = parent::prepare_links( $term );
     30
     31                // Let's make sure that menu items are embeddable for a menu collection.
     32                if ( array_key_exists( 'https://api.w.org/post_type', $links ) ) {
     33                        $post_type_links = $links['https://api.w.org/post_type'];
     34
     35                        foreach ( $post_type_links as $index => $post_type_link ) {
     36                                if ( ! array_key_exists( 'href', $post_type_link ) || strpos( $post_type_link['href'], '/menu-items?' ) === false ) {
     37                                        continue;
     38                                }
     39
     40                                $post_type_links[ $index ]['embeddable'] = true;
     41                        }
     42
     43                        $links['https://api.w.org/post_type'] = $post_type_links;
     44                }
     45
     46                return $links;
     47        }
     48
     49}
  • src/wp-includes/taxonomy.php

    diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php
    index 12864b343a..266682a367 100644
    a b function create_initial_taxonomies() { 
    107107                'nav_menu',
    108108                'nav_menu_item',
    109109                array(
    110                         'public'            => false,
    111                         'hierarchical'      => false,
    112                         'labels'            => array(
     110                        'public'                => false,
     111                        'hierarchical'          => false,
     112                        'labels'                => array(
    113113                                'name'          => __( 'Navigation Menus' ),
    114114                                'singular_name' => __( 'Navigation Menu' ),
    115115                        ),
    116                         'query_var'         => false,
    117                         'rewrite'           => false,
    118                         'show_ui'           => false,
    119                         '_builtin'          => true,
    120                         'show_in_nav_menus' => false,
     116                        'query_var'             => false,
     117                        'rewrite'               => false,
     118                        'show_ui'               => false,
     119                        '_builtin'              => true,
     120                        'show_in_nav_menus'     => false,
     121                        'show_in_rest'          => true,
     122                        'rest_base'             => 'menus',
     123                        'rest_controller_class' => 'WP_REST_Menus_Controller',
    121124                )
    122125        );
    123126
  • src/wp-settings.php

    diff --git a/src/wp-settings.php b/src/wp-settings.php
    index fa9f9a3a25..42680af27f 100644
    a b require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-blocks-controller. 
    251251require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-renderer-controller.php' );
    252252require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' );
    253253require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-themes-controller.php' );
     254require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-items-controller.php' );
     255require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-locations-controller.php' );
     256require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-settings-controller.php' );
     257require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menus-controller.php' );
    254258require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' );
    255259require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php' );
    256260require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php' );