diff --git a/lib/class-wp-rest-menus-controller.php b/lib/class-wp-rest-menus-controller.php index 807ac21..d78c5c6 100644 --- a/lib/class-wp-rest-menus-controller.php +++ b/lib/class-wp-rest-menus-controller.php @@ -2,7 +2,7 @@ /** * REST API: WP_REST_Menus_Controller class * - * @package WordPress + * @package WordPress * @subpackage REST_API */ @@ -72,6 +72,71 @@ protected function get_term( $id ) { return $nav_term; } + /** + * Checks if a request has access to create a term. + * Also check if request can assign menu locations. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error True if the request has access to create items, false or WP_Error object otherwise. + */ + public function create_item_permissions_check( $request ) { + $check = $this->check_assign_locations_permission( $request ); + if ( is_wp_error( $check ) ) { + return $check; + } + + return parent::create_item_permissions_check( $request ); + } + + /** + * Checks if a request has access to update the specified term. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error True if the request has access to update the item, false or WP_Error object otherwise. + */ + public function update_item_permissions_check( $request ) { + $check = $this->check_assign_locations_permission( $request ); + if ( is_wp_error( $check ) ) { + return $check; + } + + return parent::update_item_permissions_check( $request ); + } + + /** + * Checks whether current user can assign all locations sent with the current request. + * + * @param WP_REST_Request $request The request object with post and locations data. + * + * @return bool Whether the current user can assign the provided terms. + */ + protected function check_assign_locations_permission( $request ) { + if ( ! isset( $request['locations'] ) ) { + return true; + } + + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( 'rest_cannot_assign_location', __( 'Sorry, you are not allowed to assign the provided locations.' ), array( 'status' => rest_authorization_required_code() ) ); + } + + foreach ( $request['locations'] as $location ) { + if ( ! array_key_exists( $location, get_registered_nav_menus() ) ) { + return new WP_Error( + 'rest_menu_location_invalid', + __( 'Invalid menu location.' ), + array( + 'status' => 400, + 'location' => $location, + ) + ); + } + } + + return true; + } + /** * Prepares a single term output for response. * @@ -196,6 +261,12 @@ public function create_item( $request ) { } } + $locations_update = $this->handle_locations( $term->term_id, $request ); + + if ( is_wp_error( $locations_update ) ) { + return $locations_update; + } + $fields_update = $this->update_additional_fields_for_object( $term, $request ); if ( is_wp_error( $fields_update ) ) { @@ -276,6 +347,12 @@ public function update_item( $request ) { } } + $locations_update = $this->handle_locations( $term->term_id, $request ); + + if ( is_wp_error( $locations_update ) ) { + return $locations_update; + } + $fields_update = $this->update_additional_fields_for_object( $term, $request ); if ( is_wp_error( $fields_update ) ) { @@ -345,6 +422,40 @@ public function delete_item( $request ) { return $response; } + /** + * Updates the menu's locations from a REST request. + * + * @param int $menu_id The menu id to update the location form. + * @param WP_REST_Request $request The request object with menu and locations data. + * + * @return true|WP_Error WP_Error on an error assigning any of the locations, otherwise null. + */ + protected function handle_locations( $menu_id, $request ) { + if ( ! isset( $request['locations'] ) ) { + return true; + } + + $menu_locations = get_registered_nav_menus(); + $menu_locations = array_keys( $menu_locations ); + $new_locations = array(); + foreach ( $request['locations'] as $location ) { + if ( ! in_array( $location, $menu_locations, true ) ) { + return new WP_Error( 'invalid_menu_location', __( 'Menu location does not exist.' ), array( 'status' => 400 ) ); + } + $new_locations[ $location ] = $menu_id; + } + $assigned_menu = get_nav_menu_locations(); + foreach ( $assigned_menu as $location => $term_id ) { + if ( $term_id === $menu_id ) { + unset( $assigned_menu[ $location ] ); + } + } + $new_assignments = array_merge( $assigned_menu, $new_locations ); + set_theme_mod( 'nav_menu_locations', $new_assignments ); + + return true; + } + /** * Retrieves the term's schema, conforming to JSON Schema. * @@ -356,6 +467,15 @@ public function get_item_schema() { unset( $schema['properties']['link'] ); unset( $schema['properties']['taxonomy'] ); + $schema['properties']['locations'] = array( + 'description' => __( 'The locations assigned to the menu.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ); + return $schema; } } diff --git a/tests/test-rest-nav-menus-controller.php b/tests/test-rest-nav-menus-controller.php index a2d949d..8c0a4a2 100644 --- a/tests/test-rest-nav-menus-controller.php +++ b/tests/test-rest-nav-menus-controller.php @@ -60,7 +60,19 @@ public static function wpSetUpBeforeClass( $factory ) { */ public function setUp() { parent::setUp(); - $this->menu_id = wp_create_nav_menu( rand_str() ); + // Unregister all nav menu locations. + foreach ( array_keys( get_registered_nav_menus() ) as $location ) { + unregister_nav_menu( $location ); + } + + $orig_args = array( + 'name' => 'Original Name', + 'description' => 'Original Description', + 'slug' => 'original-slug', + 'taxonomy' => 'nav_menu', + ); + + $this->menu_id = $this->factory->term->create( $orig_args ); register_meta( 'term', @@ -74,6 +86,17 @@ public function setUp() { ); } + /** + * Register nav menu locations. + * + * @param array $locations Location slugs. + */ + public function register_nav_menu_locations( $locations ) { + foreach ( $locations as $location ) { + register_nav_menu( $location, ucfirst( $location ) ); + } + } + /** * */ @@ -190,17 +213,7 @@ public function test_create_item() { public function test_update_item() { wp_set_current_user( self::$admin_id ); - $nav_menu_id = wp_update_nav_menu_object( - 0, - array( - 'description' => 'Original Description', - 'menu-name' => 'Original Name', - ) - ); - - $term = get_term_by( 'id', $nav_menu_id, self::TAXONOMY ); - - $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $term->term_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); $request->set_param( 'name', 'New Name' ); $request->set_param( 'description', 'New Description' ); $request->set_param( @@ -273,12 +286,107 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertEquals( 5, count( $properties ) ); + $this->assertEquals( 6, count( $properties ) ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'meta', $properties ); $this->assertArrayHasKey( 'name', $properties ); $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'locations', $properties ); + } + + /** + * + */ + public function test_create_item_with_location_permission_correct() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus' ); + $request->set_param( 'name', 'My Awesome Term' ); + $request->set_param( 'slug', 'so-awesome' ); + $request->set_param( 'locations', 'primary' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 201, $response->get_status() ); + $data = $response->get_data(); + $term_id = $data['id']; + $locations = get_nav_menu_locations(); + $this->assertEquals( $locations['primary'], $term_id ); + } + + /** + * + */ + public function test_create_item_with_location_permission_incorrect() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus' ); + $request->set_param( 'name', 'My Awesome Term' ); + $request->set_param( 'slug', 'so-awesome' ); + $request->set_param( 'locations', 'primary' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( rest_authorization_required_code(), $response->get_status() ); + $this->assertErrorResponse( 'rest_cannot_assign_location', $response, rest_authorization_required_code() ); + } + + /** + * + */ + public function test_create_item_with_location_permission_no_location() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus' ); + $request->set_param( 'name', 'My Awesome Term' ); + $request->set_param( 'slug', 'so-awesome' ); + $request->set_param( 'locations', 'bar' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + $this->assertErrorResponse( 'rest_menu_location_invalid', $response, 400 ); + } + + /** + * + */ + public function test_update_item_with_no_location() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_param( 'name', 'New Name' ); + $request->set_param( 'description', 'New Description' ); + $request->set_param( 'slug', 'new-slug' ); + $request->set_param( 'locations', 'bar' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * + */ + public function test_update_item_with_location_permission_correct() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_param( 'name', 'New Name' ); + $request->set_param( 'description', 'New Description' ); + $request->set_param( 'slug', 'new-slug' ); + $request->set_param( 'locations', 'primary' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $locations = get_nav_menu_locations(); + $this->assertEquals( $locations['primary'], $this->menu_id ); + } + + /** + * + */ + public function test_update_item_with_location_permission_incorrect() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_param( 'name', 'New Name' ); + $request->set_param( 'description', 'New Description' ); + $request->set_param( 'slug', 'new-slug' ); + $request->set_param( 'locations', 'primary' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertEquals( rest_authorization_required_code(), $response->get_status() ); } /** @@ -309,6 +417,43 @@ public function test_get_item_links() { $this->assertEquals( $location_url, $links['https://api.w.org/menu-location'][0]['href'] ); } + /** + * + */ + public function test_change_menu_location() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + $secondary_id = self::factory()->term->create( + array( + 'name' => 'Secondary Name', + 'description' => 'Secondary Description', + 'slug' => 'secondary-slug', + 'taxonomy' => 'nav_menu', + ) + ); + + $locations = get_nav_menu_locations(); + $locations['primary'] = $this->menu_id; + $locations['secondary'] = $secondary_id; + set_theme_mod( 'nav_menu_locations', $locations ); + + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_body_params( + array( + 'locations' => array( 'secondary' ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $locations = get_nav_menu_locations(); + $this->assertArrayNotHasKey( 'primary', $locations ); + $this->assertArrayHasKey( 'secondary', $locations ); + $this->assertEquals( $this->menu_id, $locations['secondary'] ); + } + /** * */