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


Generated : Thu Nov 12 08:20:02 2026 Cross-referenced by PHPXref