WordPress.org

Make WordPress Core

Changeset 52342


Ignore:
Timestamp:
12/07/2021 08:56:18 PM (6 weeks ago)
Author:
spacedmonkey
Message:

REST API: Improve permission handling in global style endpoint.

The new wp_global_styles post type is registered to use edit_theme_options in the capability settings. The WP_REST_Global_Styles_Controller class's permission checks methods use the capability in a hard coded form rather than looking up the capability via the post type object. Changing the permission callbacks to lookup capabilities via the post type object, allows theme and plugin developers to modify the capability used for editing global styles via a filter and these values to be respected via the Global Styles REST API.

Props Spacedmonkey, peterwilsoncc, hellofromTonya , antonvlasenko, TimothyBlynJacobs, costdev, zieladam.
Fixes #54516.

Location:
trunk
Files:
2 edited

Legend:

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

    r52332 r52342  
    1212 */
    1313class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
     14
     15    /**
     16     * Post type.
     17     *
     18     * @since 5.9.0
     19     * @var string
     20     */
     21    protected $post_type;
     22
    1423    /**
    1524     * Constructor.
     
    1928        $this->namespace = 'wp/v2';
    2029        $this->rest_base = 'global-styles';
     30        $this->post_type = 'wp_global_styles';
    2131    }
    2232
     
    7686
    7787    /**
    78      * Checks if the user has permissions to make the request.
    79      *
    80      * @since 5.9.0
    81      *
     88     * Checks if a given request has access to read a single global style.
     89     *
     90     * @since 5.9.0
     91     *
     92     * @param WP_REST_Request $request Full details about the request.
    8293     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
    8394     */
    84     protected function permissions_check() {
    85         // Verify if the current user has edit_theme_options capability.
    86         // This capability is required to edit/view/delete templates.
    87         if ( ! current_user_can( 'edit_theme_options' ) ) {
     95    public function get_item_permissions_check( $request ) {
     96        $post = $this->get_post( $request['id'] );
     97        if ( is_wp_error( $post ) ) {
     98            return $post;
     99        }
     100
     101        if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
    88102            return new WP_Error(
    89                 'rest_cannot_manage_global_styles',
    90                 __( 'Sorry, you are not allowed to access the global styles on this site.' ),
    91                 array(
    92                     'status' => rest_authorization_required_code(),
    93                 )
     103                'rest_forbidden_context',
     104                __( 'Sorry, you are not allowed to edit this global style.' ),
     105                array( 'status' => rest_authorization_required_code() )
    94106            );
    95107        }
    96108
     109        if ( ! $this->check_read_permission( $post ) ) {
     110            return new WP_Error(
     111                'rest_cannot_view',
     112                __( 'Sorry, you are not allowed to view this global style.' ),
     113                array( 'status' => rest_authorization_required_code() )
     114            );
     115        }
     116
    97117        return true;
    98118    }
    99119
    100120    /**
    101      * Checks if a given request has access to read a single global styles config.
    102      *
    103      * @param WP_REST_Request $request Full details about the request.
    104      * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
    105      */
    106     public function get_item_permissions_check( $request ) {
    107         return $this->permissions_check( $request );
     121     * Checks if a global style can be read.
     122     *
     123     * @since 5.9.0
     124     *
     125     * @param WP_Post $post Post object.
     126     * @return bool Whether the post can be read.
     127     */
     128    protected function check_read_permission( $post ) {
     129        return current_user_can( 'read_post', $post->ID );
    108130    }
    109131
     
    118140     */
    119141    public function get_item( $request ) {
    120         $post = get_post( $request['id'] );
    121         if ( ! $post || 'wp_global_styles' !== $post->post_type ) {
    122             return new WP_Error( 'rest_global_styles_not_found', __( 'No global styles config exist with that id.' ), array( 'status' => 404 ) );
     142        $post = $this->get_post( $request['id'] );
     143        if ( is_wp_error( $post ) ) {
     144            return $post;
    123145        }
    124146
     
    135157     */
    136158    public function update_item_permissions_check( $request ) {
    137         return $this->permissions_check( $request );
     159        $post = $this->get_post( $request['id'] );
     160        if ( is_wp_error( $post ) ) {
     161            return $post;
     162        }
     163
     164        if ( $post && ! $this->check_update_permission( $post ) ) {
     165            return new WP_Error(
     166                'rest_cannot_edit',
     167                __( 'Sorry, you are not allowed to edit this global style.' ),
     168                array( 'status' => rest_authorization_required_code() )
     169            );
     170        }
     171
     172        return true;
     173    }
     174
     175    /**
     176     * Checks if a global style can be edited.
     177     *
     178     * @since 5.9.0
     179     *
     180     * @param WP_Post $post Post object.
     181     * @return bool Whether the post can be edited.
     182     */
     183    protected function check_update_permission( $post ) {
     184        return current_user_can( 'edit_post', $post->ID );
    138185    }
    139186
     
    147194     */
    148195    public function update_item( $request ) {
    149         $post_before = get_post( $request['id'] );
    150         if ( ! $post_before || 'wp_global_styles' !== $post_before->post_type ) {
    151             return new WP_Error( 'rest_global_styles_not_found', __( 'No global styles config exist with that id.' ), array( 'status' => 404 ) );
     196        $post_before = $this->get_post( $request['id'] );
     197        if ( is_wp_error( $post_before ) ) {
     198            return $post_before;
    152199        }
    153200
     
    290337    }
    291338
     339    /**
     340     * Get the post, if the ID is valid.
     341     *
     342     * @since 5.9.0
     343     *
     344     * @param int $id Supplied ID.
     345     * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
     346     */
     347    protected function get_post( $id ) {
     348        $error = new WP_Error(
     349            'rest_global_styles_not_found',
     350            __( 'No global styles config exist with that id.' ),
     351            array( 'status' => 404 )
     352        );
     353
     354        $id = (int) $id;
     355        if ( $id <= 0 ) {
     356            return $error;
     357        }
     358
     359        $post = get_post( $id );
     360        if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
     361            return $error;
     362        }
     363
     364        return $post;
     365    }
     366
    292367
    293368    /**
     
    324399        $rels = array();
    325400
    326         $post_type = get_post_type_object( 'wp_global_styles' );
     401        $post_type = get_post_type_object( $this->post_type );
    327402        if ( current_user_can( $post_type->cap->publish_posts ) ) {
    328403            $rels[] = 'https://api.w.org/action-publish';
     
    372447        $schema = array(
    373448            '$schema'    => 'http://json-schema.org/draft-04/schema#',
    374             'title'      => 'wp_global_styles',
     449            'title'      => $this->post_type,
    375450            'type'       => 'object',
    376451            'properties' => array(
     
    427502     */
    428503    public function get_theme_item_permissions_check( $request ) {
    429         return $this->permissions_check( $request );
     504        // Verify if the current user has edit_theme_options capability.
     505        // This capability is required to edit/view/delete templates.
     506        if ( ! current_user_can( 'edit_theme_options' ) ) {
     507            return new WP_Error(
     508                'rest_cannot_manage_global_styles',
     509                __( 'Sorry, you are not allowed to access the global styles on this site.' ),
     510                array(
     511                    'status' => rest_authorization_required_code(),
     512                )
     513            );
     514        }
     515
     516        return true;
    430517    }
    431518
  • trunk/tests/phpunit/tests/rest-api/rest-global-styles-controller.php

    r52282 r52342  
    88
    99/**
     10 * @covers WP_REST_Global_Styles_Controller
    1011 * @group restapi-global-styles
    1112 * @group restapi
     
    2021     * @var int
    2122     */
     23    protected static $subscriber_id;
     24
     25    /**
     26     * @var int
     27     */
    2228    protected static $global_styles_id;
     29
     30    /**
     31     * @var int
     32     */
     33    protected static $post_id;
    2334
    2435    private function find_and_normalize_global_styles_by_id( $global_styles, $id ) {
     
    4960            )
    5061        );
     62
     63        self::$subscriber_id = $factory->user->create(
     64            array(
     65                'role' => 'subscriber',
     66            )
     67        );
     68
    5169        // This creates the global styles for the current theme.
    52         self::$global_styles_id = wp_insert_post(
     70        self::$global_styles_id = $factory->post->create(
    5371            array(
    5472                'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }',
     
    6078                    'wp_theme' => 'tt1-blocks',
    6179                ),
    62             ),
    63             true
    64         );
    65     }
    66 
     80            )
     81        );
     82
     83        self::$post_id = $factory->post->create();
     84    }
     85
     86    /**
     87     *
     88     */
     89    public static function wpTearDownAfterClass() {
     90        self::delete_user( self::$admin_id );
     91        self::delete_user( self::$subscriber_id );
     92    }
     93
     94    /**
     95     * @covers WP_REST_Global_Styles_Controller::register_routes
     96     */
    6797    public function test_register_routes() {
    6898        $routes = rest_get_server()->get_routes();
    6999        $this->assertArrayHasKey( '/wp/v2/global-styles/(?P<id>[\/\w-]+)', $routes );
     100        $this->assertCount( 2, $routes['/wp/v2/global-styles/(?P<id>[\/\w-]+)'] );
     101        $this->assertArrayHasKey( '/wp/v2/global-styles/themes/(?P<stylesheet>[^.\/]+(?:\/[^.\/]+)?)', $routes );
     102        $this->assertCount( 1, $routes['/wp/v2/global-styles/themes/(?P<stylesheet>[^.\/]+(?:\/[^.\/]+)?)'] );
    70103    }
    71104
    72105    public function test_context_param() {
    73         // TODO: Implement test_context_param() method.
    74         $this->markTestIncomplete();
     106        $this->markTestSkipped( 'Controller does not implement context_param().' );
    75107    }
    76108
    77109    public function test_get_items() {
    78         $this->markTestIncomplete();
    79     }
    80 
     110        $this->markTestSkipped( 'Controller does not implement get_items().' );
     111    }
     112
     113    /**
     114     * @covers WP_REST_Global_Styles_Controller::get_theme_item
     115     * @ticket 54516
     116     */
     117    public function test_get_theme_item_no_user() {
     118        wp_set_current_user( 0 );
     119        $request  = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' );
     120        $response = rest_get_server()->dispatch( $request );
     121        $this->assertErrorResponse( 'rest_cannot_manage_global_styles', $response, 401 );
     122    }
     123
     124    /**
     125     * @covers WP_REST_Global_Styles_Controller::get_theme_item
     126     * @ticket 54516
     127     */
     128    public function test_get_theme_item_permission_check() {
     129        wp_set_current_user( self::$subscriber_id );
     130        $request  = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' );
     131        $response = rest_get_server()->dispatch( $request );
     132        $this->assertErrorResponse( 'rest_cannot_manage_global_styles', $response, 403 );
     133    }
     134
     135
     136    /**
     137     * @covers WP_REST_Global_Styles_Controller::get_theme_item
     138     * @ticket 54516
     139     */
     140    public function test_get_theme_item_invalid() {
     141        wp_set_current_user( self::$admin_id );
     142        $request  = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/invalid' );
     143        $response = rest_get_server()->dispatch( $request );
     144        $this->assertErrorResponse( 'rest_theme_not_found', $response, 404 );
     145    }
     146
     147    /**
     148     * @covers WP_REST_Global_Styles_Controller::get_theme_item
     149     */
     150    public function test_get_theme_item() {
     151        wp_set_current_user( self::$admin_id );
     152        $request  = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' );
     153        $response = rest_get_server()->dispatch( $request );
     154        $data     = $response->get_data();
     155        unset( $data['_links'] );
     156
     157        $this->assertArrayHasKey( 'settings', $data );
     158        $this->assertArrayHasKey( 'styles', $data );
     159    }
     160
     161    /**
     162     * @covers WP_REST_Global_Styles_Controller::get_item
     163     * @ticket 54516
     164     */
     165    public function test_get_item_no_user() {
     166        wp_set_current_user( 0 );
     167        $request  = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id );
     168        $response = rest_get_server()->dispatch( $request );
     169        $this->assertErrorResponse( 'rest_cannot_view', $response, 401 );
     170    }
     171
     172    /**
     173     * @covers WP_REST_Global_Styles_Controller::get_item
     174     * @ticket 54516
     175     */
     176    public function test_get_item_invalid_post() {
     177        wp_set_current_user( self::$admin_id );
     178        $request  = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$post_id );
     179        $response = rest_get_server()->dispatch( $request );
     180        $this->assertErrorResponse( 'rest_global_styles_not_found', $response, 404 );
     181    }
     182
     183    /**
     184     * @covers WP_REST_Global_Styles_Controller::get_item
     185     * @ticket 54516
     186     */
     187    public function test_get_item_permission_check() {
     188        wp_set_current_user( self::$subscriber_id );
     189        $request  = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id );
     190        $response = rest_get_server()->dispatch( $request );
     191        $this->assertErrorResponse( 'rest_cannot_view', $response, 403 );
     192    }
     193
     194    /**
     195     * @covers WP_REST_Global_Styles_Controller::get_item
     196     * @ticket 54516
     197     */
     198    public function test_get_item_no_user_edit() {
     199        wp_set_current_user( 0 );
     200        $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id );
     201        $request->set_param( 'context', 'edit' );
     202        $response = rest_get_server()->dispatch( $request );
     203        $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 );
     204    }
     205
     206    /**
     207     * @covers WP_REST_Global_Styles_Controller::get_item
     208     * @ticket 54516
     209     */
     210    public function test_get_item_permission_check_edit() {
     211        wp_set_current_user( self::$subscriber_id );
     212        $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id );
     213        $request->set_param( 'context', 'edit' );
     214        $response = rest_get_server()->dispatch( $request );
     215        $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 );
     216    }
     217
     218    /**
     219     * @covers WP_REST_Global_Styles_Controller::get_item
     220     */
    81221    public function test_get_item() {
    82222        wp_set_current_user( self::$admin_id );
     
    101241
    102242    public function test_create_item() {
    103         $this->markTestIncomplete();
    104     }
    105 
     243        $this->markTestSkipped( 'Controller does not implement create_item().' );
     244    }
     245
     246    /**
     247     * @covers WP_REST_Global_Styles_Controller::update_item
     248     * @ticket 54516
     249     */
    106250    public function test_update_item() {
    107251        wp_set_current_user( self::$admin_id );
     
    117261    }
    118262
     263
     264    /**
     265     * @covers WP_REST_Global_Styles_Controller::update_item
     266     * @ticket 54516
     267     */
     268    public function test_update_item_no_user() {
     269        wp_set_current_user( 0 );
     270        $request  = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
     271        $response = rest_get_server()->dispatch( $request );
     272        $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 );
     273    }
     274
     275    /**
     276     * @covers WP_REST_Global_Styles_Controller::update_item
     277     * @ticket 54516
     278     */
     279    public function test_update_item_invalid_post() {
     280        wp_set_current_user( self::$admin_id );
     281        $request  = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$post_id );
     282        $response = rest_get_server()->dispatch( $request );
     283        $this->assertErrorResponse( 'rest_global_styles_not_found', $response, 404 );
     284    }
     285
     286    /**
     287     * @covers WP_REST_Global_Styles_Controller::update_item
     288     * @ticket 54516
     289     */
     290    public function test_update_item_permission_check() {
     291        wp_set_current_user( self::$subscriber_id );
     292        $request  = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id );
     293        $response = rest_get_server()->dispatch( $request );
     294        $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
     295    }
     296
    119297    public function test_delete_item() {
    120         $this->markTestIncomplete();
     298        $this->markTestSkipped( 'Controller does not implement delete_item().' );
    121299    }
    122300
    123301    public function test_prepare_item() {
    124         // TODO: Implement test_prepare_item() method.
    125         $this->markTestIncomplete();
    126     }
    127 
     302        $this->markTestSkipped( 'Controller does not implement prepare_item().' );
     303    }
     304
     305    /**
     306     * @covers WP_REST_Global_Styles_Controller::get_item_schema
     307     * @ticket 54516
     308     */
    128309    public function test_get_item_schema() {
    129         // TODO: Implement test_get_item_schema() method.
    130         $this->markTestIncomplete();
     310        $request    = new WP_REST_Request( 'OPTIONS', '/wp/v2/global-styles/' . self::$global_styles_id );
     311        $response   = rest_get_server()->dispatch( $request );
     312        $data       = $response->get_data();
     313        $properties = $data['schema']['properties'];
     314        $this->assertCount( 4, $properties, 'Schema properties array does not have exactly 4 elements' );
     315        $this->assertArrayHasKey( 'id', $properties, 'Schema properties array does not have "id" key' );
     316        $this->assertArrayHasKey( 'styles', $properties, 'Schema properties array does not have "styles" key' );
     317        $this->assertArrayHasKey( 'settings', $properties, 'Schema properties array does not have "settings" key' );
     318        $this->assertArrayHasKey( 'title', $properties, 'Schema properties array does not have "title" key' );
    131319    }
    132320}
Note: See TracChangeset for help on using the changeset viewer.