class-wp-rest-themes-controller.php (18127B)
1 <?php 2 /** 3 * REST API: WP_REST_Themes_Controller class 4 * 5 * @package WordPress 6 * @subpackage REST_API 7 * @since 5.0.0 8 */ 9 10 /** 11 * Core class used to manage themes via the REST API. 12 * 13 * @since 5.0.0 14 * 15 * @see WP_REST_Controller 16 */ 17 class WP_REST_Themes_Controller extends WP_REST_Controller { 18 19 /** 20 * Constructor. 21 * 22 * @since 5.0.0 23 */ 24 public function __construct() { 25 $this->namespace = 'wp/v2'; 26 $this->rest_base = 'themes'; 27 } 28 29 /** 30 * Registers the routes for themes. 31 * 32 * @since 5.0.0 33 * 34 * @see register_rest_route() 35 */ 36 public function register_routes() { 37 register_rest_route( 38 $this->namespace, 39 '/' . $this->rest_base, 40 array( 41 array( 42 'methods' => WP_REST_Server::READABLE, 43 'callback' => array( $this, 'get_items' ), 44 'permission_callback' => array( $this, 'get_items_permissions_check' ), 45 'args' => $this->get_collection_params(), 46 ), 47 'schema' => array( $this, 'get_item_schema' ), 48 ) 49 ); 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 ); 69 } 70 71 /** 72 * Checks if a given request has access to read the theme. 73 * 74 * @since 5.0.0 75 * 76 * @param WP_REST_Request $request Full details about the request. 77 * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object. 78 */ 79 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() { 131 if ( current_user_can( 'edit_posts' ) ) { 132 return true; 133 } 134 135 foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 136 if ( current_user_can( $post_type->cap->edit_posts ) ) { 137 return true; 138 } 139 } 140 141 return new WP_Error( 142 'rest_cannot_view_active_theme', 143 __( 'Sorry, you are not allowed to view the active theme.' ), 144 array( 'status' => rest_authorization_required_code() ) 145 ); 146 } 147 148 /** 149 * Retrieves a single theme. 150 * 151 * @since 5.7.0 152 * 153 * @param WP_REST_Request $request Full details about the request. 154 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 155 */ 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 */ 178 public function get_items( $request ) { 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 ); 193 } 194 195 $response = rest_ensure_response( $themes ); 196 197 $response->header( 'X-WP-Total', count( $themes ) ); 198 $response->header( 'X-WP-TotalPages', 1 ); 199 200 return $response; 201 } 202 203 /** 204 * Prepares a single theme output for response. 205 * 206 * @since 5.0.0 207 * 208 * @param WP_Theme $theme Theme object. 209 * @param WP_REST_Request $request Request object. 210 * @return WP_REST_Response Response object. 211 */ 212 public function prepare_item_for_response( $theme, $request ) { 213 $data = array(); 214 $fields = $this->get_fields_for_response( $request ); 215 216 if ( rest_is_field_included( 'stylesheet', $fields ) ) { 217 $data['stylesheet'] = $theme->get_stylesheet(); 218 } 219 220 if ( rest_is_field_included( 'template', $fields ) ) { 221 /** 222 * Use the get_template() method, not the 'Template' header, for finding the template. 223 * The 'Template' header is only good for what was written in the style.css, while 224 * get_template() takes into account where WordPress actually located the theme and 225 * whether it is actually valid. 226 */ 227 $data['template'] = $theme->get_template(); 228 } 229 230 $plain_field_mappings = array( 231 'requires_php' => 'RequiresPHP', 232 'requires_wp' => 'RequiresWP', 233 'textdomain' => 'TextDomain', 234 'version' => 'Version', 235 ); 236 237 foreach ( $plain_field_mappings as $field => $header ) { 238 if ( rest_is_field_included( $field, $fields ) ) { 239 $data[ $field ] = $theme->get( $header ); 240 } 241 } 242 243 if ( rest_is_field_included( 'screenshot', $fields ) ) { 244 // Using $theme->get_screenshot() with no args to get absolute URL. 245 $data['screenshot'] = $theme->get_screenshot() ? $theme->get_screenshot() : ''; 246 } 247 248 $rich_field_mappings = array( 249 'author' => 'Author', 250 'author_uri' => 'AuthorURI', 251 'description' => 'Description', 252 'name' => 'Name', 253 'tags' => 'Tags', 254 'theme_uri' => 'ThemeURI', 255 ); 256 257 foreach ( $rich_field_mappings as $field => $header ) { 258 if ( rest_is_field_included( "{$field}.raw", $fields ) ) { 259 $data[ $field ]['raw'] = $theme->display( $header, false, true ); 260 } 261 262 if ( rest_is_field_included( "{$field}.rendered", $fields ) ) { 263 $data[ $field ]['rendered'] = $theme->display( $header ); 264 } 265 } 266 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 ) ) { 273 foreach ( get_registered_theme_features() as $feature => $config ) { 274 if ( ! is_array( $config['show_in_rest'] ) ) { 275 continue; 276 } 277 278 $name = $config['show_in_rest']['name']; 279 280 if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) { 281 continue; 282 } 283 284 if ( ! current_theme_supports( $feature ) ) { 285 $data['theme_supports'][ $name ] = $config['show_in_rest']['schema']['default']; 286 continue; 287 } 288 289 $support = get_theme_support( $feature ); 290 291 if ( isset( $config['show_in_rest']['prepare_callback'] ) ) { 292 $prepare = $config['show_in_rest']['prepare_callback']; 293 } else { 294 $prepare = array( $this, 'prepare_theme_support' ); 295 } 296 297 $prepared = $prepare( $support, $config, $feature, $request ); 298 299 if ( is_wp_error( $prepared ) ) { 300 continue; 301 } 302 303 $data['theme_supports'][ $name ] = $prepared; 304 } 305 } 306 307 $data = $this->add_additional_fields_to_object( $data, $request ); 308 309 // Wrap the data in a response object. 310 $response = rest_ensure_response( $data ); 311 312 $response->add_links( $this->prepare_links( $theme ) ); 313 314 /** 315 * Filters theme data returned from the REST API. 316 * 317 * @since 5.0.0 318 * 319 * @param WP_REST_Response $response The response object. 320 * @param WP_Theme $theme Theme object used to create response. 321 * @param WP_REST_Request $request Request object. 322 */ 323 return apply_filters( 'rest_prepare_theme', $response, $theme, $request ); 324 } 325 326 /** 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 * @return bool 353 */ 354 protected function is_same_theme( $theme_a, $theme_b ) { 355 return $theme_a->get_stylesheet() === $theme_b->get_stylesheet(); 356 } 357 358 /** 359 * Prepares the theme support value for inclusion in the REST API response. 360 * 361 * @since 5.5.0 362 * 363 * @param mixed $support The raw value from get_theme_support(). 364 * @param array $args The feature's registration args. 365 * @param string $feature The feature name. 366 * @param WP_REST_Request $request The request object. 367 * @return mixed The prepared support value. 368 */ 369 protected function prepare_theme_support( $support, $args, $feature, $request ) { 370 $schema = $args['show_in_rest']['schema']; 371 372 if ( 'boolean' === $schema['type'] ) { 373 return true; 374 } 375 376 if ( is_array( $support ) && ! $args['variadic'] ) { 377 $support = $support[0]; 378 } 379 380 return rest_sanitize_value_from_schema( $support, $schema ); 381 } 382 383 /** 384 * Retrieves the theme's schema, conforming to JSON Schema. 385 * 386 * @since 5.0.0 387 * 388 * @return array Item schema data. 389 */ 390 public function get_item_schema() { 391 if ( $this->schema ) { 392 return $this->add_additional_fields_schema( $this->schema ); 393 } 394 395 $schema = array( 396 '$schema' => 'http://json-schema.org/draft-04/schema#', 397 'title' => 'theme', 398 'type' => 'object', 399 'properties' => array( 400 'stylesheet' => array( 401 'description' => __( 'The theme\'s stylesheet. This uniquely identifies the theme.' ), 402 'type' => 'string', 403 'readonly' => true, 404 ), 405 'template' => array( 406 'description' => __( 'The theme\'s template. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme\'s stylesheet.' ), 407 'type' => 'string', 408 'readonly' => true, 409 ), 410 'author' => array( 411 'description' => __( 'The theme author.' ), 412 'type' => 'object', 413 'readonly' => true, 414 'properties' => array( 415 'raw' => array( 416 'description' => __( 'The theme author\'s name, as found in the theme header.' ), 417 'type' => 'string', 418 ), 419 'rendered' => array( 420 'description' => __( 'HTML for the theme author, transformed for display.' ), 421 'type' => 'string', 422 ), 423 ), 424 ), 425 'author_uri' => array( 426 'description' => __( 'The website of the theme author.' ), 427 'type' => 'object', 428 'readonly' => true, 429 'properties' => array( 430 'raw' => array( 431 'description' => __( 'The website of the theme author, as found in the theme header.' ), 432 'type' => 'string', 433 'format' => 'uri', 434 ), 435 'rendered' => array( 436 'description' => __( 'The website of the theme author, transformed for display.' ), 437 'type' => 'string', 438 'format' => 'uri', 439 ), 440 ), 441 ), 442 'description' => array( 443 'description' => __( 'A description of the theme.' ), 444 'type' => 'object', 445 'readonly' => true, 446 'properties' => array( 447 'raw' => array( 448 'description' => __( 'The theme description, as found in the theme header.' ), 449 'type' => 'string', 450 ), 451 'rendered' => array( 452 'description' => __( 'The theme description, transformed for display.' ), 453 'type' => 'string', 454 ), 455 ), 456 ), 457 'name' => array( 458 'description' => __( 'The name of the theme.' ), 459 'type' => 'object', 460 'readonly' => true, 461 'properties' => array( 462 'raw' => array( 463 'description' => __( 'The theme name, as found in the theme header.' ), 464 'type' => 'string', 465 ), 466 'rendered' => array( 467 'description' => __( 'The theme name, transformed for display.' ), 468 'type' => 'string', 469 ), 470 ), 471 ), 472 'requires_php' => array( 473 'description' => __( 'The minimum PHP version required for the theme to work.' ), 474 'type' => 'string', 475 'readonly' => true, 476 ), 477 'requires_wp' => array( 478 'description' => __( 'The minimum WordPress version required for the theme to work.' ), 479 'type' => 'string', 480 'readonly' => true, 481 ), 482 'screenshot' => array( 483 'description' => __( 'The theme\'s screenshot URL.' ), 484 'type' => 'string', 485 'format' => 'uri', 486 'readonly' => true, 487 ), 488 'tags' => array( 489 'description' => __( 'Tags indicating styles and features of the theme.' ), 490 'type' => 'object', 491 'readonly' => true, 492 'properties' => array( 493 'raw' => array( 494 'description' => __( 'The theme tags, as found in the theme header.' ), 495 'type' => 'array', 496 'items' => array( 497 'type' => 'string', 498 ), 499 ), 500 'rendered' => array( 501 'description' => __( 'The theme tags, transformed for display.' ), 502 'type' => 'string', 503 ), 504 ), 505 ), 506 'textdomain' => array( 507 'description' => __( 'The theme\'s text domain.' ), 508 'type' => 'string', 509 'readonly' => true, 510 ), 511 'theme_supports' => array( 512 'description' => __( 'Features supported by this theme.' ), 513 'type' => 'object', 514 'readonly' => true, 515 'properties' => array(), 516 ), 517 'theme_uri' => array( 518 'description' => __( 'The URI of the theme\'s webpage.' ), 519 'type' => 'object', 520 'readonly' => true, 521 'properties' => array( 522 'raw' => array( 523 'description' => __( 'The URI of the theme\'s webpage, as found in the theme header.' ), 524 'type' => 'string', 525 'format' => 'uri', 526 ), 527 'rendered' => array( 528 'description' => __( 'The URI of the theme\'s webpage, transformed for display.' ), 529 'type' => 'string', 530 'format' => 'uri', 531 ), 532 ), 533 ), 534 'version' => array( 535 'description' => __( 'The theme\'s current version.' ), 536 'type' => 'string', 537 'readonly' => true, 538 ), 539 'status' => array( 540 'description' => __( 'A named status for the theme.' ), 541 'type' => 'string', 542 'enum' => array( 'inactive', 'active' ), 543 ), 544 ), 545 ); 546 547 foreach ( get_registered_theme_features() as $feature => $config ) { 548 if ( ! is_array( $config['show_in_rest'] ) ) { 549 continue; 550 } 551 552 $name = $config['show_in_rest']['name']; 553 554 $schema['properties']['theme_supports']['properties'][ $name ] = $config['show_in_rest']['schema']; 555 } 556 557 $this->schema = $schema; 558 559 return $this->add_additional_fields_schema( $this->schema ); 560 } 561 562 /** 563 * Retrieves the search params for the themes collection. 564 * 565 * @since 5.0.0 566 * 567 * @return array Collection parameters. 568 */ 569 public function get_collection_params() { 570 $query_params = array( 571 'status' => array( 572 'description' => __( 'Limit result set to themes assigned one or more statuses.' ), 573 'type' => 'array', 574 'items' => array( 575 'enum' => array( 'active', 'inactive' ), 576 'type' => 'string', 577 ), 578 ), 579 ); 580 581 /** 582 * Filters REST API collection parameters for the themes controller. 583 * 584 * @since 5.0.0 585 * 586 * @param array $query_params JSON Schema-formatted collection parameters. 587 */ 588 return apply_filters( 'rest_themes_collection_params', $query_params ); 589 } 590 591 /** 592 * Sanitizes and validates the list of theme status. 593 * 594 * @since 5.0.0 595 * @deprecated 5.7.0 596 * 597 * @param string|array $statuses One or more theme statuses. 598 * @param WP_REST_Request $request Full details about the request. 599 * @param string $parameter Additional parameter to pass to validation. 600 * @return array|WP_Error A list of valid statuses, otherwise WP_Error object. 601 */ 602 public function sanitize_theme_status( $statuses, $request, $parameter ) { 603 _deprecated_function( __METHOD__, '5.7.0' ); 604 605 $statuses = wp_parse_slug_list( $statuses ); 606 607 foreach ( $statuses as $status ) { 608 $result = rest_validate_request_arg( $status, $request, $parameter ); 609 610 if ( is_wp_error( $result ) ) { 611 return $result; 612 } 613 } 614 615 return $statuses; 616 } 617 }