[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/rest-api/endpoints/ -> class-wp-rest-themes-controller.php (source)

   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       * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
  21       * Excludes invalid directory name characters: `/:<>*?"|`.
  22       */
  23      const PATTERN = '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?';
  24  
  25      /**
  26       * Constructor.
  27       *
  28       * @since 5.0.0
  29       */
  30  	public function __construct() {
  31          $this->namespace = 'wp/v2';
  32          $this->rest_base = 'themes';
  33      }
  34  
  35      /**
  36       * Registers the routes for themes.
  37       *
  38       * @since 5.0.0
  39       *
  40       * @see register_rest_route()
  41       */
  42  	public function register_routes() {
  43          register_rest_route(
  44              $this->namespace,
  45              '/' . $this->rest_base,
  46              array(
  47                  array(
  48                      'methods'             => WP_REST_Server::READABLE,
  49                      'callback'            => array( $this, 'get_items' ),
  50                      'permission_callback' => array( $this, 'get_items_permissions_check' ),
  51                      'args'                => $this->get_collection_params(),
  52                  ),
  53                  'schema' => array( $this, 'get_item_schema' ),
  54              )
  55          );
  56  
  57          register_rest_route(
  58              $this->namespace,
  59              sprintf( '/%s/(?P<stylesheet>%s)', $this->rest_base, self::PATTERN ),
  60              array(
  61                  'args'   => array(
  62                      'stylesheet' => array(
  63                          'description'       => __( "The theme's stylesheet. This uniquely identifies the theme." ),
  64                          'type'              => 'string',
  65                          'sanitize_callback' => array( $this, '_sanitize_stylesheet_callback' ),
  66                      ),
  67                  ),
  68                  array(
  69                      'methods'             => WP_REST_Server::READABLE,
  70                      'callback'            => array( $this, 'get_item' ),
  71                      'permission_callback' => array( $this, 'get_item_permissions_check' ),
  72                  ),
  73                  'schema' => array( $this, 'get_public_item_schema' ),
  74              )
  75          );
  76      }
  77  
  78      /**
  79       * Sanitize the stylesheet to decode endpoint.
  80       *
  81       * @since 5.9.0
  82       *
  83       * @param string $stylesheet The stylesheet name.
  84       * @return string Sanitized stylesheet.
  85       */
  86  	public function _sanitize_stylesheet_callback( $stylesheet ) {
  87          return urldecode( $stylesheet );
  88      }
  89  
  90      /**
  91       * Checks if a given request has access to read the theme.
  92       *
  93       * @since 5.0.0
  94       *
  95       * @param WP_REST_Request $request Full details about the request.
  96       * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
  97       */
  98  	public function get_items_permissions_check( $request ) {
  99          if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
 100              return true;
 101          }
 102  
 103          $registered = $this->get_collection_params();
 104          if ( isset( $registered['status'], $request['status'] ) && is_array( $request['status'] ) && array( 'active' ) === $request['status'] ) {
 105              return $this->check_read_active_theme_permission();
 106          }
 107  
 108          return new WP_Error(
 109              'rest_cannot_view_themes',
 110              __( 'Sorry, you are not allowed to view themes.' ),
 111              array( 'status' => rest_authorization_required_code() )
 112          );
 113      }
 114  
 115      /**
 116       * Checks if a given request has access to read the theme.
 117       *
 118       * @since 5.7.0
 119       *
 120       * @param WP_REST_Request $request Full details about the request.
 121       * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
 122       */
 123  	public function get_item_permissions_check( $request ) {
 124          if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
 125              return true;
 126          }
 127  
 128          $wp_theme      = wp_get_theme( $request['stylesheet'] );
 129          $current_theme = wp_get_theme();
 130  
 131          if ( $this->is_same_theme( $wp_theme, $current_theme ) ) {
 132              return $this->check_read_active_theme_permission();
 133          }
 134  
 135          return new WP_Error(
 136              'rest_cannot_view_themes',
 137              __( 'Sorry, you are not allowed to view themes.' ),
 138              array( 'status' => rest_authorization_required_code() )
 139          );
 140      }
 141  
 142      /**
 143       * Checks if a theme can be read.
 144       *
 145       * @since 5.7.0
 146       *
 147       * @return true|WP_Error True if the theme can be read, WP_Error object otherwise.
 148       */
 149  	protected function check_read_active_theme_permission() {
 150          if ( current_user_can( 'edit_posts' ) ) {
 151              return true;
 152          }
 153  
 154          foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
 155              if ( current_user_can( $post_type->cap->edit_posts ) ) {
 156                  return true;
 157              }
 158          }
 159  
 160          return new WP_Error(
 161              'rest_cannot_view_active_theme',
 162              __( 'Sorry, you are not allowed to view the active theme.' ),
 163              array( 'status' => rest_authorization_required_code() )
 164          );
 165      }
 166  
 167      /**
 168       * Retrieves a single theme.
 169       *
 170       * @since 5.7.0
 171       *
 172       * @param WP_REST_Request $request Full details about the request.
 173       * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
 174       */
 175  	public function get_item( $request ) {
 176          $wp_theme = wp_get_theme( $request['stylesheet'] );
 177          if ( ! $wp_theme->exists() ) {
 178              return new WP_Error(
 179                  'rest_theme_not_found',
 180                  __( 'Theme not found.' ),
 181                  array( 'status' => 404 )
 182              );
 183          }
 184          $data = $this->prepare_item_for_response( $wp_theme, $request );
 185  
 186          return rest_ensure_response( $data );
 187      }
 188  
 189      /**
 190       * Retrieves a collection of themes.
 191       *
 192       * @since 5.0.0
 193       *
 194       * @param WP_REST_Request $request Full details about the request.
 195       * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
 196       */
 197  	public function get_items( $request ) {
 198          $themes = array();
 199  
 200          $active_themes = wp_get_themes();
 201          $current_theme = wp_get_theme();
 202          $status        = $request['status'];
 203  
 204          foreach ( $active_themes as $theme_name => $theme ) {
 205              $theme_status = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
 206              if ( is_array( $status ) && ! in_array( $theme_status, $status, true ) ) {
 207                  continue;
 208              }
 209  
 210              $prepared = $this->prepare_item_for_response( $theme, $request );
 211              $themes[] = $this->prepare_response_for_collection( $prepared );
 212          }
 213  
 214          $response = rest_ensure_response( $themes );
 215  
 216          $response->header( 'X-WP-Total', count( $themes ) );
 217          $response->header( 'X-WP-TotalPages', 1 );
 218  
 219          return $response;
 220      }
 221  
 222      /**
 223       * Prepares a single theme output for response.
 224       *
 225       * @since 5.0.0
 226       * @since 5.9.0 Renamed `$theme` to `$item` to match parent class for PHP 8 named parameter support.
 227       *
 228       * @param WP_Theme        $item    Theme object.
 229       * @param WP_REST_Request $request Request object.
 230       * @return WP_REST_Response Response object.
 231       */
 232  	public function prepare_item_for_response( $item, $request ) {
 233          // Restores the more descriptive, specific name for use within this method.
 234          $theme = $item;
 235  
 236          $fields = $this->get_fields_for_response( $request );
 237          $data   = array();
 238  
 239          if ( rest_is_field_included( 'stylesheet', $fields ) ) {
 240              $data['stylesheet'] = $theme->get_stylesheet();
 241          }
 242  
 243          if ( rest_is_field_included( 'template', $fields ) ) {
 244              /**
 245               * Use the get_template() method, not the 'Template' header, for finding the template.
 246               * The 'Template' header is only good for what was written in the style.css, while
 247               * get_template() takes into account where WordPress actually located the theme and
 248               * whether it is actually valid.
 249               */
 250              $data['template'] = $theme->get_template();
 251          }
 252  
 253          $plain_field_mappings = array(
 254              'requires_php' => 'RequiresPHP',
 255              'requires_wp'  => 'RequiresWP',
 256              'textdomain'   => 'TextDomain',
 257              'version'      => 'Version',
 258          );
 259  
 260          foreach ( $plain_field_mappings as $field => $header ) {
 261              if ( rest_is_field_included( $field, $fields ) ) {
 262                  $data[ $field ] = $theme->get( $header );
 263              }
 264          }
 265  
 266          if ( rest_is_field_included( 'screenshot', $fields ) ) {
 267              // Using $theme->get_screenshot() with no args to get absolute URL.
 268              $data['screenshot'] = $theme->get_screenshot() ? $theme->get_screenshot() : '';
 269          }
 270  
 271          $rich_field_mappings = array(
 272              'author'      => 'Author',
 273              'author_uri'  => 'AuthorURI',
 274              'description' => 'Description',
 275              'name'        => 'Name',
 276              'tags'        => 'Tags',
 277              'theme_uri'   => 'ThemeURI',
 278          );
 279  
 280          foreach ( $rich_field_mappings as $field => $header ) {
 281              if ( rest_is_field_included( "{$field}.raw", $fields ) ) {
 282                  $data[ $field ]['raw'] = $theme->display( $header, false, true );
 283              }
 284  
 285              if ( rest_is_field_included( "{$field}.rendered", $fields ) ) {
 286                  $data[ $field ]['rendered'] = $theme->display( $header );
 287              }
 288          }
 289  
 290          $current_theme = wp_get_theme();
 291          if ( rest_is_field_included( 'status', $fields ) ) {
 292              $data['status'] = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
 293          }
 294  
 295          if ( rest_is_field_included( 'theme_supports', $fields ) && $this->is_same_theme( $theme, $current_theme ) ) {
 296              foreach ( get_registered_theme_features() as $feature => $config ) {
 297                  if ( ! is_array( $config['show_in_rest'] ) ) {
 298                      continue;
 299                  }
 300  
 301                  $name = $config['show_in_rest']['name'];
 302  
 303                  if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) {
 304                      continue;
 305                  }
 306  
 307                  if ( ! current_theme_supports( $feature ) ) {
 308                      $data['theme_supports'][ $name ] = $config['show_in_rest']['schema']['default'];
 309                      continue;
 310                  }
 311  
 312                  $support = get_theme_support( $feature );
 313  
 314                  if ( isset( $config['show_in_rest']['prepare_callback'] ) ) {
 315                      $prepare = $config['show_in_rest']['prepare_callback'];
 316                  } else {
 317                      $prepare = array( $this, 'prepare_theme_support' );
 318                  }
 319  
 320                  $prepared = $prepare( $support, $config, $feature, $request );
 321  
 322                  if ( is_wp_error( $prepared ) ) {
 323                      continue;
 324                  }
 325  
 326                  $data['theme_supports'][ $name ] = $prepared;
 327              }
 328          }
 329  
 330          if ( rest_is_field_included( 'is_block_theme', $fields ) ) {
 331              $data['is_block_theme'] = $theme->is_block_theme();
 332          }
 333  
 334          $data = $this->add_additional_fields_to_object( $data, $request );
 335  
 336          // Wrap the data in a response object.
 337          $response = rest_ensure_response( $data );
 338  
 339          if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
 340              $response->add_links( $this->prepare_links( $theme ) );
 341          }
 342  
 343          /**
 344           * Filters theme data returned from the REST API.
 345           *
 346           * @since 5.0.0
 347           *
 348           * @param WP_REST_Response $response The response object.
 349           * @param WP_Theme         $theme    Theme object used to create response.
 350           * @param WP_REST_Request  $request  Request object.
 351           */
 352          return apply_filters( 'rest_prepare_theme', $response, $theme, $request );
 353      }
 354  
 355      /**
 356       * Prepares links for the request.
 357       *
 358       * @since 5.7.0
 359       *
 360       * @param WP_Theme $theme Theme data.
 361       * @return array Links for the given block type.
 362       */
 363  	protected function prepare_links( $theme ) {
 364          $links = array(
 365              'self'       => array(
 366                  'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $theme->get_stylesheet() ) ),
 367              ),
 368              'collection' => array(
 369                  'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
 370              ),
 371          );
 372  
 373          if ( $this->is_same_theme( $theme, wp_get_theme() ) ) {
 374              // This creates a record for the active theme if not existent.
 375              $id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
 376          } else {
 377              $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme );
 378              $id       = isset( $user_cpt['ID'] ) ? $user_cpt['ID'] : null;
 379          }
 380  
 381          if ( $id ) {
 382              $links['https://api.w.org/user-global-styles'] = array(
 383                  'href' => rest_url( 'wp/v2/global-styles/' . $id ),
 384              );
 385          }
 386  
 387          return $links;
 388      }
 389  
 390      /**
 391       * Helper function to compare two themes.
 392       *
 393       * @since 5.7.0
 394       *
 395       * @param WP_Theme $theme_a First theme to compare.
 396       * @param WP_Theme $theme_b Second theme to compare.
 397       * @return bool
 398       */
 399  	protected function is_same_theme( $theme_a, $theme_b ) {
 400          return $theme_a->get_stylesheet() === $theme_b->get_stylesheet();
 401      }
 402  
 403      /**
 404       * Prepares the theme support value for inclusion in the REST API response.
 405       *
 406       * @since 5.5.0
 407       *
 408       * @param mixed           $support The raw value from get_theme_support().
 409       * @param array           $args    The feature's registration args.
 410       * @param string          $feature The feature name.
 411       * @param WP_REST_Request $request The request object.
 412       * @return mixed The prepared support value.
 413       */
 414  	protected function prepare_theme_support( $support, $args, $feature, $request ) {
 415          $schema = $args['show_in_rest']['schema'];
 416  
 417          if ( 'boolean' === $schema['type'] ) {
 418              return true;
 419          }
 420  
 421          if ( is_array( $support ) && ! $args['variadic'] ) {
 422              $support = $support[0];
 423          }
 424  
 425          return rest_sanitize_value_from_schema( $support, $schema );
 426      }
 427  
 428      /**
 429       * Retrieves the theme's schema, conforming to JSON Schema.
 430       *
 431       * @since 5.0.0
 432       *
 433       * @return array Item schema data.
 434       */
 435  	public function get_item_schema() {
 436          if ( $this->schema ) {
 437              return $this->add_additional_fields_schema( $this->schema );
 438          }
 439  
 440          $schema = array(
 441              '$schema'    => 'http://json-schema.org/draft-04/schema#',
 442              'title'      => 'theme',
 443              'type'       => 'object',
 444              'properties' => array(
 445                  'stylesheet'     => array(
 446                      'description' => __( 'The theme\'s stylesheet. This uniquely identifies the theme.' ),
 447                      'type'        => 'string',
 448                      'readonly'    => true,
 449                  ),
 450                  'template'       => array(
 451                      '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.' ),
 452                      'type'        => 'string',
 453                      'readonly'    => true,
 454                  ),
 455                  'author'         => array(
 456                      'description' => __( 'The theme author.' ),
 457                      'type'        => 'object',
 458                      'readonly'    => true,
 459                      'properties'  => array(
 460                          'raw'      => array(
 461                              'description' => __( 'The theme author\'s name, as found in the theme header.' ),
 462                              'type'        => 'string',
 463                          ),
 464                          'rendered' => array(
 465                              'description' => __( 'HTML for the theme author, transformed for display.' ),
 466                              'type'        => 'string',
 467                          ),
 468                      ),
 469                  ),
 470                  'author_uri'     => array(
 471                      'description' => __( 'The website of the theme author.' ),
 472                      'type'        => 'object',
 473                      'readonly'    => true,
 474                      'properties'  => array(
 475                          'raw'      => array(
 476                              'description' => __( 'The website of the theme author, as found in the theme header.' ),
 477                              'type'        => 'string',
 478                              'format'      => 'uri',
 479                          ),
 480                          'rendered' => array(
 481                              'description' => __( 'The website of the theme author, transformed for display.' ),
 482                              'type'        => 'string',
 483                              'format'      => 'uri',
 484                          ),
 485                      ),
 486                  ),
 487                  'description'    => array(
 488                      'description' => __( 'A description of the theme.' ),
 489                      'type'        => 'object',
 490                      'readonly'    => true,
 491                      'properties'  => array(
 492                          'raw'      => array(
 493                              'description' => __( 'The theme description, as found in the theme header.' ),
 494                              'type'        => 'string',
 495                          ),
 496                          'rendered' => array(
 497                              'description' => __( 'The theme description, transformed for display.' ),
 498                              'type'        => 'string',
 499                          ),
 500                      ),
 501                  ),
 502                  'is_block_theme' => array(
 503                      'description' => __( 'Whether the theme is a block-based theme.' ),
 504                      'type'        => 'boolean',
 505                      'readonly'    => true,
 506                  ),
 507                  'name'           => array(
 508                      'description' => __( 'The name of the theme.' ),
 509                      'type'        => 'object',
 510                      'readonly'    => true,
 511                      'properties'  => array(
 512                          'raw'      => array(
 513                              'description' => __( 'The theme name, as found in the theme header.' ),
 514                              'type'        => 'string',
 515                          ),
 516                          'rendered' => array(
 517                              'description' => __( 'The theme name, transformed for display.' ),
 518                              'type'        => 'string',
 519                          ),
 520                      ),
 521                  ),
 522                  'requires_php'   => array(
 523                      'description' => __( 'The minimum PHP version required for the theme to work.' ),
 524                      'type'        => 'string',
 525                      'readonly'    => true,
 526                  ),
 527                  'requires_wp'    => array(
 528                      'description' => __( 'The minimum WordPress version required for the theme to work.' ),
 529                      'type'        => 'string',
 530                      'readonly'    => true,
 531                  ),
 532                  'screenshot'     => array(
 533                      'description' => __( 'The theme\'s screenshot URL.' ),
 534                      'type'        => 'string',
 535                      'format'      => 'uri',
 536                      'readonly'    => true,
 537                  ),
 538                  'tags'           => array(
 539                      'description' => __( 'Tags indicating styles and features of the theme.' ),
 540                      'type'        => 'object',
 541                      'readonly'    => true,
 542                      'properties'  => array(
 543                          'raw'      => array(
 544                              'description' => __( 'The theme tags, as found in the theme header.' ),
 545                              'type'        => 'array',
 546                              'items'       => array(
 547                                  'type' => 'string',
 548                              ),
 549                          ),
 550                          'rendered' => array(
 551                              'description' => __( 'The theme tags, transformed for display.' ),
 552                              'type'        => 'string',
 553                          ),
 554                      ),
 555                  ),
 556                  'textdomain'     => array(
 557                      'description' => __( 'The theme\'s text domain.' ),
 558                      'type'        => 'string',
 559                      'readonly'    => true,
 560                  ),
 561                  'theme_supports' => array(
 562                      'description' => __( 'Features supported by this theme.' ),
 563                      'type'        => 'object',
 564                      'readonly'    => true,
 565                      'properties'  => array(),
 566                  ),
 567                  'theme_uri'      => array(
 568                      'description' => __( 'The URI of the theme\'s webpage.' ),
 569                      'type'        => 'object',
 570                      'readonly'    => true,
 571                      'properties'  => array(
 572                          'raw'      => array(
 573                              'description' => __( 'The URI of the theme\'s webpage, as found in the theme header.' ),
 574                              'type'        => 'string',
 575                              'format'      => 'uri',
 576                          ),
 577                          'rendered' => array(
 578                              'description' => __( 'The URI of the theme\'s webpage, transformed for display.' ),
 579                              'type'        => 'string',
 580                              'format'      => 'uri',
 581                          ),
 582                      ),
 583                  ),
 584                  'version'        => array(
 585                      'description' => __( 'The theme\'s current version.' ),
 586                      'type'        => 'string',
 587                      'readonly'    => true,
 588                  ),
 589                  'status'         => array(
 590                      'description' => __( 'A named status for the theme.' ),
 591                      'type'        => 'string',
 592                      'enum'        => array( 'inactive', 'active' ),
 593                  ),
 594              ),
 595          );
 596  
 597          foreach ( get_registered_theme_features() as $feature => $config ) {
 598              if ( ! is_array( $config['show_in_rest'] ) ) {
 599                  continue;
 600              }
 601  
 602              $name = $config['show_in_rest']['name'];
 603  
 604              $schema['properties']['theme_supports']['properties'][ $name ] = $config['show_in_rest']['schema'];
 605          }
 606  
 607          $this->schema = $schema;
 608  
 609          return $this->add_additional_fields_schema( $this->schema );
 610      }
 611  
 612      /**
 613       * Retrieves the search params for the themes collection.
 614       *
 615       * @since 5.0.0
 616       *
 617       * @return array Collection parameters.
 618       */
 619  	public function get_collection_params() {
 620          $query_params = array(
 621              'status' => array(
 622                  'description' => __( 'Limit result set to themes assigned one or more statuses.' ),
 623                  'type'        => 'array',
 624                  'items'       => array(
 625                      'enum' => array( 'active', 'inactive' ),
 626                      'type' => 'string',
 627                  ),
 628              ),
 629          );
 630  
 631          /**
 632           * Filters REST API collection parameters for the themes controller.
 633           *
 634           * @since 5.0.0
 635           *
 636           * @param array $query_params JSON Schema-formatted collection parameters.
 637           */
 638          return apply_filters( 'rest_themes_collection_params', $query_params );
 639      }
 640  
 641      /**
 642       * Sanitizes and validates the list of theme status.
 643       *
 644       * @since 5.0.0
 645       * @deprecated 5.7.0
 646       *
 647       * @param string|array    $statuses  One or more theme statuses.
 648       * @param WP_REST_Request $request   Full details about the request.
 649       * @param string          $parameter Additional parameter to pass to validation.
 650       * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
 651       */
 652  	public function sanitize_theme_status( $statuses, $request, $parameter ) {
 653          _deprecated_function( __METHOD__, '5.7.0' );
 654  
 655          $statuses = wp_parse_slug_list( $statuses );
 656  
 657          foreach ( $statuses as $status ) {
 658              $result = rest_validate_request_arg( $status, $request, $parameter );
 659  
 660              if ( is_wp_error( $result ) ) {
 661                  return $result;
 662              }
 663          }
 664  
 665          return $statuses;
 666      }
 667  }


Generated : Tue May 7 08:20:01 2024 Cross-referenced by PHPXref