[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

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

   1  <?php
   2  /**
   3   * REST API: WP_REST_Global_Styles_Controller class
   4   *
   5   * @package    WordPress
   6   * @subpackage REST_API
   7   * @since 5.9.0
   8   */
   9  
  10  /**
  11   * Base Global Styles REST API Controller.
  12   */
  13  class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
  14      /**
  15       * Whether the controller supports batching.
  16       *
  17       * @since 6.6.0
  18       * @var array
  19       */
  20      protected $allow_batch = array( 'v1' => false );
  21  
  22      /**
  23       * Constructor.
  24       *
  25       * @since 6.6.0
  26       *
  27       * @param string $post_type Post type.
  28       */
  29  	public function __construct( $post_type = 'wp_global_styles' ) {
  30          parent::__construct( $post_type );
  31      }
  32  
  33      /**
  34       * Registers the controllers routes.
  35       *
  36       * @since 5.9.0
  37       */
  38  	public function register_routes() {
  39          register_rest_route(
  40              $this->namespace,
  41              '/' . $this->rest_base . '/themes/(?P<stylesheet>[\/\s%\w\.\(\)\[\]\@_\-]+)/variations',
  42              array(
  43                  array(
  44                      'methods'             => WP_REST_Server::READABLE,
  45                      'callback'            => array( $this, 'get_theme_items' ),
  46                      'permission_callback' => array( $this, 'get_theme_items_permissions_check' ),
  47                      'args'                => array(
  48                          'stylesheet' => array(
  49                              'description' => __( 'The theme identifier' ),
  50                              'type'        => 'string',
  51                          ),
  52                      ),
  53                      'allow_batch'         => $this->allow_batch,
  54                  ),
  55              )
  56          );
  57  
  58          // List themes global styles.
  59          register_rest_route(
  60              $this->namespace,
  61              // The route.
  62              sprintf(
  63                  '/%s/themes/(?P<stylesheet>%s)',
  64                  $this->rest_base,
  65                  /*
  66                   * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
  67                   * Excludes invalid directory name characters: `/:<>*?"|`.
  68                   */
  69                  '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?'
  70              ),
  71              array(
  72                  array(
  73                      'methods'             => WP_REST_Server::READABLE,
  74                      'callback'            => array( $this, 'get_theme_item' ),
  75                      'permission_callback' => array( $this, 'get_theme_item_permissions_check' ),
  76                      'args'                => array(
  77                          'stylesheet' => array(
  78                              'description'       => __( 'The theme identifier' ),
  79                              'type'              => 'string',
  80                              'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
  81                          ),
  82                      ),
  83                      'allow_batch'         => $this->allow_batch,
  84                  ),
  85              )
  86          );
  87  
  88          // Lists/updates a single global style variation based on the given id.
  89          register_rest_route(
  90              $this->namespace,
  91              '/' . $this->rest_base . '/(?P<id>[\/\d+]+)',
  92              array(
  93                  array(
  94                      'methods'             => WP_REST_Server::READABLE,
  95                      'callback'            => array( $this, 'get_item' ),
  96                      'permission_callback' => array( $this, 'get_item_permissions_check' ),
  97                      'args'                => array(
  98                          'id' => array(
  99                              'description' => __( 'ID of global styles config.' ),
 100                              'type'        => 'integer',
 101                          ),
 102                      ),
 103                  ),
 104                  array(
 105                      'methods'             => WP_REST_Server::EDITABLE,
 106                      'callback'            => array( $this, 'update_item' ),
 107                      'permission_callback' => array( $this, 'update_item_permissions_check' ),
 108                      'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
 109                  ),
 110                  'schema'      => array( $this, 'get_public_item_schema' ),
 111                  'allow_batch' => $this->allow_batch,
 112              )
 113          );
 114      }
 115  
 116      /**
 117       * Sanitize the global styles stylesheet to decode endpoint.
 118       * For example, `wp/v2/global-styles/twentytwentytwo%200.4.0`
 119       * would be decoded to `twentytwentytwo 0.4.0`.
 120       *
 121       * @since 5.9.0
 122       *
 123       * @param string $stylesheet Global styles stylesheet.
 124       * @return string Sanitized global styles stylesheet.
 125       */
 126  	public function _sanitize_global_styles_callback( $stylesheet ) {
 127          return urldecode( $stylesheet );
 128      }
 129  
 130      /**
 131       * Get the post, if the ID is valid.
 132       *
 133       * @since 5.9.0
 134       *
 135       * @param int $id Supplied ID.
 136       * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
 137       */
 138  	protected function get_post( $id ) {
 139          $error = new WP_Error(
 140              'rest_global_styles_not_found',
 141              __( 'No global styles config exists with that ID.' ),
 142              array( 'status' => 404 )
 143          );
 144  
 145          $id = (int) $id;
 146          if ( $id <= 0 ) {
 147              return $error;
 148          }
 149  
 150          $post = get_post( $id );
 151          if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
 152              return $error;
 153          }
 154  
 155          return $post;
 156      }
 157  
 158      /**
 159       * Checks if a given request has access to read a single global style.
 160       *
 161       * @since 5.9.0
 162       *
 163       * @param WP_REST_Request $request Full details about the request.
 164       * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
 165       */
 166  	public function get_item_permissions_check( $request ) {
 167          $post = $this->get_post( $request['id'] );
 168          if ( is_wp_error( $post ) ) {
 169              return $post;
 170          }
 171  
 172          if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
 173              return new WP_Error(
 174                  'rest_forbidden_context',
 175                  __( 'Sorry, you are not allowed to edit this global style.' ),
 176                  array( 'status' => rest_authorization_required_code() )
 177              );
 178          }
 179  
 180          if ( ! $this->check_read_permission( $post ) ) {
 181              return new WP_Error(
 182                  'rest_cannot_view',
 183                  __( 'Sorry, you are not allowed to view this global style.' ),
 184                  array( 'status' => rest_authorization_required_code() )
 185              );
 186          }
 187  
 188          return true;
 189      }
 190  
 191      /**
 192       * Checks if a global style can be read.
 193       *
 194       * @since 5.9.0
 195       *
 196       * @param WP_Post $post Post object.
 197       * @return bool Whether the post can be read.
 198       */
 199  	public function check_read_permission( $post ) {
 200          return current_user_can( 'read_post', $post->ID );
 201      }
 202  
 203      /**
 204       * Checks if a given request has access to write a single global styles config.
 205       *
 206       * @since 5.9.0
 207       *
 208       * @param WP_REST_Request $request Full details about the request.
 209       * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
 210       */
 211  	public function update_item_permissions_check( $request ) {
 212          $post = $this->get_post( $request['id'] );
 213          if ( is_wp_error( $post ) ) {
 214              return $post;
 215          }
 216  
 217          if ( $post && ! $this->check_update_permission( $post ) ) {
 218              return new WP_Error(
 219                  'rest_cannot_edit',
 220                  __( 'Sorry, you are not allowed to edit this global style.' ),
 221                  array( 'status' => rest_authorization_required_code() )
 222              );
 223          }
 224  
 225          return true;
 226      }
 227  
 228      /**
 229       * Prepares a single global styles config for update.
 230       *
 231       * @since 5.9.0
 232       * @since 6.2.0 Added validation of styles.css property.
 233       * @since 6.6.0 Added registration of block style variations from theme.json sources (theme.json, user theme.json, partials).
 234       *
 235       * @param WP_REST_Request $request Request object.
 236       * @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid.
 237       */
 238  	protected function prepare_item_for_database( $request ) {
 239          $changes     = new stdClass();
 240          $changes->ID = $request['id'];
 241  
 242          $post            = get_post( $request['id'] );
 243          $existing_config = array();
 244          if ( $post ) {
 245              $existing_config     = json_decode( $post->post_content, true );
 246              $json_decoding_error = json_last_error();
 247              if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) ||
 248                  ! $existing_config['isGlobalStylesUserThemeJSON'] ) {
 249                  $existing_config = array();
 250              }
 251          }
 252  
 253          if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
 254              $config = array();
 255              if ( isset( $request['styles'] ) ) {
 256                  if ( isset( $request['styles']['css'] ) ) {
 257                      $css_validation_result = $this->validate_custom_css( $request['styles']['css'] );
 258                      if ( is_wp_error( $css_validation_result ) ) {
 259                          return $css_validation_result;
 260                      }
 261                  }
 262                  $config['styles'] = $request['styles'];
 263              } elseif ( isset( $existing_config['styles'] ) ) {
 264                  $config['styles'] = $existing_config['styles'];
 265              }
 266  
 267              // Register theme-defined variations e.g. from block style variation partials under `/styles`.
 268              $variations = WP_Theme_JSON_Resolver::get_style_variations( 'block' );
 269              wp_register_block_style_variations_from_theme_json_partials( $variations );
 270  
 271              if ( isset( $request['settings'] ) ) {
 272                  $config['settings'] = $request['settings'];
 273              } elseif ( isset( $existing_config['settings'] ) ) {
 274                  $config['settings'] = $existing_config['settings'];
 275              }
 276              $config['isGlobalStylesUserThemeJSON'] = true;
 277              $config['version']                     = WP_Theme_JSON::LATEST_SCHEMA;
 278              /**
 279               * JSON encode the data stored in post content.
 280               * Escape characters that are likely to be mangled by HTML filters: "<>&".
 281               *
 282               * This data is later re-encoded by {@see wp_filter_global_styles_post()}.
 283               * The escaping is also applied here as a precaution.
 284               */
 285              $changes->post_content = wp_json_encode( $config, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
 286          }
 287  
 288          // Post title.
 289          if ( isset( $request['title'] ) ) {
 290              if ( is_string( $request['title'] ) ) {
 291                  $changes->post_title = $request['title'];
 292              } elseif ( ! empty( $request['title']['raw'] ) ) {
 293                  $changes->post_title = $request['title']['raw'];
 294              }
 295          }
 296  
 297          return $changes;
 298      }
 299  
 300      /**
 301       * Prepare a global styles config output for response.
 302       *
 303       * @since 5.9.0
 304       * @since 6.6.0 Added custom relative theme file URIs to `_links`.
 305       *
 306       * @param WP_Post         $post    Global Styles post object.
 307       * @param WP_REST_Request $request Request object.
 308       * @return WP_REST_Response Response object.
 309       */
 310  	public function prepare_item_for_response( $post, $request ) {
 311          $raw_config                       = json_decode( $post->post_content, true );
 312          $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
 313          $config                           = array();
 314          $theme_json                       = null;
 315          if ( $is_global_styles_user_theme_json ) {
 316              $theme_json = new WP_Theme_JSON( $raw_config, 'custom' );
 317              $config     = $theme_json->get_raw_data();
 318          }
 319  
 320          // Base fields for every post.
 321          $fields = $this->get_fields_for_response( $request );
 322          $data   = array();
 323  
 324          if ( rest_is_field_included( 'id', $fields ) ) {
 325              $data['id'] = $post->ID;
 326          }
 327  
 328          if ( rest_is_field_included( 'title', $fields ) ) {
 329              $data['title'] = array();
 330          }
 331          if ( rest_is_field_included( 'title.raw', $fields ) ) {
 332              $data['title']['raw'] = $post->post_title;
 333          }
 334          if ( rest_is_field_included( 'title.rendered', $fields ) ) {
 335              add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
 336              add_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
 337  
 338              $data['title']['rendered'] = get_the_title( $post->ID );
 339  
 340              remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
 341              remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
 342          }
 343  
 344          if ( rest_is_field_included( 'settings', $fields ) ) {
 345              $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
 346          }
 347  
 348          if ( rest_is_field_included( 'styles', $fields ) ) {
 349              $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
 350          }
 351  
 352          $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
 353          $data    = $this->add_additional_fields_to_object( $data, $request );
 354          $data    = $this->filter_response_by_context( $data, $context );
 355  
 356          // Wrap the data in a response object.
 357          $response = rest_ensure_response( $data );
 358  
 359          if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
 360              $links = $this->prepare_links( $post->ID );
 361  
 362              // Only return resolved URIs for get requests to user theme JSON.
 363              if ( $theme_json ) {
 364                  $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json );
 365                  if ( ! empty( $resolved_theme_uris ) ) {
 366                      $links['https://api.w.org/theme-file'] = $resolved_theme_uris;
 367                  }
 368              }
 369  
 370              $response->add_links( $links );
 371              if ( ! empty( $links['self']['href'] ) ) {
 372                  $actions = $this->get_available_actions( $post, $request );
 373                  $self    = $links['self']['href'];
 374                  foreach ( $actions as $rel ) {
 375                      $response->add_link( $rel, $self );
 376                  }
 377              }
 378          }
 379  
 380          return $response;
 381      }
 382  
 383      /**
 384       * Prepares links for the request.
 385       *
 386       * @since 5.9.0
 387       * @since 6.3.0 Adds revisions count and rest URL href to version-history.
 388       *
 389       * @param integer $id ID.
 390       * @return array Links for the given post.
 391       */
 392  	protected function prepare_links( $id ) {
 393          $base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
 394  
 395          $links = array(
 396              'self'  => array(
 397                  'href' => rest_url( trailingslashit( $base ) . $id ),
 398              ),
 399              'about' => array(
 400                  'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
 401              ),
 402          );
 403  
 404          if ( post_type_supports( $this->post_type, 'revisions' ) ) {
 405              $revisions                = wp_get_latest_revision_id_and_total_count( $id );
 406              $revisions_count          = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
 407              $revisions_base           = sprintf( '/%s/%d/revisions', $base, $id );
 408              $links['version-history'] = array(
 409                  'href'  => rest_url( $revisions_base ),
 410                  'count' => $revisions_count,
 411              );
 412          }
 413  
 414          return $links;
 415      }
 416  
 417      /**
 418       * Get the link relations available for the post and current user.
 419       *
 420       * @since 5.9.0
 421       * @since 6.2.0 Added 'edit-css' action.
 422       * @since 6.6.0 Added $post and $request parameters.
 423       *
 424       * @param WP_Post         $post    Post object.
 425       * @param WP_REST_Request $request Request object.
 426       * @return array List of link relations.
 427       */
 428  	protected function get_available_actions( $post, $request ) {
 429          $rels = array();
 430  
 431          $post_type = get_post_type_object( $post->post_type );
 432          if ( current_user_can( $post_type->cap->publish_posts ) ) {
 433              $rels[] = 'https://api.w.org/action-publish';
 434          }
 435  
 436          if ( current_user_can( 'edit_css' ) ) {
 437              $rels[] = 'https://api.w.org/action-edit-css';
 438          }
 439  
 440          return $rels;
 441      }
 442  
 443      /**
 444       * Retrieves the query params for the global styles collection.
 445       *
 446       * @since 5.9.0
 447       *
 448       * @return array Collection parameters.
 449       */
 450  	public function get_collection_params() {
 451          return array();
 452      }
 453  
 454      /**
 455       * Retrieves the global styles type' schema, conforming to JSON Schema.
 456       *
 457       * @since 5.9.0
 458       *
 459       * @return array Item schema data.
 460       */
 461  	public function get_item_schema() {
 462          if ( $this->schema ) {
 463              return $this->add_additional_fields_schema( $this->schema );
 464          }
 465  
 466          $schema = array(
 467              '$schema'    => 'http://json-schema.org/draft-04/schema#',
 468              'title'      => $this->post_type,
 469              'type'       => 'object',
 470              'properties' => array(
 471                  'id'       => array(
 472                      'description' => __( 'ID of global styles config.' ),
 473                      'type'        => 'integer',
 474                      'context'     => array( 'embed', 'view', 'edit' ),
 475                      'readonly'    => true,
 476                  ),
 477                  'styles'   => array(
 478                      'description' => __( 'Global styles.' ),
 479                      'type'        => array( 'object' ),
 480                      'context'     => array( 'view', 'edit' ),
 481                  ),
 482                  'settings' => array(
 483                      'description' => __( 'Global settings.' ),
 484                      'type'        => array( 'object' ),
 485                      'context'     => array( 'view', 'edit' ),
 486                  ),
 487                  'title'    => array(
 488                      'description' => __( 'Title of the global styles variation.' ),
 489                      'type'        => array( 'object', 'string' ),
 490                      'default'     => '',
 491                      'context'     => array( 'embed', 'view', 'edit' ),
 492                      'properties'  => array(
 493                          'raw'      => array(
 494                              'description' => __( 'Title for the global styles variation, as it exists in the database.' ),
 495                              'type'        => 'string',
 496                              'context'     => array( 'view', 'edit', 'embed' ),
 497                          ),
 498                          'rendered' => array(
 499                              'description' => __( 'HTML title for the post, transformed for display.' ),
 500                              'type'        => 'string',
 501                              'context'     => array( 'view', 'edit', 'embed' ),
 502                              'readonly'    => true,
 503                          ),
 504                      ),
 505                  ),
 506              ),
 507          );
 508  
 509          $this->schema = $schema;
 510  
 511          return $this->add_additional_fields_schema( $this->schema );
 512      }
 513  
 514      /**
 515       * Checks if a given request has access to read a single theme global styles config.
 516       *
 517       * @since 5.9.0
 518       * @since 6.7.0 Allow users with edit post capabilities to view theme global styles.
 519       *
 520       * @param WP_REST_Request $request Full details about the request.
 521       * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
 522       */
 523  	public function get_theme_item_permissions_check( $request ) {
 524          /*
 525           * Verify if the current user has edit_posts capability.
 526           * This capability is required to view global styles.
 527           */
 528          if ( current_user_can( 'edit_posts' ) ) {
 529              return true;
 530          }
 531  
 532          foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
 533              if ( current_user_can( $post_type->cap->edit_posts ) ) {
 534                  return true;
 535              }
 536          }
 537  
 538          /*
 539           * Verify if the current user has edit_theme_options capability.
 540           */
 541          if ( current_user_can( 'edit_theme_options' ) ) {
 542              return true;
 543          }
 544  
 545          return new WP_Error(
 546              'rest_cannot_read_global_styles',
 547              __( 'Sorry, you are not allowed to access the global styles on this site.' ),
 548              array(
 549                  'status' => rest_authorization_required_code(),
 550              )
 551          );
 552      }
 553  
 554      /**
 555       * Returns the given theme global styles config.
 556       *
 557       * @since 5.9.0
 558       * @since 6.6.0 Added custom relative theme file URIs to `_links`.
 559       *
 560       * @param WP_REST_Request $request The request instance.
 561       * @return WP_REST_Response|WP_Error
 562       */
 563  	public function get_theme_item( $request ) {
 564          if ( get_stylesheet() !== $request['stylesheet'] ) {
 565              // This endpoint only supports the active theme for now.
 566              return new WP_Error(
 567                  'rest_theme_not_found',
 568                  __( 'Theme not found.' ),
 569                  array( 'status' => 404 )
 570              );
 571          }
 572  
 573          $theme  = WP_Theme_JSON_Resolver::get_merged_data( 'theme' );
 574          $fields = $this->get_fields_for_response( $request );
 575          $data   = array();
 576  
 577          if ( rest_is_field_included( 'settings', $fields ) ) {
 578              $data['settings'] = $theme->get_settings();
 579          }
 580  
 581          if ( rest_is_field_included( 'styles', $fields ) ) {
 582              $raw_data       = $theme->get_raw_data();
 583              $data['styles'] = $raw_data['styles'] ?? array();
 584          }
 585  
 586          $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
 587          $data    = $this->add_additional_fields_to_object( $data, $request );
 588          $data    = $this->filter_response_by_context( $data, $context );
 589  
 590          $response = rest_ensure_response( $data );
 591  
 592          if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
 593              $links               = array(
 594                  'self' => array(
 595                      'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
 596                  ),
 597              );
 598              $resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme );
 599              if ( ! empty( $resolved_theme_uris ) ) {
 600                  $links['https://api.w.org/theme-file'] = $resolved_theme_uris;
 601              }
 602              $response->add_links( $links );
 603          }
 604  
 605          return $response;
 606      }
 607  
 608      /**
 609       * Checks if a given request has access to read a single theme global styles config.
 610       *
 611       * @since 6.0.0
 612       * @since 6.7.0 Allow users with edit post capabilities to view theme global styles.
 613       *
 614       * @param WP_REST_Request $request Full details about the request.
 615       * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
 616       */
 617  	public function get_theme_items_permissions_check( $request ) {
 618          return $this->get_theme_item_permissions_check( $request );
 619      }
 620  
 621      /**
 622       * Returns the given theme global styles variations.
 623       *
 624       * @since 6.0.0
 625       * @since 6.2.0 Returns parent theme variations, if they exist.
 626       * @since 6.6.0 Added custom relative theme file URIs to `_links` for each item.
 627       *
 628       * @param WP_REST_Request $request The request instance.
 629       *
 630       * @return WP_REST_Response|WP_Error
 631       */
 632  	public function get_theme_items( $request ) {
 633          if ( get_stylesheet() !== $request['stylesheet'] ) {
 634              // This endpoint only supports the active theme for now.
 635              return new WP_Error(
 636                  'rest_theme_not_found',
 637                  __( 'Theme not found.' ),
 638                  array( 'status' => 404 )
 639              );
 640          }
 641  
 642          $response = array();
 643  
 644          // Register theme-defined variations e.g. from block style variation partials under `/styles`.
 645          $partials = WP_Theme_JSON_Resolver::get_style_variations( 'block' );
 646          wp_register_block_style_variations_from_theme_json_partials( $partials );
 647  
 648          $variations = WP_Theme_JSON_Resolver::get_style_variations();
 649          foreach ( $variations as $variation ) {
 650              $variation_theme_json = new WP_Theme_JSON( $variation );
 651              $resolved_theme_uris  = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $variation_theme_json );
 652              $data                 = rest_ensure_response( $variation );
 653              if ( ! empty( $resolved_theme_uris ) ) {
 654                  $data->add_links(
 655                      array(
 656                          'https://api.w.org/theme-file' => $resolved_theme_uris,
 657                      )
 658                  );
 659              }
 660              $response[] = $this->prepare_response_for_collection( $data );
 661          }
 662  
 663          return rest_ensure_response( $response );
 664      }
 665  
 666      /**
 667       * Validate style.css as valid CSS.
 668       *
 669       * Currently just checks that CSS will not break an HTML STYLE tag.
 670       *
 671       * @since 6.2.0
 672       * @since 6.4.0 Changed method visibility to protected.
 673       * @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element,
 674       *              either through a STYLE end tag or a prefix of one which might become a
 675       *              full end tag when combined with the contents of other styles.
 676       *
 677       * @see WP_Customize_Custom_CSS_Setting::validate()
 678       *
 679       * @param string $css CSS to validate.
 680       * @return true|WP_Error True if the input was validated, otherwise WP_Error.
 681       */
 682  	protected function validate_custom_css( $css ) {
 683          $length = strlen( $css );
 684          for (
 685              $at = strcspn( $css, '<' );
 686              $at < $length;
 687              $at += strcspn( $css, '<', ++$at )
 688          ) {
 689              $remaining_strlen = $length - $at;
 690              /**
 691               * Custom CSS text is expected to render inside an HTML STYLE element.
 692               * A STYLE closing tag must not appear within the CSS text because it
 693               * would close the element prematurely.
 694               *
 695               * The text must also *not* end with a partial closing tag (e.g., `<`,
 696               * `</`, … `</style`) because subsequent styles which are concatenated
 697               * could complete it, forming a valid `</style>` tag.
 698               *
 699               * Example:
 700               *
 701               *     $style_a = 'p { font-weight: bold; </sty';
 702               *     $style_b = 'le> gotcha!';
 703               *     $combined = "{$style_a}{$style_b}";
 704               *
 705               *     $style_a = 'p { font-weight: bold; </style';
 706               *     $style_b = 'p > b { color: red; }';
 707               *     $combined = "{$style_a}\n{$style_b}";
 708               *
 709               * Note how in the second example, both of the style contents are benign
 710               * when analyzed on their own. The first style was likely the result of
 711               * improper truncation, while the second is perfectly sound. It was only
 712               * through concatenation that these two styles combined to form content
 713               * that would have broken out of the containing STYLE element, thus
 714               * corrupting the page and potentially introducing security issues.
 715               *
 716               * @link https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
 717               */
 718              $possible_style_close_tag = 0 === substr_compare(
 719                  $css,
 720                  '</style',
 721                  $at,
 722                  min( 7, $remaining_strlen ),
 723                  true
 724              );
 725              if ( $possible_style_close_tag ) {
 726                  if ( $remaining_strlen < 8 ) {
 727                      return new WP_Error(
 728                          'rest_custom_css_illegal_markup',
 729                          sprintf(
 730                              /* translators: %s is the CSS that was provided. */
 731                              __( 'The CSS must not end in "%s".' ),
 732                              esc_html( substr( $css, $at ) )
 733                          ),
 734                          array( 'status' => 400 )
 735                      );
 736                  }
 737  
 738                  if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
 739                      return new WP_Error(
 740                          'rest_custom_css_illegal_markup',
 741                          sprintf(
 742                              /* translators: %s is the CSS that was provided. */
 743                              __( 'The CSS must not contain "%s".' ),
 744                              esc_html( substr( $css, $at, 8 ) )
 745                          ),
 746                          array( 'status' => 400 )
 747                      );
 748                  }
 749              }
 750          }
 751  
 752          return true;
 753      }
 754  }


Generated : Fri Jun 26 08:20:11 2026 Cross-referenced by PHPXref