WordPress.org

Make WordPress Core

Changeset 49925


Ignore:
Timestamp:
01/03/2021 09:45:42 PM (9 months ago)
Author:
TimothyBlynJacobs
Message:

REST API: Expose all themes in the themes controller.

Previously, only the active theme was made available. This commit allows for all themes to be queried if the user has the switch_themes or manage_network_themes capabilities.

This commit also no longer exposes the page, per_page, search and context query parameters since they are not supported by this controller.

Props spacedmonkey, lpawlik, TimothyBlynJacobs.
Fixes #50152.

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/class-wp-rest-server.php

    r49918 r49925  
    12451245
    12461246        $response = new WP_REST_Response( $available );
    1247 
    12481247        $response->add_link( 'help', 'http://v2.wp-api.org/' );
     1248        $this->add_active_theme_link_to_index( $response );
    12491249
    12501250        /**
     
    12601260         */
    12611261        return apply_filters( 'rest_index', $response );
     1262    }
     1263
     1264    /**
     1265     * Adds a link to the active theme for users who have proper permissions.
     1266     *
     1267     * @since 5.7.0
     1268     *
     1269     * @param WP_REST_Response $response REST API response.
     1270     */
     1271    protected function add_active_theme_link_to_index( WP_REST_Response $response ) {
     1272        $should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' );
     1273
     1274        if ( ! $should_add && current_user_can( 'edit_posts' ) ) {
     1275            $should_add = true;
     1276        }
     1277
     1278        if ( ! $should_add ) {
     1279            foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
     1280                if ( current_user_can( $post_type->cap->edit_posts ) ) {
     1281                    $should_add = true;
     1282                    break;
     1283                }
     1284            }
     1285        }
     1286
     1287        if ( $should_add ) {
     1288            $theme = wp_get_theme();
     1289            $response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) );
     1290        }
    12621291    }
    12631292
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-themes-controller.php

    r48782 r49925  
    4848            )
    4949        );
     50
     51        register_rest_route(
     52            $this->namespace,
     53            '/' . $this->rest_base . '/(?P<stylesheet>[\w-]+)',
     54            array(
     55                'args'   => array(
     56                    'stylesheet' => array(
     57                        'description' => __( "The theme's stylesheet. This uniquely identifies the theme." ),
     58                        'type'        => 'string',
     59                    ),
     60                ),
     61                array(
     62                    'methods'             => WP_REST_Server::READABLE,
     63                    'callback'            => array( $this, 'get_item' ),
     64                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
     65                ),
     66                'schema' => array( $this, 'get_public_item_schema' ),
     67            )
     68        );
    5069    }
    5170
     
    5978     */
    6079    public function get_items_permissions_check( $request ) {
     80        if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
     81            return true;
     82        }
     83
     84        $registered = $this->get_collection_params();
     85        if ( isset( $registered['status'], $request['status'] ) && is_array( $request['status'] ) && array( 'active' ) === $request['status'] ) {
     86            return $this->check_read_active_theme_permission();
     87        }
     88
     89        return new WP_Error(
     90            'rest_cannot_view_themes',
     91            __( 'Sorry, you are not allowed to view themes.' ),
     92            array( 'status' => rest_authorization_required_code() )
     93        );
     94    }
     95
     96    /**
     97     * Checks if a given request has access to read the theme.
     98     *
     99     * @since 5.7.0
     100     *
     101     * @param WP_REST_Request $request Full details about the request.
     102     * @return bool|WP_Error True if the request has read access for the item, otherwise WP_Error object.
     103     */
     104    public function get_item_permissions_check( $request ) {
     105        if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
     106            return true;
     107        }
     108
     109        $wp_theme      = wp_get_theme( $request['stylesheet'] );
     110        $current_theme = wp_get_theme();
     111
     112        if ( $this->is_same_theme( $wp_theme, $current_theme ) ) {
     113            return $this->check_read_active_theme_permission();
     114        }
     115
     116        return new WP_Error(
     117            'rest_cannot_view_themes',
     118            __( 'Sorry, you are not allowed to view themes.' ),
     119            array( 'status' => rest_authorization_required_code() )
     120        );
     121    }
     122
     123    /**
     124     * Checks if a theme can be read.
     125     *
     126     * @since 5.7.0
     127     *
     128     * @return bool|WP_Error Whether the theme can be read.
     129     */
     130    protected function check_read_active_theme_permission() {
    61131        if ( current_user_can( 'edit_posts' ) ) {
    62132            return true;
     
    70140
    71141        return new WP_Error(
    72             'rest_user_cannot_view',
    73             __( 'Sorry, you are not allowed to view themes.' ),
     142            'rest_cannot_view_active_theme',
     143            __( 'Sorry, you are not allowed to view the active theme.' ),
    74144            array( 'status' => rest_authorization_required_code() )
    75145        );
     
    77147
    78148    /**
    79      * Retrieves a collection of themes.
    80      *
    81      * @since 5.0.0
     149     * Retrieves a single theme.
     150     *
     151     * @since 5.7.0
    82152     *
    83153     * @param WP_REST_Request $request Full details about the request.
    84154     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
    85155     */
     156    public function get_item( $request ) {
     157        $wp_theme = wp_get_theme( $request['stylesheet'] );
     158        if ( ! $wp_theme->exists() ) {
     159            return new WP_Error(
     160                'rest_theme_not_found',
     161                __( 'Theme not found.' ),
     162                array( 'status' => 404 )
     163            );
     164        }
     165        $data = $this->prepare_item_for_response( $wp_theme, $request );
     166
     167        return rest_ensure_response( $data );
     168    }
     169
     170    /**
     171     * Retrieves a collection of themes.
     172     *
     173     * @since 5.0.0
     174     *
     175     * @param WP_REST_Request $request Full details about the request.
     176     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     177     */
    86178    public function get_items( $request ) {
    87         // Retrieve the list of registered collection query parameters.
    88         $registered = $this->get_collection_params();
    89         $themes     = array();
    90 
    91         if ( isset( $registered['status'], $request['status'] ) && in_array( 'active', $request['status'], true ) ) {
    92             $active_theme = wp_get_theme();
    93             $active_theme = $this->prepare_item_for_response( $active_theme, $request );
    94             $themes[]     = $this->prepare_response_for_collection( $active_theme );
     179        $themes = array();
     180
     181        $active_themes = wp_get_themes();
     182        $current_theme = wp_get_theme();
     183        $status        = $request['status'];
     184
     185        foreach ( $active_themes as $theme_name => $theme ) {
     186            $theme_status = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
     187            if ( is_array( $status ) && ! in_array( $theme_status, $status, true ) ) {
     188                continue;
     189            }
     190
     191            $prepared = $this->prepare_item_for_response( $theme, $request );
     192            $themes[] = $this->prepare_response_for_collection( $prepared );
    95193        }
    96194
     
    98196
    99197        $response->header( 'X-WP-Total', count( $themes ) );
    100         $response->header( 'X-WP-TotalPages', count( $themes ) );
     198        $response->header( 'X-WP-TotalPages', 1 );
    101199
    102200        return $response;
     
    167265        }
    168266
    169         if ( rest_is_field_included( 'theme_supports', $fields ) ) {
     267        $current_theme = wp_get_theme();
     268        if ( rest_is_field_included( 'status', $fields ) ) {
     269            $data['status'] = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
     270        }
     271
     272        if ( rest_is_field_included( 'theme_supports', $fields ) && $this->is_same_theme( $theme, $current_theme ) ) {
    170273            foreach ( get_registered_theme_features() as $feature => $config ) {
    171274                if ( ! is_array( $config['show_in_rest'] ) ) {
     
    206309        // Wrap the data in a response object.
    207310        $response = rest_ensure_response( $data );
     311
     312        $response->add_links( $this->prepare_links( $theme ) );
    208313
    209314        /**
     
    220325
    221326    /**
     327     * Prepares links for the request.
     328     *
     329     * @since 5.7.0
     330     *
     331     * @param WP_Theme $theme Theme data.
     332     * @return array Links for the given block type.
     333     */
     334    protected function prepare_links( $theme ) {
     335        return array(
     336            'self'       => array(
     337                'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $theme->get_stylesheet() ) ),
     338            ),
     339            'collection' => array(
     340                'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
     341            ),
     342        );
     343    }
     344
     345    /**
     346     * Helper function to compare two themes.
     347     *
     348     * @since 5.7.0
     349     *
     350     * @param WP_Theme $theme_a First theme to compare.
     351     * @param WP_Theme $theme_b Second theme to compare.
     352     *
     353     * @return bool
     354     */
     355    protected function is_same_theme( $theme_a, $theme_b ) {
     356        return $theme_a->get_stylesheet() === $theme_b->get_stylesheet();
     357    }
     358
     359    /**
    222360     * Prepares the theme support value for inclusion in the REST API response.
    223361     *
     
    400538                    'readonly'    => true,
    401539                ),
     540                'status'         => array(
     541                    'description' => __( 'A named status for the theme.' ),
     542                    'type'        => 'string',
     543                    'enum'        => array( 'inactive', 'active' ),
     544                ),
    402545            ),
    403546        );
     
    426569     */
    427570    public function get_collection_params() {
    428         $query_params = parent::get_collection_params();
    429 
    430         $query_params['status'] = array(
    431             'description'       => __( 'Limit result set to themes assigned one or more statuses.' ),
    432             'type'              => 'array',
    433             'items'             => array(
    434                 'enum' => array( 'active' ),
    435                 'type' => 'string',
     571        $query_params = array(
     572            'status' => array(
     573                'description' => __( 'Limit result set to themes assigned one or more statuses.' ),
     574                'type'        => 'array',
     575                'items'       => array(
     576                    'enum' => array( 'active', 'inactive' ),
     577                    'type' => 'string',
     578                ),
    436579            ),
    437             'required'          => true,
    438             'sanitize_callback' => array( $this, 'sanitize_theme_status' ),
    439580        );
    440581
     
    453594     *
    454595     * @since 5.0.0
     596     * @deprecated 5.7.0
    455597     *
    456598     * @param string|array    $statuses  One or more theme statuses.
     
    460602     */
    461603    public function sanitize_theme_status( $statuses, $request, $parameter ) {
     604        _deprecated_function( __METHOD__, '5.7.0' );
     605
    462606        $statuses = wp_parse_slug_list( $statuses );
    463607
  • trunk/tests/phpunit/tests/rest-api/rest-schema-setup.php

    r49334 r49925  
    130130            '/wp/v2/settings',
    131131            '/wp/v2/themes',
     132            '/wp/v2/themes/(?P<stylesheet>[\w-]+)',
    132133            '/wp/v2/plugins',
    133134            '/wp/v2/plugins/(?P<plugin>[^.\/]+(?:\/[^.\/]+)?)',
  • trunk/tests/phpunit/tests/rest-api/rest-server.php

    r49547 r49925  
    960960        $this->assertContains( 'DELETE', $route['methods'] );
    961961        $this->assertArrayHasKey( '_links', $route );
     962
     963        $this->assertArrayHasKey( 'help', $index->get_links() );
     964        $this->assertArrayNotHasKey( 'wp:active-theme', $index->get_links() );
    962965    }
    963966
     
    19972000    }
    19982001
     2002    /**
     2003     * @ticket 50152
     2004     */
     2005    public function test_index_includes_link_to_active_theme_if_authenticated() {
     2006        wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
     2007
     2008        $index = rest_do_request( '/' );
     2009        $this->assertArrayHasKey( 'https://api.w.org/active-theme', $index->get_links() );
     2010    }
     2011
    19992012    public function _validate_as_integer_123( $value, $request, $key ) {
    20002013        if ( ! is_int( $value ) ) {
  • trunk/tests/phpunit/tests/rest-api/rest-themes-controller.php

    r49603 r49925  
    3131
    3232    /**
     33     * Admin user ID.
     34     *
     35     * @since 5.7.0
     36     *
     37     * @var int $admin_id
     38     */
     39    protected static $admin_id;
     40
     41    /**
    3342     * The current theme object.
    3443     *
     
    92101     */
    93102    public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
     103        self::$admin_id       = $factory->user->create(
     104            array(
     105                'role' => 'administrator',
     106            )
     107        );
    94108        self::$subscriber_id  = $factory->user->create(
    95109            array(
     
    115129        self::delete_user( self::$subscriber_id );
    116130        self::delete_user( self::$contributor_id );
     131        self::delete_user( self::$admin_id );
    117132    }
    118133
     
    137152        $routes = rest_get_server()->get_routes();
    138153        $this->assertArrayHasKey( self::$themes_route, $routes );
     154        $this->assertArrayHasKey( self::$themes_route . '/(?P<stylesheet>[\\w-]+)', $routes );
    139155    }
    140156
     
    152168        $this->check_get_theme_response( $response );
    153169        $fields = array(
     170            '_links',
    154171            'author',
    155172            'author_uri',
     
    159176            'requires_wp',
    160177            'screenshot',
     178            'status',
    161179            'stylesheet',
    162180            'tags',
     
    171189
    172190    /**
     191     * Test retrieving a collection of inactive themes.
     192     *
     193     * @ticket 50152
     194     */
     195    public function test_get_items_inactive() {
     196        wp_set_current_user( self::$admin_id );
     197        $request = new WP_REST_Request( 'GET', self::$themes_route );
     198        $request->set_param( 'status', 'inactive' );
     199
     200        $response = rest_get_server()->dispatch( $request );
     201
     202        $this->assertEquals( 200, $response->get_status() );
     203        $data = $response->get_data();
     204
     205        $fields = array(
     206            '_links',
     207            'author',
     208            'author_uri',
     209            'description',
     210            'name',
     211            'requires_php',
     212            'requires_wp',
     213            'screenshot',
     214            'status',
     215            'stylesheet',
     216            'tags',
     217            'template',
     218            'textdomain',
     219            'theme_uri',
     220            'version',
     221        );
     222        $this->assertEqualSets( $fields, array_keys( $data[0] ) );
     223
     224        $this->assertContains( 'twentytwenty', wp_list_pluck( $data, 'stylesheet' ) );
     225        $this->assertNotContains( get_stylesheet(), wp_list_pluck( $data, 'stylesheet' ) );
     226    }
     227
     228    /**
     229     * Test retrieving a collection of inactive themes.
     230     *
     231     * @ticket 50152
     232     */
     233    public function test_get_items_active_and_inactive() {
     234        wp_set_current_user( self::$admin_id );
     235        $request = new WP_REST_Request( 'GET', self::$themes_route );
     236        $request->set_param( 'status', array( 'active', 'inactive' ) );
     237
     238        $response = rest_get_server()->dispatch( $request );
     239
     240        $this->assertEquals( 200, $response->get_status() );
     241        $data = $response->get_data();
     242
     243        $this->assertContains( 'twentytwenty', wp_list_pluck( $data, 'stylesheet' ) );
     244        $this->assertContains( get_stylesheet(), wp_list_pluck( $data, 'stylesheet' ) );
     245    }
     246
     247    /**
    173248     * @ticket 46723
    174      */
    175     public function test_get_items_logged_out() {
     249     * @ticket 50152
     250     * @dataProvider data_get_items_by_status
     251     */
     252    public function test_get_items_logged_out( $status, $error_code ) {
    176253        wp_set_current_user( 0 );
    177         $response = self::perform_active_theme_request();
    178         $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 );
     254        $request = new WP_REST_Request( 'GET', self::$themes_route );
     255        $request->set_param( 'status', $status );
     256
     257        $response = rest_get_server()->dispatch( $request );
     258        $this->assertErrorResponse( $error_code, $response, 401 );
    179259    }
    180260
     
    183263     *
    184264     * @ticket 45016
    185      */
    186     public function test_get_items_no_permission() {
     265     * @ticket 50152
     266     * @dataProvider data_get_items_by_status
     267     */
     268    public function test_get_items_no_permission( $status, $error_code ) {
    187269        wp_set_current_user( self::$subscriber_id );
    188         $response = self::perform_active_theme_request();
    189         $this->assertErrorResponse( 'rest_user_cannot_view', $response, 403 );
     270        $request = new WP_REST_Request( 'GET', self::$themes_route );
     271        $request->set_param( 'status', $status );
     272
     273        $response = rest_get_server()->dispatch( $request );
     274        $this->assertErrorResponse( $error_code, $response, 403 );
     275    }
     276
     277    public function data_get_items_by_status() {
     278        return array(
     279            array( 'active', 'rest_cannot_view_active_theme' ),
     280            array( 'active, inactive', 'rest_cannot_view_themes' ),
     281            array( 'inactive', 'rest_cannot_view_themes' ),
     282            array( '', 'rest_cannot_view_themes' ),
     283        );
     284    }
     285
     286    /**
     287     * @ticket 50152
     288     * @dataProvider data_get_items_by_status_for_contributor
     289     */
     290    public function test_get_items_contributor( $status, $error_code ) {
     291        wp_set_current_user( self::$contributor_id );
     292        $request = new WP_REST_Request( 'GET', self::$themes_route );
     293        $request->set_param( 'status', $status );
     294
     295        $response = rest_get_server()->dispatch( $request );
     296
     297        if ( $error_code ) {
     298            $this->assertErrorResponse( $error_code, $response, 403 );
     299        } else {
     300            $this->assertEquals( 200, $response->get_status() );
     301        }
     302    }
     303
     304    public function data_get_items_by_status_for_contributor() {
     305        return array(
     306            array( 'active', '' ),
     307            array( 'active, inactive', 'rest_cannot_view_themes' ),
     308            array( 'inactive', 'rest_cannot_view_themes' ),
     309            array( '', 'rest_cannot_view_themes' ),
     310        );
    190311    }
    191312
     
    222343        $data       = $response->get_data();
    223344        $properties = $data['schema']['properties'];
    224         $this->assertSame( 14, count( $properties ) );
     345        $this->assertSame( 15, count( $properties ) );
    225346
    226347        $this->assertArrayHasKey( 'author', $properties );
     
    243364        $this->assertArrayHasKey( 'requires_wp', $properties );
    244365        $this->assertArrayHasKey( 'screenshot', $properties );
     366        $this->assertArrayHasKey( 'status', $properties );
    245367        $this->assertArrayHasKey( 'stylesheet', $properties );
    246368
     
    10841206
    10851207    /**
    1086      * The get_item() method does not exist for themes.
    1087      */
    1088     public function test_get_item() {}
     1208     * Test single theme
     1209     *
     1210     * @ticket 50152
     1211     */
     1212    public function test_get_item() {
     1213        wp_set_current_user( self::$admin_id );
     1214        $route    = sprintf( '%s/%s', self::$themes_route, WP_DEFAULT_THEME );
     1215        $request  = new WP_REST_Request( 'GET', $route );
     1216        $response = rest_get_server()->dispatch( $request );
     1217
     1218        $this->assertEquals( 200, $response->get_status() );
     1219        $data         = $response->get_data();
     1220        $links        = $response->get_links();
     1221        $fields       = array(
     1222            'author',
     1223            'author_uri',
     1224            'description',
     1225            'name',
     1226            'requires_php',
     1227            'requires_wp',
     1228            'screenshot',
     1229            'status',
     1230            'stylesheet',
     1231            'tags',
     1232            'template',
     1233            'textdomain',
     1234            'theme_uri',
     1235            'version',
     1236        );
     1237        $fields_links = array( 'collection', 'self' );
     1238
     1239        $this->assertEqualSets( $fields, array_keys( $data ) );
     1240        $this->assertEqualSets( $fields_links, array_keys( $links ) );
     1241    }
     1242
     1243    /**
     1244     * @ticket 50152
     1245     */
     1246    public function test_get_item_no_permission() {
     1247        wp_set_current_user( self::$subscriber_id );
     1248        $request  = new WP_REST_Request( 'GET', self::$themes_route . '/' . WP_DEFAULT_THEME );
     1249        $response = rest_get_server()->dispatch( $request );
     1250        $this->assertErrorResponse( 'rest_cannot_view_themes', $response, 403 );
     1251    }
     1252
     1253    /**
     1254     * @ticket 50152
     1255     */
     1256    public function test_get_active_item_no_permission() {
     1257        wp_set_current_user( self::$subscriber_id );
     1258        $request  = new WP_REST_Request( 'GET', self::$themes_route . '/' . get_stylesheet() );
     1259        $response = rest_get_server()->dispatch( $request );
     1260        $this->assertErrorResponse( 'rest_cannot_view_active_theme', $response, 403 );
     1261    }
     1262
     1263    /**
     1264     * @ticket 50152
     1265     */
     1266    public function test_get_item_invalid() {
     1267        wp_set_current_user( self::$admin_id );
     1268        $request  = new WP_REST_Request( 'GET', self::$themes_route . '/invalid' );
     1269        $response = rest_get_server()->dispatch( $request );
     1270        $this->assertErrorResponse( 'rest_theme_not_found', $response, 404 );
     1271    }
     1272
     1273    /**
     1274     * @ticket 50152
     1275     */
     1276    public function test_get_active_item_as_contributor() {
     1277        $route    = sprintf( '%s/%s', self::$themes_route, get_stylesheet() );
     1278        $request  = new WP_REST_Request( 'GET', $route );
     1279        $response = rest_get_server()->dispatch( $request );
     1280
     1281        $this->assertEquals( 200, $response->get_status() );
     1282    }
    10891283
    10901284    /**
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r49476 r49925  
    57855785                    ],
    57865786                    "args": {
    5787                         "context": {
    5788                             "description": "Scope under which the request is made; determines fields present in response.",
    5789                             "type": "string",
    5790                             "required": false
    5791                         },
    5792                         "page": {
    5793                             "description": "Current page of the collection.",
    5794                             "type": "integer",
    5795                             "default": 1,
    5796                             "minimum": 1,
    5797                             "required": false
    5798                         },
    5799                         "per_page": {
    5800                             "description": "Maximum number of items to be returned in result set.",
    5801                             "type": "integer",
    5802                             "default": 10,
    5803                             "minimum": 1,
    5804                             "maximum": 100,
    5805                             "required": false
    5806                         },
    5807                         "search": {
    5808                             "description": "Limit results to those matching a string.",
    5809                             "type": "string",
    5810                             "required": false
    5811                         },
    58125787                        "status": {
    58135788                            "description": "Limit result set to themes assigned one or more statuses.",
     
    58155790                            "items": {
    58165791                                "enum": [
    5817                                     "active"
     5792                                    "active",
     5793                                    "inactive"
    58185794                                ],
    58195795                                "type": "string"
    58205796                            },
    5821                             "required": true
     5797                            "required": false
    58225798                        }
    58235799                    }
     
    58275803                "self": "http://example.org/index.php?rest_route=/wp/v2/themes"
    58285804            }
     5805        },
     5806        "/wp/v2/themes/(?P<stylesheet>[\\w-]+)": {
     5807            "namespace": "wp/v2",
     5808            "methods": [
     5809                "GET"
     5810            ],
     5811            "endpoints": [
     5812                {
     5813                    "methods": [
     5814                        "GET"
     5815                    ],
     5816                    "args": {
     5817                        "stylesheet": {
     5818                            "description": "The theme's stylesheet. This uniquely identifies the theme.",
     5819                            "type": "string",
     5820                            "required": false
     5821                        }
     5822                    }
     5823                }
     5824            ]
    58295825        },
    58305826        "/wp/v2/plugins": {
Note: See TracChangeset for help on using the changeset viewer.