Make WordPress Core

Changeset 59718


Ignore:
Timestamp:
01/28/2025 04:07:07 AM (5 months ago)
Author:
spacedmonkey
Message:

REST API: Introduce filter for controlling menu read access.

The menu, menu item, and menu location endpoints were added to the REST API in [52079]. In that commit, menu data was treated as private and restricted to logged-in users with the edit_theme_options capability. However, in many cases, this data can be considered public. Previously, there was no simple way for developers to allow this data to be exposed via the REST API.

This commit introduces the rest_menu_read_access filter, enabling developers to control read access to menus, menu items, and menu locations in the REST API. The same filter is applied across all three REST API classes, simplifying the process of opting into exposing this data.

Each instance of the filter provides the current request and the relevant class instance as context, allowing developers to selectively or globally enable access to the data.

Props spacedmonkey, antonvlasenko, kadamwhite, julianmar, masteradhoc.
Fixes #54304.

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php

    r59009 r59718  
    8181     */
    8282    protected function check_has_read_only_access( $request ) {
     83        /**
     84         * Filters whether the current user has read access to menu items via the REST API.
     85         *
     86         * @since 6.8.0
     87         * @param $read_only_access bool Whether the current user has read access to menu items via the REST API.
     88         * @param $request WP_REST_Request Full details about the request.
     89         * @param $this WP_REST_Controller The current instance of the controller.
     90         */
     91        $read_only_access = apply_filters( 'rest_menu_read_access', false, $request, $this );
     92        if ( $read_only_access ) {
     93            return true;
     94        }
     95
    8396        if ( current_user_can( 'edit_theme_options' ) ) {
    8497            return true;
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-locations-controller.php

    r58704 r59718  
    8181     */
    8282    public function get_items_permissions_check( $request ) {
     83        return $this->check_has_read_only_access( $request );
     84    }
     85
     86    /**
     87     * Retrieves all menu locations, depending on user context.
     88     *
     89     * @since 5.9.0
     90     *
     91     * @param WP_REST_Request $request Full details about the request.
     92     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     93     */
     94    public function get_items( $request ) {
     95        $data = array();
     96
     97        foreach ( get_registered_nav_menus() as $name => $description ) {
     98            $location              = new stdClass();
     99            $location->name        = $name;
     100            $location->description = $description;
     101
     102            $location      = $this->prepare_item_for_response( $location, $request );
     103            $data[ $name ] = $this->prepare_response_for_collection( $location );
     104        }
     105
     106        return rest_ensure_response( $data );
     107    }
     108
     109    /**
     110     * Checks if a given request has access to read a menu location.
     111     *
     112     * @since 5.9.0
     113     *
     114     * @param WP_REST_Request $request Full details about the request.
     115     * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
     116     */
     117    public function get_item_permissions_check( $request ) {
     118        return $this->check_has_read_only_access( $request );
     119    }
     120
     121    /**
     122     * Retrieves a specific menu location.
     123     *
     124     * @since 5.9.0
     125     *
     126     * @param WP_REST_Request $request Full details about the request.
     127     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     128     */
     129    public function get_item( $request ) {
     130        $registered_menus = get_registered_nav_menus();
     131        if ( ! array_key_exists( $request['location'], $registered_menus ) ) {
     132            return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
     133        }
     134
     135        $location              = new stdClass();
     136        $location->name        = $request['location'];
     137        $location->description = $registered_menus[ $location->name ];
     138
     139        $data = $this->prepare_item_for_response( $location, $request );
     140
     141        return rest_ensure_response( $data );
     142    }
     143
     144    /**
     145     * Checks whether the current user has read permission for the endpoint.
     146     *
     147     * @since 6.8.0
     148     *
     149     * @param WP_REST_Request $request Full details about the request.
     150     * @return true|WP_Error True if the current user has permission, WP_Error object otherwise.
     151     */
     152    protected function check_has_read_only_access( $request ) {
     153        /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
     154        $read_only_access = apply_filters( 'rest_menu_read_access', false, $request, $this );
     155        if ( $read_only_access ) {
     156            return true;
     157        }
     158
    83159        if ( ! current_user_can( 'edit_theme_options' ) ) {
    84160            return new WP_Error(
     
    90166
    91167        return true;
    92     }
    93 
    94     /**
    95      * Retrieves all menu locations, depending on user context.
    96      *
    97      * @since 5.9.0
    98      *
    99      * @param WP_REST_Request $request Full details about the request.
    100      * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    101      */
    102     public function get_items( $request ) {
    103         $data = array();
    104 
    105         foreach ( get_registered_nav_menus() as $name => $description ) {
    106             $location              = new stdClass();
    107             $location->name        = $name;
    108             $location->description = $description;
    109 
    110             $location      = $this->prepare_item_for_response( $location, $request );
    111             $data[ $name ] = $this->prepare_response_for_collection( $location );
    112         }
    113 
    114         return rest_ensure_response( $data );
    115     }
    116 
    117     /**
    118      * Checks if a given request has access to read a menu location.
    119      *
    120      * @since 5.9.0
    121      *
    122      * @param WP_REST_Request $request Full details about the request.
    123      * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
    124      */
    125     public function get_item_permissions_check( $request ) {
    126         if ( ! current_user_can( 'edit_theme_options' ) ) {
    127             return new WP_Error(
    128                 'rest_cannot_view',
    129                 __( 'Sorry, you are not allowed to view menu locations.' ),
    130                 array( 'status' => rest_authorization_required_code() )
    131             );
    132         }
    133 
    134         return true;
    135     }
    136 
    137     /**
    138      * Retrieves a specific menu location.
    139      *
    140      * @since 5.9.0
    141      *
    142      * @param WP_REST_Request $request Full details about the request.
    143      * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    144      */
    145     public function get_item( $request ) {
    146         $registered_menus = get_registered_nav_menus();
    147         if ( ! array_key_exists( $request['location'], $registered_menus ) ) {
    148             return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
    149         }
    150 
    151         $location              = new stdClass();
    152         $location->name        = $request['location'];
    153         $location->description = $registered_menus[ $location->name ];
    154 
    155         $data = $this->prepare_item_for_response( $location, $request );
    156 
    157         return rest_ensure_response( $data );
    158168    }
    159169
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-menus-controller.php

    r56415 r59718  
    8585     */
    8686    protected function check_has_read_only_access( $request ) {
     87        /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
     88        $read_only_access = apply_filters( 'rest_menu_read_access', false, $request, $this );
     89        if ( $read_only_access ) {
     90            return true;
     91        }
     92
    8793        if ( current_user_can( 'edit_theme_options' ) ) {
    8894            return true;
  • trunk/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php

    r58783 r59718  
    178178    public function test_get_item() {
    179179        wp_set_current_user( self::$admin_id );
     180        $request  = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
     181        $response = rest_get_server()->dispatch( $request );
     182
     183        $this->check_get_menu_item_response( $response, 'view' );
     184    }
     185
     186    /**
     187     * @ticket 54304
     188     * @covers ::get_items
     189     */
     190    public function test_get_items_filter() {
     191        add_filter( 'rest_menu_read_access', '__return_true' );
     192        $request  = new WP_REST_Request( 'GET', '/wp/v2/menu-items' );
     193        $response = rest_get_server()->dispatch( $request );
     194
     195        $this->check_get_menu_items_response( $response );
     196    }
     197
     198    /**
     199     * @ticket 54304
     200     * @covers ::get_item
     201     */
     202    public function test_get_item_filter() {
     203        add_filter( 'rest_menu_read_access', '__return_true' );
    180204        $request  = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) );
    181205        $response = rest_get_server()->dispatch( $request );
  • trunk/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php

    r56746 r59718  
    122122
    123123    /**
     124     * @ticket 54304
     125     * @covers ::get_items
     126     */
     127    public function test_get_items_filter() {
     128        $menus = array( 'primary', 'secondary' );
     129        $this->register_nav_menu_locations( array( 'primary', 'secondary' ) );
     130        add_filter( 'rest_menu_read_access', '__return_true' );
     131
     132        $request  = new WP_REST_Request( 'GET', '/wp/v2/menu-locations' );
     133        $response = rest_get_server()->dispatch( $request );
     134        $data     = $response->get_data();
     135        $data     = array_values( $data );
     136        $this->assertCount( 2, $data, 'Number of menu location are not 2' );
     137
     138        $names        = wp_list_pluck( $data, 'name' );
     139        $descriptions = wp_list_pluck( $data, 'description' );
     140        $this->assertSame( $menus, $names );
     141        $menu_descriptions = array_map( 'ucfirst', $names );
     142
     143        $this->assertSame( $menu_descriptions, $descriptions, 'Menu descriptions do not match' );
     144    }
     145
     146    /**
     147     * @ticket 54304
     148     * @covers ::get_item
     149     */
     150    public function test_get_item_filter() {
     151        $menu = 'primary';
     152        $this->register_nav_menu_locations( array( $menu ) );
     153
     154        add_filter( 'rest_menu_read_access', '__return_true' );
     155        $request  = new WP_REST_Request( 'GET', '/wp/v2/menu-locations/' . $menu );
     156        $response = rest_get_server()->dispatch( $request );
     157        $data     = $response->get_data();
     158
     159        $this->assertSame( $menu, $data['name'] );
     160    }
     161
     162    /**
    124163     * @ticket 40878
    125164     * @covers ::get_item
  • trunk/tests/phpunit/tests/rest-api/wpRestMenusController.php

    r56746 r59718  
    209209    }
    210210
     211
     212    /**
     213     * @ticket 54304
     214     * @covers ::get_items
     215     */
     216    public function test_get_items_filter() {
     217        add_filter( 'rest_menu_read_access', '__return_true' );
     218        wp_update_nav_menu_object(
     219            0,
     220            array(
     221                'description' => 'Test get',
     222                'menu-name'   => 'test Name get',
     223            )
     224        );
     225        $request = new WP_REST_Request( 'GET', '/wp/v2/menus' );
     226        $request->set_param( 'per_page', self::$per_page );
     227        $response = rest_get_server()->dispatch( $request );
     228        $this->check_get_taxonomy_terms_response( $response );
     229    }
     230
     231    /**
     232     * @ticket 54304
     233     * @covers ::get_item
     234     */
     235    public function test_get_item_filter() {
     236        add_filter( 'rest_menu_read_access', '__return_true' );
     237        $nav_menu_id = wp_update_nav_menu_object(
     238            0,
     239            array(
     240                'description' => 'Test menu',
     241                'menu-name'   => 'test Name',
     242            )
     243        );
     244
     245        $this->register_nav_menu_locations( array( 'primary' ) );
     246        set_theme_mod( 'nav_menu_locations', array( 'primary' => $nav_menu_id ) );
     247
     248        $request  = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $nav_menu_id );
     249        $response = rest_get_server()->dispatch( $request );
     250        $this->check_get_taxonomy_term_response( $response, $nav_menu_id );
     251    }
     252
    211253    /**
    212254     * @ticket 40878
Note: See TracChangeset for help on using the changeset viewer.