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/src/wp-includes/post.php
+++ b/src/wp-includes/post.php
@@ -123,16 +123,19 @@ function create_initial_post_types() {
 	register_post_type(
 		'nav_menu_item',
 		array(
-			'labels'           => array(
+			'labels'                => array(
 				'name'          => __( 'Navigation Menu Items' ),
 				'singular_name' => __( 'Navigation Menu Item' ),
 			),
-			'public'           => false,
-			'_builtin'         => true, /* internal use only. don't use this when registering your own post type. */
-			'hierarchical'     => false,
-			'rewrite'          => false,
-			'delete_with_user' => false,
-			'query_var'        => false,
+			'public'                => false,
+			'_builtin'              => true, /* internal use only. don't use this when registering your own post type. */
+			'hierarchical'          => false,
+			'rewrite'               => false,
+			'delete_with_user'      => false,
+			'query_var'             => false,
+			'show_in_rest'          => true,
+			'rest_base'             => 'menu-items',
+			'rest_controller_class' => 'WP_REST_Posts_Controller'
 		)
 	);
 
diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php
index 1c73b97824..c4f83e87b6 100644
--- a/src/wp-includes/rest-api.php
+++ b/src/wp-includes/rest-api.php
@@ -276,6 +276,24 @@ function create_initial_rest_routes() {
 	$controller = new WP_REST_Themes_Controller;
 	$controller->register_routes();
 
+	// Menu Items.
+	$controller = new WP_REST_Menu_Items_Controller( 'nav_menu_item' );
+	$controller->register_routes();
+
+	// Menus.
+	$controller = new WP_REST_Menus_Controller( 'nav_menu' );
+	$controller->register_routes();
+
+	// Menu Locations.
+	$controller = new WP_REST_Menu_Locations_Controller();
+	$controller->register_routes();
+
+	// Menu Settings.
+	$controller = new WP_REST_Menu_Settings_Controller();
+	$controller->register_routes();
+
+
+
 }
 
 /**
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
--- /dev/null
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php
@@ -0,0 +1,292 @@
+<?php
+/**
+ * REST API: WP_REST_Menu_Items_Controller class
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 5.x
+ */
+
+/**
+ * Core controller used to access menu items via the REST API.
+ *
+ * @since 5.x
+ *
+ * @see WP_REST_Posts_Controller
+ */
+class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller {
+
+	/**
+	 * Constructor
+	 *
+	 * @since 5.x
+	 *
+	 * @param string $post_type Post type name
+	 */
+	public function __construct( $post_type ) {
+
+		parent::__construct( $post_type );
+
+		add_filter( "rest_pre_insert_{$post_type}", array( $this, 'pre_insert_item' ), 0, 2 );
+		add_action( "rest_insert_{$post_type}", array( $this, 'post_insert_item' ), 0, 2 );
+		add_action( "rest_after_insert_{$post_type}", array( $this, 'post_insert_item' ), 0, 2 );
+		add_filter( "rest_prepare_{$post_type}", array( $this, 'prepare_item_response' ), 0, 3 );
+
+	}
+
+	/**
+	 * Sets the menu item properties before storing the post in the database.
+	 *
+	 * @since 5.x
+	 *
+	 * @param stdClass        $prepared_post Post
+	 * @param WP_REST_Request $request Request
+	 *
+	 * @return stdClass|WP_Error
+	 */
+	public function pre_insert_item( $prepared_post, $request ) {
+
+		$schema = $this->get_item_schema();
+
+		// Set the menu item type (stored as meta)
+		if ( ! empty( $schema['properties']['item_type'] ) && isset( $request['item_type'] ) ) {
+			$prepared_post->meta_input['_menu_item_type'] = $request['item_type'];
+		}
+
+		// Set the menu item attr_title (stored as post excerpt)
+		if ( ! empty( $schema['properties']['attr_title'] ) && isset( $request['attr_title'] ) ) {
+			$prepared_post->post_excerpt = $request['attr_title'];
+		}
+
+		// Set the menu item classes (stored as meta)
+		if ( ! empty( $schema['properties']['classes'] ) && isset( $request['classes'] ) ) {
+			$prepared_post->meta_input['_menu_item_classes'] = $request['classes'];
+		}
+
+		// Set the menu item description (stored as post content)
+		if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) {
+			$prepared_post->post_content = $request['description'];
+		}
+
+		if ( ! empty( $schema['properties']['menus'] ) ) {
+			if ( empty( $request['menus'] ) ) {
+				// If no menu is set, go ahead and mark as orphaned.
+				$prepared_post->meta_input['_menu_item_orphaned'] = (string) time();
+			}
+		}
+
+		// Set the menu item object type (stored as meta)
+		if ( ! empty( $schema['properties']['object'] ) && isset( $request['object'] ) ) {
+			$prepared_post->meta_input['_menu_item_object'] = $request['object'];
+		}
+
+		// Set the menu item object id (stored as meta)
+		if ( ! empty( $schema['properties']['object_id'] ) && isset( $request['object_id'] ) ) {
+			$prepared_post->meta_input['_menu_item_object_id'] = $request['object_id'];
+		}
+
+		// Set the menu item parent (stored as meta)
+		if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) {
+			$prepared_post->meta_input['_menu_item_menu_item_parent'] = $request['parent'];
+		}
+
+		// Set the menu item parent (stored as meta)
+		if ( ! empty( $schema['properties']['target'] ) && isset( $request['target'] ) ) {
+			$prepared_post->meta_input['_menu_item_target'] = $request['target'];
+		}
+
+		// Set the menu item URL (stored as meta)
+		if ( ! empty( $schema['properties']['url'] ) && isset( $request['url'] ) ) {
+			$prepared_post->meta_input['_menu_item_url'] = $request['url'];
+		}
+
+		// Set the menu item xfn (stored as meta)
+		if ( ! empty( $schema['properties']['xfn'] ) && isset( $request['xfn'] ) ) {
+			$prepared_post->meta_input['_menu_item_xfn'] = $request['xfn'];
+		}
+
+		return $prepared_post;
+	}
+
+	/**
+	 * Handles special cases after a post has been updated.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_Post         $post Post object.
+	 * @param WP_REST_Request $request Request object.
+	 */
+	public function post_insert_item( $post, $request ) {
+		// If a menu is set, make sure we remove the orphaned marker.
+		if ( ! empty( $request['menus'] ) ) {
+			delete_post_meta( $post->ID, '_menu_item_orphaned' );
+		}
+	}
+
+	/**
+	 * Prepares a single item response.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Response $response Response object.
+	 * @param WP_Post          $post Post object.
+	 * @param WP_REST_Request  $request Request object.
+	 *
+	 * @return WP_REST_Response Response object.
+	 */
+	public function prepare_item_response( $response, $post, $request ) {
+
+		$menu_item = wp_setup_nav_menu_item( $post );
+
+		$response->data['attr_title']        = $menu_item->attr_title; // Same as post_excerpt
+		$response->data['classes']           = $menu_item->classes;
+		$response->data['description']       = $menu_item->description; // Same as post_content
+		$response->data['item_type']         = $menu_item->type; // Using 'item_type' since 'type' already exists.
+		$response->data['item_type_label']   = $menu_item->type_label; // Using 'item_type_label' to match up with 'item_type' - IS READ ONLY!
+		$response->data['object']            = $menu_item->object;
+		$response->data['object_id']         = absint( $menu_item->object_id ); // Usually is a string, but lets expose as an integer.
+		$response->data['parent']            = absint( $menu_item->menu_item_parent ); // Same as post_parent, expose as integer
+		$response->data['target']            = $menu_item->target;
+		$response->data['title']['rendered'] = $menu_item->title; // Overwrites 'title' (should be same as post_title)
+		$response->data['url']               = $menu_item->url;
+		$response->data['xfn']               = $menu_item->xfn;
+
+		return $response;
+	}
+
+	/**
+	 * Retrieves the menu item's schema, conforming to JSON Schema.
+	 *
+	 * @since 5.x
+	 *
+	 * @return array Item schema as an array.
+	 */
+	public function get_item_schema() {
+
+		$schema = parent::get_item_schema();
+
+		$schema['properties']['attr_title'] = array(
+			'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' => __( 'Class names for the link element of this menu item.' ),
+			'type'        => 'array',
+			'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.' ),
+			'type'        => 'string',
+			'context'     => array( 'view', 'edit', 'embed' ),
+			'arg_options' => array(
+				'sanitize_callback' => 'sanitize_text_field',
+			),
+		);
+
+		$schema['properties']['item_type'] = array(
+			'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ),
+			'type'        => 'string',
+			'context'     => array( 'view', 'edit', 'embed' ),
+			'default'     => 'custom',
+			'arg_options' => array(
+				'sanitize_callback' => 'sanitize_key',
+			),
+			'required'    => true,
+		);
+
+		$schema['properties']['item_type_label'] = array(
+			'description' => __( 'The singular label used to describe this type of menu item.' ),
+			'type'        => 'string',
+			'context'     => array( 'view', 'edit', 'embed' ),
+			'readonly'    => true,
+		);
+
+		$schema['properties']['menu_order'] = array(
+			'description' => __( 'The order of the object in relation to other objects of its type.' ),
+			'type'        => 'integer',
+			'context'     => array( 'view', 'edit', 'embed' ),
+		);
+
+		$schema['properties']['menus'] = array(
+			'description' => __( 'The IDs representing the menus to which this menu item should be added.' ),
+			'type'        => 'array',
+			'context'     => array( 'view', 'edit', 'embed' ),
+		);
+
+		$schema['properties']['object'] = array(
+			'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."' ),
+			'type'        => 'string',
+			'context'     => array( 'view', 'edit' ),
+			'default'     => 'custom',
+			'arg_options' => array(
+				'sanitize_callback' => 'sanitize_key',
+			),
+			'required'    => true,
+		);
+
+		$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.' ),
+			'type'        => 'integer',
+			'context'     => array( 'view', 'edit' ),
+			'default'     => 0,
+			'required'    => true,
+		);
+
+		$schema['properties']['parent'] = array(
+			'description' => __( "The DB ID of the nav_menu_item that is this item's menu parent, if any. 0 otherwise." ),
+			'type'        => 'integer',
+			'context'     => array( 'view', 'edit' ),
+		);
+
+		$schema['properties']['target'] = array(
+			'description' => __( 'The target attribute of the link element for this menu item.' ),
+			'type'        => 'string',
+			'context'     => array( 'view', 'edit', 'embed' ),
+			'enum'        => array(
+				'_blank',
+				'',
+			),
+		);
+
+		$schema['properties']['title']['required'] = true;
+
+		$schema['properties']['url'] = array(
+			'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.' ),
+			'type'        => 'string',
+			'context'     => array( 'view', 'edit', 'embed' ),
+			'arg_options' => array(
+				'sanitize_callback' => function ( $value ) {
+					return implode( ' ', array_map( 'sanitize_html_class', explode( ' ', $value ) ) );
+				},
+			),
+		);
+
+		unset(
+			$schema['properties']['link'],
+			$schema['properties']['password']
+		);
+
+		return $schema;
+	}
+}
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
--- /dev/null
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-locations-controller.php
@@ -0,0 +1,369 @@
+<?php
+/**
+ * REST API: WP_REST_Menu_Locations_Controller class
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 5.x
+ */
+
+/**
+ * Core controller used to access menu locations via the REST API.
+ *
+ * @since 5.x
+ *
+ * @see WP_REST_Controller
+ */
+class WP_REST_Menu_Locations_Controller extends WP_REST_Controller {
+
+	/**
+	 * The namespace of this controller's route.
+	 *
+	 * @since 5.x
+	 *
+	 * @var string
+	 */
+	protected $namespace = 'wp/v2';
+
+	/**
+	 * The base of this controller's route.
+	 *
+	 * @since 5.x
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'menu-locations';
+
+	/**
+	 * Registers the routes for the objects of the controller.
+	 *
+	 * @since 5.x
+	 *
+	 * @see register_rest_route()
+	 */
+	public function register_routes() {
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base,
+			array(
+				array(
+					'methods'             => WP_REST_Server::READABLE,
+					'callback'            => array( $this, 'get_items' ),
+					'permission_callback' => array( $this, 'get_items_permissions_check' ),
+					'args'                => $this->get_collection_params(),
+				),
+				'schema' => array( $this, 'get_public_item_schema' ),
+			)
+		);
+
+		register_rest_route(
+			$this->namespace,
+			'/' . $this->rest_base . '/(?P<location>[\w-]+)',
+			array(
+				'args'   => array(
+					'location' => array(
+						'description' => __( 'An alphanumeric identifier for the menu location.' ),
+						'type'        => 'string',
+					),
+				),
+				array(
+					'methods'             => WP_REST_Server::READABLE,
+					'callback'            => array( $this, 'get_item' ),
+					'permission_callback' => array( $this, 'get_item_permissions_check' ),
+					'args'                => array(
+						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+					),
+				),
+				array(
+					'methods'             => WP_REST_Server::EDITABLE,
+					'callback'            => array( $this, 'update_item' ),
+					'permission_callback' => array( $this, 'update_item_permissions_check' ),
+					'args'                => array(
+						'menu' => array(
+							'validate_callback' => function( $id ) {
+								return 0 === $id || false !== wp_get_nav_menu_object( $id );
+							},
+							'required'          => true,
+						),
+					),
+				),
+				'schema' => array( $this, 'get_public_item_schema' ),
+			)
+		);
+	}
+
+	/**
+	 * Checks whether a given request has permission to read menu locations.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|bool True if the request has read access, WP_Error object otherwise.
+	 */
+	public function get_items_permissions_check( $request ) {
+		if ( ! current_user_can( 'edit_theme_options' ) ) {
+			return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Retrieves all menu locations, depending on user context.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
+	 */
+	public function get_items( $request ) {
+
+		$data = array();
+
+		$locations = $this->get_locations();
+
+		foreach ( $locations as $location ) {
+			$item                    = $this->prepare_item_for_response( $location, $request );
+			$data[ $location->name ] = $this->prepare_response_for_collection( $item );
+		}
+
+		return rest_ensure_response( $data );
+	}
+
+	/**
+	 * Checks if a given request has access to read a menu location.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
+	 */
+	public function get_item_permissions_check( $request ) {
+
+		if ( ! current_user_can( 'edit_theme_options' ) ) {
+			return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
+		}
+
+		if ( ! array_key_exists( $request['location'], get_registered_nav_menus() ) ) {
+			return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Retrieves a specific menu location.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
+	 */
+	public function get_item( $request ) {
+
+		$locations = $this->get_locations();
+		if ( ! array_key_exists( $request['location'], $locations ) ) {
+			return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
+		}
+
+		$item = $locations[ $request['location'] ];
+
+		return rest_ensure_response( $this->prepare_item_for_response( $item, $request ) );
+	}
+
+	/**
+	 * Checks if a given request has access to update a menu location.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise.
+	 */
+	public function update_item_permissions_check( $request ) {
+
+		if ( ! current_user_can( 'edit_theme_options' ) ) {
+			return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to manage menu locations.' ), array( 'status' => rest_authorization_required_code() ) );
+		}
+
+		if ( ! array_key_exists( $request['location'], get_registered_nav_menus() ) ) {
+			return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Updates one item from the collection.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request REST request
+	 *
+	 * @return WP_REST_Response|WP_Error
+	 */
+	public function update_item( $request ) {
+
+		$locations      = $this->get_locations();
+		$location       = $locations[ $request['location'] ];
+		$location->menu = $request['menu'];
+
+		// Update theme mod
+		$theme_mod                    = get_nav_menu_locations();
+		$theme_mod[ $location->name ] = $request['menu'];
+		set_theme_mod( 'nav_menu_locations', $theme_mod );
+
+		return rest_ensure_response( $this->prepare_item_for_response( $location, $request ) );
+	}
+
+	/**
+	 * Prepares a menu location object for serialization.
+	 *
+	 * @since 5.x
+	 *
+	 * @param stdClass        $location  Menu location data.
+	 * @param WP_REST_Request $request Full details about the request.
+	 *
+	 * @return WP_REST_Response Response object.
+	 */
+	public function prepare_item_for_response( $location, $request ) {
+
+		$item = (array) $location;
+
+		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+		$data    = $this->add_additional_fields_to_object( $item, $request );
+		$data    = $this->filter_response_by_context( $data, $context );
+
+		$response = rest_ensure_response( $data );
+
+		$response->add_links( $this->prepare_links( $item ) );
+
+		/**
+		 * Filters a menu location returned from the REST API.
+		 *
+		 * Allows modification of the menu location data right before it is
+		 * returned.
+		 *
+		 * @since 5.x
+		 *
+		 * @param WP_REST_Response $response The response object.
+		 * @param stdClass        $location The original location object.
+		 * @param WP_REST_Request  $request  Request used to generate the response.
+		 */
+		return apply_filters( 'rest_prepare_nav_menu_location', $response, $location, $request );
+	}
+
+	/**
+	 * Retrieves the menu location's schema, conforming to JSON Schema.
+	 *
+	 * @since 5.x
+	 *
+	 * @return array Item schema data.
+	 */
+	public function get_item_schema() {
+
+		$schema = array(
+			'$schema'    => 'http://json-schema.org/draft-04/schema#',
+			'title'      => 'menu-location',
+			'type'       => 'object',
+			'properties' => array(
+				'name'        => array(
+					'description' => __( 'The name of the menu location.' ),
+					'type'        => 'string',
+					'context'     => array( 'view', 'edit' ),
+					'readonly'    => true,
+				),
+				'description' => array(
+					'description' => __( 'The description of the menu location.' ),
+					'type'        => 'string',
+					'context'     => array( 'view', 'edit' ),
+					'readonly'    => true,
+				),
+				'menu'        => array(
+					'description' => __( 'The ID of the assigned menu.' ),
+					'type'        => 'int',
+					'context'     => array( 'embed', 'view', 'edit' ),
+				),
+			),
+		);
+
+		return $this->add_additional_fields_schema( $schema );
+	}
+
+	/**
+	 * Retrieves the query params for collections.
+	 *
+	 * @since 5.x
+	 *
+	 * @return array Collection parameters.
+	 */
+	public function get_collection_params() {
+		return array(
+			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+		);
+	}
+
+	/**
+	 * Prepares links for the request.
+	 *
+	 * @since 5.x
+	 *
+	 * @param array $location Menu location.
+	 *
+	 * @return array Links for the given menu location.
+	 */
+	protected function prepare_links( $location ) {
+		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
+
+		// Entity meta.
+		$links = array(
+			'self'       => array(
+				'href' => rest_url( trailingslashit( $base ) . $location['name'] ),
+			),
+			'collection' => array(
+				'href' => rest_url( $base ),
+			),
+		);
+
+		if ( $location['menu'] > 0 ) {
+			$links['menu'] = array(
+				'href'       => rest_url( sprintf( '%s/menus/%d', $this->namespace, $location['menu'] ) ),
+				'embeddable' => true,
+			);
+		}
+
+		return $links;
+	}
+
+	/**
+	 * Get all nav menu locations as well as assigned menu IDs.
+	 *
+	 * @since 5.x
+	 *
+	 * @return stdClass[]
+	 */
+	protected function get_locations() {
+
+		$locations = array();
+
+		$registered = get_registered_nav_menus();
+		$assigned   = get_nav_menu_locations();
+
+		foreach ( $registered as $name => $description ) {
+			$locations[ $name ] = (object) array(
+				'name'        => $name,
+				'description' => $description,
+				'menu'        => isset( $assigned[ $name ] ) ? absint( $assigned[ $name ] ) : 0,
+			);
+		}
+
+		return $locations;
+
+	}
+
+}
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
--- /dev/null
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-settings-controller.php
@@ -0,0 +1,289 @@
+<?php
+/**
+ * REST API: WP_REST_Menu_Settings_Controller class
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 5.x
+ */
+
+/**
+ * Core controller used to access menu settings via the REST API.
+ *
+ * @since 5.x
+ *
+ * @see WP_REST_Posts_Controller
+ */
+class WP_REST_Menu_Settings_Controller extends WP_REST_Controller {
+
+	/**
+	 * The namespace of this controller's route.
+	 *
+	 * @since 5.x
+	 *
+	 * @var string
+	 */
+	protected $namespace = 'wp/v2';
+
+	/**
+	 * The base of this controller's route.
+	 *
+	 * @since 5.x
+	 *
+	 * @var string
+	 */
+	protected $rest_base = 'menus';
+
+	/**
+	 * Registers the routes for the objects of the controller.
+	 *
+	 * @since 5.x
+	 */
+	public function register_routes() {
+
+		register_rest_route(
+			$this->namespace,
+			"/{$this->rest_base}/(?P<id>[\d]+)/settings",
+			array(
+				'args'   => array(
+					'id' => array(
+						'description' => __( 'Unique identifier for the menu.' ),
+						'type'        => 'integer',
+					),
+				),
+				array(
+					'methods'             => WP_REST_Server::READABLE,
+					'callback'            => array( $this, 'get_item' ),
+					'permission_callback' => array( $this, 'get_item_permissions_check' ),
+					'args'                => array(
+						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
+					),
+				),
+				array(
+					'methods'             => WP_REST_Server::EDITABLE,
+					'callback'            => array( $this, 'update_item' ),
+					'permission_callback' => array( $this, 'update_item_permissions_check' ),
+					'args'                => array(
+						'auto_add' => array(
+							'sanitize_callback' => 'wp_validate_boolean',
+							'validate_callback' => 'rest_is_boolean',
+							'required'          => true,
+						),
+					),
+				),
+				'schema' => array( $this, 'get_public_item_schema' ),
+			)
+		);
+
+	}
+
+	/**
+	 * Retrieves one item from the collection.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request REST request
+	 *
+	 * @return WP_REST_Response|WP_Error
+	 */
+	public function get_item( $request ) {
+
+		$menu_id = $request['id'];
+		$menu    = wp_get_nav_menu_object( $menu_id );
+
+		if ( ! $menu ) {
+			return new WP_Error( 'rest_invalid_menu_id', __( 'Invalid menu ID' ), array( 'status' => 404 ) );
+		}
+
+		$item = array(
+			'id'       => $menu_id,
+			'auto_add' => false,
+		);
+
+		$nav_menu_options = (array) get_option( 'nav_menu_options' );
+
+		if ( isset( $nav_menu_options['auto_add'] ) ) {
+			if ( in_array( $menu_id, $nav_menu_options['auto_add'], true ) ) {
+				$item['auto_add'] = (bool) $nav_menu_options['auto_add'];
+			}
+		}
+
+		return $this->prepare_item_for_response( $item, $request );
+	}
+
+	/**
+	 * Checks if a given request has access to get a specific item.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request REST request
+	 *
+	 * @return bool|WP_Error
+	 */
+	public function get_item_permissions_check( $request ) {
+		if ( ! current_user_can( 'edit_theme_options' ) ) {
+			return new WP_Error( 'rest_forbidden_context', __( "Sorry, you are not allowed to view this menu's settings." ), array( 'status' => rest_authorization_required_code() ) );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Updates one item from the collection.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request REST request
+	 *
+	 * @return WP_REST_Response|WP_Error
+	 */
+	public function update_item( $request ) {
+
+		$menu_id = $request['id'];
+		$menu    = wp_get_nav_menu_object( $menu_id );
+
+		if ( ! $menu ) {
+			return new WP_Error( 'rest_invalid_menu_id', __( 'Invalid menu ID' ), array( 'status' => 404 ) );
+		}
+
+		$item = array(
+			'id'       => $menu_id,
+			'auto_add' => false,
+		);
+
+		$nav_menu_options = (array) get_option( 'nav_menu_options' );
+
+		if ( isset( $nav_menu_options['auto_add'] ) ) {
+			if ( in_array( $menu_id, $nav_menu_options['auto_add'], true ) ) {
+				$item['auto_add'] = true;
+			}
+		} else {
+			$nav_menu_options['auto_add'] = array();
+		}
+
+		// Update auto add pages setting
+		$item['auto_add'] = $request->get_param( 'auto_add' );
+		if ( $item['auto_add'] ) {
+			$nav_menu_options['auto_add'][] = $menu_id;
+		} else {
+			$key = array_search( $menu_id, $nav_menu_options['auto_add'], true );
+			if ( false !== $key ) {
+				unset( $nav_menu_options['auto_add'][ $key ] );
+			}
+		}
+		update_option( 'nav_menu_options', $nav_menu_options );
+
+		return $this->prepare_item_for_response( $item, $request );
+	}
+
+	/**
+	 * Checks if a given request has access to update a specific item.
+	 *
+	 * @since 5.x
+	 *
+	 * @param WP_REST_Request $request REST request
+	 *
+	 * @return bool|WP_Error
+	 */
+	public function update_item_permissions_check( $request ) {
+		if ( ! current_user_can( 'edit_theme_options' ) ) {
+			return new WP_Error( 'rest_cannot_update', __( "Sorry, you are not allowed to edit this menu's settings." ), array( 'status' => rest_authorization_required_code() ) );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Prepares the item for the REST response.
+	 *
+	 * @since 5.x
+	 *
+	 * @param array            $item Menu settings
+	 * @param WP_REST_Request $request REST request
+	 *
+	 * @return WP_REST_Response|WP_Error
+	 */
+	public function prepare_item_for_response( $item, $request ) {
+
+		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
+		$data    = $this->add_additional_fields_to_object( $item, $request );
+		$data    = $this->filter_response_by_context( $data, $context );
+
+		$response = rest_ensure_response( $data );
+
+		$response->add_links( $this->prepare_links( $item ) );
+
+		/**
+		 * Filters a menu location returned from the REST API.
+		 *
+		 * Allows modification of the menu's settings data right before it is
+		 * returned.
+		 *
+		 * @since 5.x
+		 *
+		 * @param WP_REST_Response $response The response object.
+		 * @param array            $item The original status object.
+		 * @param WP_REST_Request  $request  Request used to generate the response.
+		 */
+		return apply_filters( 'rest_prepare_nav_menu_settings', $response, $item, $request );
+	}
+
+	/**
+	 * Prepares links for the request.
+	 *
+	 * @since 5.x
+	 *
+	 * @param array $item Menu settings.
+	 *
+	 * @return array Links for the given menu settings.
+	 */
+	protected function prepare_links( $item ) {
+
+		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
+
+		// Entity meta.
+		$links = array(
+			'self' => array(
+				'href' => rest_url( trailingslashit( $base ) . $item['id'] . '/settings' ),
+			),
+			'menu' => array(
+				'href'       => rest_url( trailingslashit( $base ) . $item['id'] ),
+				'embeddable' => true,
+			),
+		);
+
+		return $links;
+	}
+
+	/**
+	 * Retrieves the menu settings schema, conforming to JSON Schema.
+	 *
+	 * @since 5.x
+	 *
+	 * @return array Item schema data.
+	 */
+	public function get_item_schema() {
+
+		$schema = array(
+			'$schema'    => 'http://json-schema.org/draft-04/schema#',
+			'title'      => 'menu-settings',
+			'type'       => 'object',
+			'properties' => array(
+				'id'       => array(
+					'description' => 'Unique identifier for the menu.',
+					'type'        => 'integer',
+					'context'     => array( 'view', 'edit' ),
+					'readonly'    => true,
+				),
+				'auto_add' => array(
+					'description' => __( 'Whether or not to automatically add top level pages to the menu.' ),
+					'type'        => 'boolean',
+					'context'     => array( 'view', 'edit' ),
+				),
+			),
+		);
+
+		return $this->add_additional_fields_schema( $schema );
+	}
+
+}
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
--- /dev/null
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menus-controller.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * REST API: WP_REST_Menus_Controller class
+ *
+ * @package WordPress
+ * @subpackage REST_API
+ * @since 5.x
+ */
+
+/**
+ * Core controller used to access menus via the REST API.
+ *
+ * @since 5.x
+ *
+ * @see WP_REST_Posts_Controller
+ */
+class WP_REST_Menus_Controller extends WP_REST_Terms_Controller {
+
+	/**
+	 * Prepares links for the request.
+	 *
+	 * @since 5.x
+	 *
+	 * @param object $term Term object.
+	 * @return array Links for the given term.
+	 */
+	protected function prepare_links( $term ) {
+
+		$links = parent::prepare_links( $term );
+
+		// Let's make sure that menu items are embeddable for a menu collection.
+		if ( array_key_exists( 'https://api.w.org/post_type', $links ) ) {
+			$post_type_links = $links['https://api.w.org/post_type'];
+
+			foreach ( $post_type_links as $index => $post_type_link ) {
+				if ( ! array_key_exists( 'href', $post_type_link ) || strpos( $post_type_link['href'], '/menu-items?' ) === false ) {
+					continue;
+				}
+
+				$post_type_links[ $index ]['embeddable'] = true;
+			}
+
+			$links['https://api.w.org/post_type'] = $post_type_links;
+		}
+
+		return $links;
+	}
+
+}
diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php
index 12864b343a..266682a367 100644
--- a/src/wp-includes/taxonomy.php
+++ b/src/wp-includes/taxonomy.php
@@ -107,17 +107,20 @@ function create_initial_taxonomies() {
 		'nav_menu',
 		'nav_menu_item',
 		array(
-			'public'            => false,
-			'hierarchical'      => false,
-			'labels'            => array(
+			'public'                => false,
+			'hierarchical'          => false,
+			'labels'                => array(
 				'name'          => __( 'Navigation Menus' ),
 				'singular_name' => __( 'Navigation Menu' ),
 			),
-			'query_var'         => false,
-			'rewrite'           => false,
-			'show_ui'           => false,
-			'_builtin'          => true,
-			'show_in_nav_menus' => false,
+			'query_var'             => false,
+			'rewrite'               => false,
+			'show_ui'               => false,
+			'_builtin'              => true,
+			'show_in_nav_menus'     => false,
+			'show_in_rest'          => true,
+			'rest_base'             => 'menus',
+			'rest_controller_class' => 'WP_REST_Menus_Controller',
 		)
 	);
 
diff --git a/src/wp-settings.php b/src/wp-settings.php
index fa9f9a3a25..42680af27f 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -251,6 +251,10 @@ require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-blocks-controller.
 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-renderer-controller.php' );
 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-settings-controller.php' );
 require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-themes-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-items-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-locations-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-settings-controller.php' );
+require( ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menus-controller.php' );
 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-meta-fields.php' );
 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-comment-meta-fields.php' );
 require( ABSPATH . WPINC . '/rest-api/fields/class-wp-rest-post-meta-fields.php' );
-- 
2.20.1

