[ Index ]

PHP Cross Reference of WordPress

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 bool|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 bool|WP_Error Whether the theme can be read.
 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          $data   = array();
 236          $fields = $this->get_fields_for_response( $request );
 237  
 238          if ( rest_is_field_included( 'stylesheet', $fields ) ) {
 239              $data['stylesheet'] = $theme->get_stylesheet();
 240          }
 241  
 242          if ( rest_is_field_included( 'template', $fields ) ) {
 243              /**
 244               * Use the get_template() method, not the 'Template' header, for finding the template.
 245               * The 'Template' header is only good for what was written in the style.css, while
 246               * get_template() takes into account where WordPress actually located the theme and
 247               * whether it is actually valid.
 248               */
 249              $data['template'] = $theme->get_template();
 250          }
 251  
 252          $plain_field_mappings = array(
 253              'requires_php' => 'RequiresPHP',
 254              'requires_wp'  => 'RequiresWP',
 255              'textdomain'   => 'TextDomain',
 256              'version'      => 'Version',
 257          );
 258  
 259          foreach ( $plain_field_mappings as $field => $header ) {
 260              if ( rest_is_field_included( $field, $fields ) ) {
 261                  $data[ $field ] = $theme->get( $header );
 262              }
 263          }
 264  
 265          if ( rest_is_field_included( 'screenshot', $fields ) ) {
 266              // Using $theme->get_screenshot() with no args to get absolute URL.
 267              $data['screenshot'] = $theme->get_screenshot() ? $theme->get_screenshot() : '';
 268          }
 269  
 270          $rich_field_mappings = array(
 271              'author'      => 'Author',
 272              'author_uri'  => 'AuthorURI',
 273              'description' => 'Description',
 274              'name'        => 'Name',
 275              'tags'        => 'Tags',
 276              'theme_uri'   => 'ThemeURI',
 277          );
 278  
 279          foreach ( $rich_field_mappings as $field => $header ) {
 280              if ( rest_is_field_included( "{$field}.raw", $fields ) ) {
 281                  $data[ $field ]['raw'] = $theme->display( $header, false, true );
 282              }
 283  
 284              if ( rest_is_field_included( "{$field}.rendered", $fields ) ) {
 285                  $data[ $field ]['rendered'] = $theme->display( $header );
 286              }
 287          }
 288  
 289          $current_theme = wp_get_theme();
 290          if ( rest_is_field_included( 'status', $fields ) ) {
 291              $data['status'] = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
 292          }
 293  
 294          if ( rest_is_field_included( 'theme_supports', $fields ) && $this->is_same_theme( $theme, $current_theme ) ) {
 295              foreach ( get_registered_theme_features() as $feature => $config ) {
 296                  if ( ! is_array( $config['show_in_rest'] ) ) {
 297                      continue;
 298                  }
 299  
 300                  $name = $config['show_in_rest']['name'];
 301  
 302                  if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) {
 303                      continue;
 304                  }
 305  
 306                  if ( ! current_theme_supports( $feature ) ) {
 307                      $data['theme_supports'][ $name ] = $config['show_in_rest']['schema']['default'];
 308                      continue;
 309                  }
 310  
 311                  $support = get_theme_support( $feature );
 312  
 313                  if ( isset( $config['show_in_rest']['prepare_callback'] ) ) {
 314                      $prepare = $config['show_in_rest']['prepare_callback'];
 315                  } else {
 316                      $prepare = array( $this, 'prepare_theme_support' );
 317                  }
 318  
 319                  $prepared = $prepare( $support, $config, $feature, $request );
 320  
 321                  if ( is_wp_error( $prepared ) ) {
 322                      continue;
 323                  }
 324  
 325                  $data['theme_supports'][ $name ] = $prepared;
 326              }
 327          }
 328  
 329          $data = $this->add_additional_fields_to_object( $data, $request );
 330  
 331          // Wrap the data in a response object.
 332          $response = rest_ensure_response( $data );
 333  
 334          $response->add_links( $this->prepare_links( $theme ) );
 335  
 336          if ( $theme->get_stylesheet() === wp_get_theme()->get_stylesheet() ) {
 337              // This creates a record for the active theme if not existent.
 338              $id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
 339          } else {
 340              $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme );
 341              $id       = isset( $user_cpt['ID'] ) ? $user_cpt['ID'] : null;
 342          }
 343  
 344          if ( $id ) {
 345              $response->add_link(
 346                  'https://api.w.org/user-global-styles',
 347                  rest_url( 'wp/v2/global-styles/' . $id )
 348              );
 349          }
 350  
 351          /**
 352           * Filters theme data returned from the REST API.
 353           *
 354           * @since 5.0.0
 355           *
 356           * @param WP_REST_Response $response The response object.
 357           * @param WP_Theme         $theme    Theme object used to create response.
 358           * @param WP_REST_Request  $request  Request object.
 359           */
 360          return apply_filters( 'rest_prepare_theme', $response, $theme, $request );
 361      }
 362  
 363      /**
 364       * Prepares links for the request.
 365       *
 366       * @since 5.7.0
 367       *
 368       * @param WP_Theme $theme Theme data.
 369       * @return array Links for the given block type.
 370       */
 371  	protected function prepare_links( $theme ) {
 372          return array(
 373              'self'       => array(
 374                  'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $theme->get_stylesheet() ) ),
 375              ),
 376              'collection' => array(
 377                  'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
 378              ),
 379          );
 380      }
 381  
 382      /**
 383       * Helper function to compare two themes.
 384       *
 385       * @since 5.7.0
 386       *
 387       * @param WP_Theme $theme_a First theme to compare.
 388       * @param WP_Theme $theme_b Second theme to compare.
 389       * @return bool
 390       */
 391  	protected function is_same_theme( $theme_a, $theme_b ) {
 392          return $theme_a->get_stylesheet() === $theme_b->get_stylesheet();
 393      }
 394  
 395      /**
 396       * Prepares the theme support value for inclusion in the REST API response.
 397       *
 398       * @since 5.5.0
 399       *
 400       * @param mixed           $support The raw value from get_theme_support().
 401       * @param array           $args    The feature's registration args.
 402       * @param string          $feature The feature name.
 403       * @param WP_REST_Request $request The request object.
 404       * @return mixed The prepared support value.
 405       */
 406  	protected function prepare_theme_support( $support, $args, $feature, $request ) {
 407          $schema = $args['show_in_rest']['schema'];
 408  
 409          if ( 'boolean' === $schema['type'] ) {
 410              return true;
 411          }
 412  
 413          if ( is_array( $support ) && ! $args['variadic'] ) {
 414              $support = $support[0];
 415          }
 416  
 417          return rest_sanitize_value_from_schema( $support, $schema );
 418      }
 419  
 420      /**
 421       * Retrieves the theme's schema, conforming to JSON Schema.
 422       *
 423       * @since 5.0.0
 424       *
 425       * @return array Item schema data.
 426       */
 427  	public function get_item_schema() {
 428          if ( $this->schema ) {
 429              return $this->add_additional_fields_schema( $this->schema );
 430          }
 431  
 432          $schema = array(
 433              '$schema'    => 'http://json-schema.org/draft-04/schema#',
 434              'title'      => 'theme',
 435              'type'       => 'object',
 436              'properties' => array(
 437                  'stylesheet'     => array(
 438                      'description' => __( 'The theme\'s stylesheet. This uniquely identifies the theme.' ),
 439                      'type'        => 'string',
 440                      'readonly'    => true,
 441                  ),
 442                  'template'       => array(
 443                      '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.' ),
 444                      'type'        => 'string',
 445                      'readonly'    => true,
 446                  ),
 447                  'author'         => array(
 448                      'description' => __( 'The theme author.' ),
 449                      'type'        => 'object',
 450                      'readonly'    => true,
 451                      'properties'  => array(
 452                          'raw'      => array(
 453                              'description' => __( 'The theme author\'s name, as found in the theme header.' ),
 454                              'type'        => 'string',
 455                          ),
 456                          'rendered' => array(
 457                              'description' => __( 'HTML for the theme author, transformed for display.' ),
 458                              'type'        => 'string',
 459                          ),
 460                      ),
 461                  ),
 462                  'author_uri'     => array(
 463                      'description' => __( 'The website of the theme author.' ),
 464                      'type'        => 'object',
 465                      'readonly'    => true,
 466                      'properties'  => array(
 467                          'raw'      => array(
 468                              'description' => __( 'The website of the theme author, as found in the theme header.' ),
 469                              'type'        => 'string',
 470                              'format'      => 'uri',
 471                          ),
 472                          'rendered' => array(
 473                              'description' => __( 'The website of the theme author, transformed for display.' ),
 474                              'type'        => 'string',
 475                              'format'      => 'uri',
 476                          ),
 477                      ),
 478                  ),
 479                  'description'    => array(
 480                      'description' => __( 'A description of the theme.' ),
 481                      'type'        => 'object',
 482                      'readonly'    => true,
 483                      'properties'  => array(
 484                          'raw'      => array(
 485                              'description' => __( 'The theme description, as found in the theme header.' ),
 486                              'type'        => 'string',
 487                          ),
 488                          'rendered' => array(
 489                              'description' => __( 'The theme description, transformed for display.' ),
 490                              'type'        => 'string',
 491                          ),
 492                      ),
 493                  ),
 494                  'name'           => array(
 495                      'description' => __( 'The name of the theme.' ),
 496                      'type'        => 'object',
 497                      'readonly'    => true,
 498                      'properties'  => array(
 499                          'raw'      => array(
 500                              'description' => __( 'The theme name, as found in the theme header.' ),
 501                              'type'        => 'string',
 502                          ),
 503                          'rendered' => array(
 504                              'description' => __( 'The theme name, transformed for display.' ),
 505                              'type'        => 'string',
 506                          ),
 507                      ),
 508                  ),
 509                  'requires_php'   => array(
 510                      'description' => __( 'The minimum PHP version required for the theme to work.' ),
 511                      'type'        => 'string',
 512                      'readonly'    => true,
 513                  ),
 514                  'requires_wp'    => array(
 515                      'description' => __( 'The minimum WordPress version required for the theme to work.' ),
 516                      'type'        => 'string',
 517                      'readonly'    => true,
 518                  ),
 519                  'screenshot'     => array(
 520                      'description' => __( 'The theme\'s screenshot URL.' ),
 521                      'type'        => 'string',
 522                      'format'      => 'uri',
 523                      'readonly'    => true,
 524                  ),
 525                  'tags'           => array(
 526                      'description' => __( 'Tags indicating styles and features of the theme.' ),
 527                      'type'        => 'object',
 528                      'readonly'    => true,
 529                      'properties'  => array(
 530                          'raw'      => array(
 531                              'description' => __( 'The theme tags, as found in the theme header.' ),
 532                              'type'        => 'array',
 533                              'items'       => array(
 534                                  'type' => 'string',
 535                              ),
 536                          ),
 537                          'rendered' => array(
 538                              'description' => __( 'The theme tags, transformed for display.' ),
 539                              'type'        => 'string',
 540                          ),
 541                      ),
 542                  ),
 543                  'textdomain'     => array(
 544                      'description' => __( 'The theme\'s text domain.' ),
 545                      'type'        => 'string',
 546                      'readonly'    => true,
 547                  ),
 548                  'theme_supports' => array(
 549                      'description' => __( 'Features supported by this theme.' ),
 550                      'type'        => 'object',
 551                      'readonly'    => true,
 552                      'properties'  => array(),
 553                  ),
 554                  'theme_uri'      => array(
 555                      'description' => __( 'The URI of the theme\'s webpage.' ),
 556                      'type'        => 'object',
 557                      'readonly'    => true,
 558                      'properties'  => array(
 559                          'raw'      => array(
 560                              'description' => __( 'The URI of the theme\'s webpage, as found in the theme header.' ),
 561                              'type'        => 'string',
 562                              'format'      => 'uri',
 563                          ),
 564                          'rendered' => array(
 565                              'description' => __( 'The URI of the theme\'s webpage, transformed for display.' ),
 566                              'type'        => 'string',
 567                              'format'      => 'uri',
 568                          ),
 569                      ),
 570                  ),
 571                  'version'        => array(
 572                      'description' => __( 'The theme\'s current version.' ),
 573                      'type'        => 'string',
 574                      'readonly'    => true,
 575                  ),
 576                  'status'         => array(
 577                      'description' => __( 'A named status for the theme.' ),
 578                      'type'        => 'string',
 579                      'enum'        => array( 'inactive', 'active' ),
 580                  ),
 581              ),
 582          );
 583  
 584          foreach ( get_registered_theme_features() as $feature => $config ) {
 585              if ( ! is_array( $config['show_in_rest'] ) ) {
 586                  continue;
 587              }
 588  
 589              $name = $config['show_in_rest']['name'];
 590  
 591              $schema['properties']['theme_supports']['properties'][ $name ] = $config['show_in_rest']['schema'];
 592          }
 593  
 594          $this->schema = $schema;
 595  
 596          return $this->add_additional_fields_schema( $this->schema );
 597      }
 598  
 599      /**
 600       * Retrieves the search params for the themes collection.
 601       *
 602       * @since 5.0.0
 603       *
 604       * @return array Collection parameters.
 605       */
 606  	public function get_collection_params() {
 607          $query_params = array(
 608              'status' => array(
 609                  'description' => __( 'Limit result set to themes assigned one or more statuses.' ),
 610                  'type'        => 'array',
 611                  'items'       => array(
 612                      'enum' => array( 'active', 'inactive' ),
 613                      'type' => 'string',
 614                  ),
 615              ),
 616          );
 617  
 618          /**
 619           * Filters REST API collection parameters for the themes controller.
 620           *
 621           * @since 5.0.0
 622           *
 623           * @param array $query_params JSON Schema-formatted collection parameters.
 624           */
 625          return apply_filters( 'rest_themes_collection_params', $query_params );
 626      }
 627  
 628      /**
 629       * Sanitizes and validates the list of theme status.
 630       *
 631       * @since 5.0.0
 632       * @deprecated 5.7.0
 633       *
 634       * @param string|array    $statuses  One or more theme statuses.
 635       * @param WP_REST_Request $request   Full details about the request.
 636       * @param string          $parameter Additional parameter to pass to validation.
 637       * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
 638       */
 639  	public function sanitize_theme_status( $statuses, $request, $parameter ) {
 640          _deprecated_function( __METHOD__, '5.7.0' );
 641  
 642          $statuses = wp_parse_slug_list( $statuses );
 643  
 644          foreach ( $statuses as $status ) {
 645              $result = rest_validate_request_arg( $status, $request, $parameter );
 646  
 647              if ( is_wp_error( $result ) ) {
 648                  return $result;
 649              }
 650          }
 651  
 652          return $statuses;
 653      }
 654  }


Generated: Wed Apr 24 01:00:03 2024 Cross-referenced by PHPXref 0.7.1