wp/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
     8  */
     8  */
     9 
     9 
    10 /**
    10 /**
    11  * Base Global Styles REST API Controller.
    11  * Base Global Styles REST API Controller.
    12  */
    12  */
    13 class WP_REST_Global_Styles_Controller extends WP_REST_Controller {
    13 class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
    14 
    14 	/**
    15 	/**
    15 	 * Whether the controller supports batching.
    16 	 * Post type.
    16 	 *
    17 	 *
    17 	 * @since 6.6.0
    18 	 * @since 5.9.0
    18 	 * @var array
    19 	 * @var string
    19 	 */
    20 	 */
    20 	protected $allow_batch = array( 'v1' => false );
    21 	protected $post_type;
       
    22 
    21 
    23 	/**
    22 	/**
    24 	 * Constructor.
    23 	 * Constructor.
    25 	 * @since 5.9.0
    24 	 *
    26 	 */
    25 	 * @since 6.6.0
    27 	public function __construct() {
    26 	 *
    28 		$this->namespace = 'wp/v2';
    27 	 * @param string $post_type Post type.
    29 		$this->rest_base = 'global-styles';
    28 	 */
    30 		$this->post_type = 'wp_global_styles';
    29 	public function __construct( $post_type = 'wp_global_styles' ) {
       
    30 		parent::__construct( $post_type );
    31 	}
    31 	}
    32 
    32 
    33 	/**
    33 	/**
    34 	 * Registers the controllers routes.
    34 	 * Registers the controllers routes.
    35 	 *
    35 	 *
    36 	 * @since 5.9.0
    36 	 * @since 5.9.0
    37 	 *
       
    38 	 * @return void
       
    39 	 */
    37 	 */
    40 	public function register_routes() {
    38 	public function register_routes() {
    41 		register_rest_route(
    39 		register_rest_route(
    42 			$this->namespace,
    40 			$this->namespace,
    43 			'/' . $this->rest_base . '/themes/(?P<stylesheet>[\/\s%\w\.\(\)\[\]\@_\-]+)/variations',
    41 			'/' . $this->rest_base . '/themes/(?P<stylesheet>[\/\s%\w\.\(\)\[\]\@_\-]+)/variations',
    50 						'stylesheet' => array(
    48 						'stylesheet' => array(
    51 							'description' => __( 'The theme identifier' ),
    49 							'description' => __( 'The theme identifier' ),
    52 							'type'        => 'string',
    50 							'type'        => 'string',
    53 						),
    51 						),
    54 					),
    52 					),
       
    53 					'allow_batch'         => $this->allow_batch,
    55 				),
    54 				),
    56 			)
    55 			)
    57 		);
    56 		);
    58 
    57 
    59 		// List themes global styles.
    58 		// List themes global styles.
    61 			$this->namespace,
    60 			$this->namespace,
    62 			// The route.
    61 			// The route.
    63 			sprintf(
    62 			sprintf(
    64 				'/%s/themes/(?P<stylesheet>%s)',
    63 				'/%s/themes/(?P<stylesheet>%s)',
    65 				$this->rest_base,
    64 				$this->rest_base,
    66 				// Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
    65 				/*
    67 				// Excludes invalid directory name characters: `/:<>*?"|`.
    66 				 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
       
    67 				 * Excludes invalid directory name characters: `/:<>*?"|`.
       
    68 				 */
    68 				'[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?'
    69 				'[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?'
    69 			),
    70 			),
    70 			array(
    71 			array(
    71 				array(
    72 				array(
    72 					'methods'             => WP_REST_Server::READABLE,
    73 					'methods'             => WP_REST_Server::READABLE,
    77 							'description'       => __( 'The theme identifier' ),
    78 							'description'       => __( 'The theme identifier' ),
    78 							'type'              => 'string',
    79 							'type'              => 'string',
    79 							'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
    80 							'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
    80 						),
    81 						),
    81 					),
    82 					),
       
    83 					'allow_batch'         => $this->allow_batch,
    82 				),
    84 				),
    83 			)
    85 			)
    84 		);
    86 		);
    85 
    87 
    86 		// Lists/updates a single global style variation based on the given id.
    88 		// Lists/updates a single global style variation based on the given id.
   104 					'methods'             => WP_REST_Server::EDITABLE,
   106 					'methods'             => WP_REST_Server::EDITABLE,
   105 					'callback'            => array( $this, 'update_item' ),
   107 					'callback'            => array( $this, 'update_item' ),
   106 					'permission_callback' => array( $this, 'update_item_permissions_check' ),
   108 					'permission_callback' => array( $this, 'update_item_permissions_check' ),
   107 					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
   109 					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
   108 				),
   110 				),
   109 				'schema' => array( $this, 'get_public_item_schema' ),
   111 				'schema'      => array( $this, 'get_public_item_schema' ),
       
   112 				'allow_batch' => $this->allow_batch,
   110 			)
   113 			)
   111 		);
   114 		);
   112 	}
   115 	}
   113 
   116 
   114 	/**
   117 	/**
   121 	 * @param string $id_or_stylesheet Global styles ID or stylesheet.
   124 	 * @param string $id_or_stylesheet Global styles ID or stylesheet.
   122 	 * @return string Sanitized global styles ID or stylesheet.
   125 	 * @return string Sanitized global styles ID or stylesheet.
   123 	 */
   126 	 */
   124 	public function _sanitize_global_styles_callback( $id_or_stylesheet ) {
   127 	public function _sanitize_global_styles_callback( $id_or_stylesheet ) {
   125 		return urldecode( $id_or_stylesheet );
   128 		return urldecode( $id_or_stylesheet );
       
   129 	}
       
   130 
       
   131 	/**
       
   132 	 * Get the post, if the ID is valid.
       
   133 	 *
       
   134 	 * @since 5.9.0
       
   135 	 *
       
   136 	 * @param int $id Supplied ID.
       
   137 	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
       
   138 	 */
       
   139 	protected function get_post( $id ) {
       
   140 		$error = new WP_Error(
       
   141 			'rest_global_styles_not_found',
       
   142 			__( 'No global styles config exist with that id.' ),
       
   143 			array( 'status' => 404 )
       
   144 		);
       
   145 
       
   146 		$id = (int) $id;
       
   147 		if ( $id <= 0 ) {
       
   148 			return $error;
       
   149 		}
       
   150 
       
   151 		$post = get_post( $id );
       
   152 		if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
       
   153 			return $error;
       
   154 		}
       
   155 
       
   156 		return $post;
   126 	}
   157 	}
   127 
   158 
   128 	/**
   159 	/**
   129 	 * Checks if a given request has access to read a single global style.
   160 	 * Checks if a given request has access to read a single global style.
   130 	 *
   161 	 *
   164 	 * @since 5.9.0
   195 	 * @since 5.9.0
   165 	 *
   196 	 *
   166 	 * @param WP_Post $post Post object.
   197 	 * @param WP_Post $post Post object.
   167 	 * @return bool Whether the post can be read.
   198 	 * @return bool Whether the post can be read.
   168 	 */
   199 	 */
   169 	protected function check_read_permission( $post ) {
   200 	public function check_read_permission( $post ) {
   170 		return current_user_can( 'read_post', $post->ID );
   201 		return current_user_can( 'read_post', $post->ID );
   171 	}
       
   172 
       
   173 	/**
       
   174 	 * Returns the given global styles config.
       
   175 	 *
       
   176 	 * @since 5.9.0
       
   177 	 *
       
   178 	 * @param WP_REST_Request $request The request instance.
       
   179 	 *
       
   180 	 * @return WP_REST_Response|WP_Error
       
   181 	 */
       
   182 	public function get_item( $request ) {
       
   183 		$post = $this->get_post( $request['id'] );
       
   184 		if ( is_wp_error( $post ) ) {
       
   185 			return $post;
       
   186 		}
       
   187 
       
   188 		return $this->prepare_item_for_response( $post, $request );
       
   189 	}
   202 	}
   190 
   203 
   191 	/**
   204 	/**
   192 	 * Checks if a given request has access to write a single global styles config.
   205 	 * Checks if a given request has access to write a single global styles config.
   193 	 *
   206 	 *
   212 
   225 
   213 		return true;
   226 		return true;
   214 	}
   227 	}
   215 
   228 
   216 	/**
   229 	/**
   217 	 * Checks if a global style can be edited.
       
   218 	 *
       
   219 	 * @since 5.9.0
       
   220 	 *
       
   221 	 * @param WP_Post $post Post object.
       
   222 	 * @return bool Whether the post can be edited.
       
   223 	 */
       
   224 	protected function check_update_permission( $post ) {
       
   225 		return current_user_can( 'edit_post', $post->ID );
       
   226 	}
       
   227 
       
   228 	/**
       
   229 	 * Updates a single global style config.
       
   230 	 *
       
   231 	 * @since 5.9.0
       
   232 	 *
       
   233 	 * @param WP_REST_Request $request Full details about the request.
       
   234 	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
       
   235 	 */
       
   236 	public function update_item( $request ) {
       
   237 		$post_before = $this->get_post( $request['id'] );
       
   238 		if ( is_wp_error( $post_before ) ) {
       
   239 			return $post_before;
       
   240 		}
       
   241 
       
   242 		$changes = $this->prepare_item_for_database( $request );
       
   243 		$result  = wp_update_post( wp_slash( (array) $changes ), true, false );
       
   244 		if ( is_wp_error( $result ) ) {
       
   245 			return $result;
       
   246 		}
       
   247 
       
   248 		$post          = get_post( $request['id'] );
       
   249 		$fields_update = $this->update_additional_fields_for_object( $post, $request );
       
   250 		if ( is_wp_error( $fields_update ) ) {
       
   251 			return $fields_update;
       
   252 		}
       
   253 
       
   254 		wp_after_insert_post( $post, true, $post_before );
       
   255 
       
   256 		$response = $this->prepare_item_for_response( $post, $request );
       
   257 
       
   258 		return rest_ensure_response( $response );
       
   259 	}
       
   260 
       
   261 	/**
       
   262 	 * Prepares a single global styles config for update.
   230 	 * Prepares a single global styles config for update.
   263 	 *
   231 	 *
   264 	 * @since 5.9.0
   232 	 * @since 5.9.0
       
   233 	 * @since 6.2.0 Added validation of styles.css property.
       
   234 	 * @since 6.6.0 Added registration of block style variations from theme.json sources (theme.json, user theme.json, partials).
   265 	 *
   235 	 *
   266 	 * @param WP_REST_Request $request Request object.
   236 	 * @param WP_REST_Request $request Request object.
   267 	 * @return stdClass Changes to pass to wp_update_post.
   237 	 * @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid.
   268 	 */
   238 	 */
   269 	protected function prepare_item_for_database( $request ) {
   239 	protected function prepare_item_for_database( $request ) {
   270 		$changes     = new stdClass();
   240 		$changes     = new stdClass();
   271 		$changes->ID = $request['id'];
   241 		$changes->ID = $request['id'];
   272 
   242 
   282 		}
   252 		}
   283 
   253 
   284 		if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
   254 		if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
   285 			$config = array();
   255 			$config = array();
   286 			if ( isset( $request['styles'] ) ) {
   256 			if ( isset( $request['styles'] ) ) {
       
   257 				if ( isset( $request['styles']['css'] ) ) {
       
   258 					$css_validation_result = $this->validate_custom_css( $request['styles']['css'] );
       
   259 					if ( is_wp_error( $css_validation_result ) ) {
       
   260 						return $css_validation_result;
       
   261 					}
       
   262 				}
   287 				$config['styles'] = $request['styles'];
   263 				$config['styles'] = $request['styles'];
   288 			} elseif ( isset( $existing_config['styles'] ) ) {
   264 			} elseif ( isset( $existing_config['styles'] ) ) {
   289 				$config['styles'] = $existing_config['styles'];
   265 				$config['styles'] = $existing_config['styles'];
   290 			}
   266 			}
       
   267 
       
   268 			// Register theme-defined variations e.g. from block style variation partials under `/styles`.
       
   269 			$variations = WP_Theme_JSON_Resolver::get_style_variations( 'block' );
       
   270 			wp_register_block_style_variations_from_theme_json_partials( $variations );
       
   271 
   291 			if ( isset( $request['settings'] ) ) {
   272 			if ( isset( $request['settings'] ) ) {
   292 				$config['settings'] = $request['settings'];
   273 				$config['settings'] = $request['settings'];
   293 			} elseif ( isset( $existing_config['settings'] ) ) {
   274 			} elseif ( isset( $existing_config['settings'] ) ) {
   294 				$config['settings'] = $existing_config['settings'];
   275 				$config['settings'] = $existing_config['settings'];
   295 			}
   276 			}
   312 
   293 
   313 	/**
   294 	/**
   314 	 * Prepare a global styles config output for response.
   295 	 * Prepare a global styles config output for response.
   315 	 *
   296 	 *
   316 	 * @since 5.9.0
   297 	 * @since 5.9.0
       
   298 	 * @since 6.6.0 Added custom relative theme file URIs to `_links`.
   317 	 *
   299 	 *
   318 	 * @param WP_Post         $post    Global Styles post object.
   300 	 * @param WP_Post         $post    Global Styles post object.
   319 	 * @param WP_REST_Request $request Request object.
   301 	 * @param WP_REST_Request $request Request object.
   320 	 * @return WP_REST_Response Response object.
   302 	 * @return WP_REST_Response Response object.
   321 	 */
   303 	 */
   322 	public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
   304 	public function prepare_item_for_response( $post, $request ) {
   323 		$raw_config                       = json_decode( $post->post_content, true );
   305 		$raw_config                       = json_decode( $post->post_content, true );
   324 		$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
   306 		$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
   325 		$config                           = array();
   307 		$config                           = array();
       
   308 		$theme_json                       = null;
   326 		if ( $is_global_styles_user_theme_json ) {
   309 		if ( $is_global_styles_user_theme_json ) {
   327 			$config = ( new WP_Theme_JSON( $raw_config, 'custom' ) )->get_raw_data();
   310 			$theme_json = new WP_Theme_JSON( $raw_config, 'custom' );
       
   311 			$config     = $theme_json->get_raw_data();
   328 		}
   312 		}
   329 
   313 
   330 		// Base fields for every post.
   314 		// Base fields for every post.
       
   315 		$fields = $this->get_fields_for_response( $request );
   331 		$data   = array();
   316 		$data   = array();
   332 		$fields = $this->get_fields_for_response( $request );
       
   333 
   317 
   334 		if ( rest_is_field_included( 'id', $fields ) ) {
   318 		if ( rest_is_field_included( 'id', $fields ) ) {
   335 			$data['id'] = $post->ID;
   319 			$data['id'] = $post->ID;
   336 		}
   320 		}
   337 
   321 
   362 		$data    = $this->filter_response_by_context( $data, $context );
   346 		$data    = $this->filter_response_by_context( $data, $context );
   363 
   347 
   364 		// Wrap the data in a response object.
   348 		// Wrap the data in a response object.
   365 		$response = rest_ensure_response( $data );
   349 		$response = rest_ensure_response( $data );
   366 
   350 
   367 		$links = $this->prepare_links( $post->ID );
   351 		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
   368 		$response->add_links( $links );
   352 			$links = $this->prepare_links( $post->ID );
   369 		if ( ! empty( $links['self']['href'] ) ) {
   353 
   370 			$actions = $this->get_available_actions();
   354 			// Only return resolved URIs for get requests to user theme JSON.
   371 			$self    = $links['self']['href'];
   355 			if ( $theme_json ) {
   372 			foreach ( $actions as $rel ) {
   356 				$resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json );
   373 				$response->add_link( $rel, $self );
   357 				if ( ! empty( $resolved_theme_uris ) ) {
       
   358 					$links['https://api.w.org/theme-file'] = $resolved_theme_uris;
       
   359 				}
       
   360 			}
       
   361 
       
   362 			$response->add_links( $links );
       
   363 			if ( ! empty( $links['self']['href'] ) ) {
       
   364 				$actions = $this->get_available_actions( $post, $request );
       
   365 				$self    = $links['self']['href'];
       
   366 				foreach ( $actions as $rel ) {
       
   367 					$response->add_link( $rel, $self );
       
   368 				}
   374 			}
   369 			}
   375 		}
   370 		}
   376 
   371 
   377 		return $response;
   372 		return $response;
   378 	}
   373 	}
   379 
   374 
   380 	/**
   375 	/**
   381 	 * Get the post, if the ID is valid.
       
   382 	 *
       
   383 	 * @since 5.9.0
       
   384 	 *
       
   385 	 * @param int $id Supplied ID.
       
   386 	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
       
   387 	 */
       
   388 	protected function get_post( $id ) {
       
   389 		$error = new WP_Error(
       
   390 			'rest_global_styles_not_found',
       
   391 			__( 'No global styles config exist with that id.' ),
       
   392 			array( 'status' => 404 )
       
   393 		);
       
   394 
       
   395 		$id = (int) $id;
       
   396 		if ( $id <= 0 ) {
       
   397 			return $error;
       
   398 		}
       
   399 
       
   400 		$post = get_post( $id );
       
   401 		if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
       
   402 			return $error;
       
   403 		}
       
   404 
       
   405 		return $post;
       
   406 	}
       
   407 
       
   408 
       
   409 	/**
       
   410 	 * Prepares links for the request.
   376 	 * Prepares links for the request.
   411 	 *
   377 	 *
   412 	 * @since 5.9.0
   378 	 * @since 5.9.0
       
   379 	 * @since 6.3.0 Adds revisions count and rest URL href to version-history.
   413 	 *
   380 	 *
   414 	 * @param integer $id ID.
   381 	 * @param integer $id ID.
   415 	 * @return array Links for the given post.
   382 	 * @return array Links for the given post.
   416 	 */
   383 	 */
   417 	protected function prepare_links( $id ) {
   384 	protected function prepare_links( $id ) {
   418 		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
   385 		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );
   419 
   386 
   420 		$links = array(
   387 		$links = array(
   421 			'self' => array(
   388 			'self'  => array(
   422 				'href' => rest_url( trailingslashit( $base ) . $id ),
   389 				'href' => rest_url( trailingslashit( $base ) . $id ),
   423 			),
   390 			),
       
   391 			'about' => array(
       
   392 				'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
       
   393 			),
   424 		);
   394 		);
   425 
   395 
       
   396 		if ( post_type_supports( $this->post_type, 'revisions' ) ) {
       
   397 			$revisions                = wp_get_latest_revision_id_and_total_count( $id );
       
   398 			$revisions_count          = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
       
   399 			$revisions_base           = sprintf( '/%s/%d/revisions', $base, $id );
       
   400 			$links['version-history'] = array(
       
   401 				'href'  => rest_url( $revisions_base ),
       
   402 				'count' => $revisions_count,
       
   403 			);
       
   404 		}
       
   405 
   426 		return $links;
   406 		return $links;
   427 	}
   407 	}
   428 
   408 
   429 	/**
   409 	/**
   430 	 * Get the link relations available for the post and current user.
   410 	 * Get the link relations available for the post and current user.
   431 	 *
   411 	 *
   432 	 * @since 5.9.0
   412 	 * @since 5.9.0
   433 	 *
   413 	 * @since 6.2.0 Added 'edit-css' action.
       
   414 	 * @since 6.6.0 Added $post and $request parameters.
       
   415 	 *
       
   416 	 * @param WP_Post         $post    Post object.
       
   417 	 * @param WP_REST_Request $request Request object.
   434 	 * @return array List of link relations.
   418 	 * @return array List of link relations.
   435 	 */
   419 	 */
   436 	protected function get_available_actions() {
   420 	protected function get_available_actions( $post, $request ) {
   437 		$rels = array();
   421 		$rels = array();
   438 
   422 
   439 		$post_type = get_post_type_object( $this->post_type );
   423 		$post_type = get_post_type_object( $post->post_type );
   440 		if ( current_user_can( $post_type->cap->publish_posts ) ) {
   424 		if ( current_user_can( $post_type->cap->publish_posts ) ) {
   441 			$rels[] = 'https://api.w.org/action-publish';
   425 			$rels[] = 'https://api.w.org/action-publish';
   442 		}
   426 		}
   443 
   427 
       
   428 		if ( current_user_can( 'edit_css' ) ) {
       
   429 			$rels[] = 'https://api.w.org/action-edit-css';
       
   430 		}
       
   431 
   444 		return $rels;
   432 		return $rels;
   445 	}
       
   446 
       
   447 	/**
       
   448 	 * Overwrites the default protected title format.
       
   449 	 *
       
   450 	 * By default, WordPress will show password protected posts with a title of
       
   451 	 * "Protected: %s", as the REST API communicates the protected status of a post
       
   452 	 * in a machine readable format, we remove the "Protected: " prefix.
       
   453 	 *
       
   454 	 * @since 5.9.0
       
   455 	 *
       
   456 	 * @return string Protected title format.
       
   457 	 */
       
   458 	public function protected_title_format() {
       
   459 		return '%s';
       
   460 	}
   433 	}
   461 
   434 
   462 	/**
   435 	/**
   463 	 * Retrieves the query params for the global styles collection.
   436 	 * Retrieves the query params for the global styles collection.
   464 	 *
   437 	 *
   537 	 *
   510 	 *
   538 	 * @param WP_REST_Request $request Full details about the request.
   511 	 * @param WP_REST_Request $request Full details about the request.
   539 	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
   512 	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
   540 	 */
   513 	 */
   541 	public function get_theme_item_permissions_check( $request ) {
   514 	public function get_theme_item_permissions_check( $request ) {
   542 		// Verify if the current user has edit_theme_options capability.
   515 		/*
   543 		// This capability is required to edit/view/delete templates.
   516 		 * Verify if the current user has edit_theme_options capability.
       
   517 		 * This capability is required to edit/view/delete templates.
       
   518 		 */
   544 		if ( ! current_user_can( 'edit_theme_options' ) ) {
   519 		if ( ! current_user_can( 'edit_theme_options' ) ) {
   545 			return new WP_Error(
   520 			return new WP_Error(
   546 				'rest_cannot_manage_global_styles',
   521 				'rest_cannot_manage_global_styles',
   547 				__( 'Sorry, you are not allowed to access the global styles on this site.' ),
   522 				__( 'Sorry, you are not allowed to access the global styles on this site.' ),
   548 				array(
   523 				array(
   556 
   531 
   557 	/**
   532 	/**
   558 	 * Returns the given theme global styles config.
   533 	 * Returns the given theme global styles config.
   559 	 *
   534 	 *
   560 	 * @since 5.9.0
   535 	 * @since 5.9.0
       
   536 	 * @since 6.6.0 Added custom relative theme file URIs to `_links`.
   561 	 *
   537 	 *
   562 	 * @param WP_REST_Request $request The request instance.
   538 	 * @param WP_REST_Request $request The request instance.
   563 	 * @return WP_REST_Response|WP_Error
   539 	 * @return WP_REST_Response|WP_Error
   564 	 */
   540 	 */
   565 	public function get_theme_item( $request ) {
   541 	public function get_theme_item( $request ) {
   566 		if ( wp_get_theme()->get_stylesheet() !== $request['stylesheet'] ) {
   542 		if ( get_stylesheet() !== $request['stylesheet'] ) {
   567 			// This endpoint only supports the active theme for now.
   543 			// This endpoint only supports the active theme for now.
   568 			return new WP_Error(
   544 			return new WP_Error(
   569 				'rest_theme_not_found',
   545 				'rest_theme_not_found',
   570 				__( 'Theme not found.' ),
   546 				__( 'Theme not found.' ),
   571 				array( 'status' => 404 )
   547 				array( 'status' => 404 )
   572 			);
   548 			);
   573 		}
   549 		}
   574 
   550 
   575 		$theme  = WP_Theme_JSON_Resolver::get_merged_data( 'theme' );
   551 		$theme  = WP_Theme_JSON_Resolver::get_merged_data( 'theme' );
       
   552 		$fields = $this->get_fields_for_response( $request );
   576 		$data   = array();
   553 		$data   = array();
   577 		$fields = $this->get_fields_for_response( $request );
       
   578 
   554 
   579 		if ( rest_is_field_included( 'settings', $fields ) ) {
   555 		if ( rest_is_field_included( 'settings', $fields ) ) {
   580 			$data['settings'] = $theme->get_settings();
   556 			$data['settings'] = $theme->get_settings();
   581 		}
   557 		}
   582 
   558 
   589 		$data    = $this->add_additional_fields_to_object( $data, $request );
   565 		$data    = $this->add_additional_fields_to_object( $data, $request );
   590 		$data    = $this->filter_response_by_context( $data, $context );
   566 		$data    = $this->filter_response_by_context( $data, $context );
   591 
   567 
   592 		$response = rest_ensure_response( $data );
   568 		$response = rest_ensure_response( $data );
   593 
   569 
   594 		$links = array(
   570 		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
   595 			'self' => array(
   571 			$links               = array(
   596 				'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
   572 				'self' => array(
   597 			),
   573 					'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
   598 		);
   574 				),
   599 
   575 			);
   600 		$response->add_links( $links );
   576 			$resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme );
       
   577 			if ( ! empty( $resolved_theme_uris ) ) {
       
   578 				$links['https://api.w.org/theme-file'] = $resolved_theme_uris;
       
   579 			}
       
   580 			$response->add_links( $links );
       
   581 		}
   601 
   582 
   602 		return $response;
   583 		return $response;
   603 	}
   584 	}
   604 
   585 
   605 	/**
   586 	/**
   608 	 * @since 6.0.0
   589 	 * @since 6.0.0
   609 	 *
   590 	 *
   610 	 * @param WP_REST_Request $request Full details about the request.
   591 	 * @param WP_REST_Request $request Full details about the request.
   611 	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
   592 	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
   612 	 */
   593 	 */
   613 	public function get_theme_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
   594 	public function get_theme_items_permissions_check( $request ) {
   614 		// Verify if the current user has edit_theme_options capability.
   595 		/*
   615 		// This capability is required to edit/view/delete templates.
   596 		 * Verify if the current user has edit_theme_options capability.
       
   597 		 * This capability is required to edit/view/delete templates.
       
   598 		 */
   616 		if ( ! current_user_can( 'edit_theme_options' ) ) {
   599 		if ( ! current_user_can( 'edit_theme_options' ) ) {
   617 			return new WP_Error(
   600 			return new WP_Error(
   618 				'rest_cannot_manage_global_styles',
   601 				'rest_cannot_manage_global_styles',
   619 				__( 'Sorry, you are not allowed to access the global styles on this site.' ),
   602 				__( 'Sorry, you are not allowed to access the global styles on this site.' ),
   620 				array(
   603 				array(
   628 
   611 
   629 	/**
   612 	/**
   630 	 * Returns the given theme global styles variations.
   613 	 * Returns the given theme global styles variations.
   631 	 *
   614 	 *
   632 	 * @since 6.0.0
   615 	 * @since 6.0.0
       
   616 	 * @since 6.2.0 Returns parent theme variations, if they exist.
       
   617 	 * @since 6.6.0 Added custom relative theme file URIs to `_links` for each item.
   633 	 *
   618 	 *
   634 	 * @param WP_REST_Request $request The request instance.
   619 	 * @param WP_REST_Request $request The request instance.
   635 	 *
   620 	 *
   636 	 * @return WP_REST_Response|WP_Error
   621 	 * @return WP_REST_Response|WP_Error
   637 	 */
   622 	 */
   638 	public function get_theme_items( $request ) {
   623 	public function get_theme_items( $request ) {
   639 		if ( wp_get_theme()->get_stylesheet() !== $request['stylesheet'] ) {
   624 		if ( get_stylesheet() !== $request['stylesheet'] ) {
   640 			// This endpoint only supports the active theme for now.
   625 			// This endpoint only supports the active theme for now.
   641 			return new WP_Error(
   626 			return new WP_Error(
   642 				'rest_theme_not_found',
   627 				'rest_theme_not_found',
   643 				__( 'Theme not found.' ),
   628 				__( 'Theme not found.' ),
   644 				array( 'status' => 404 )
   629 				array( 'status' => 404 )
   645 			);
   630 			);
   646 		}
   631 		}
   647 
   632 
       
   633 		$response   = array();
       
   634 
       
   635 		// Register theme-defined variations e.g. from block style variation partials under `/styles`.
       
   636 		$partials = WP_Theme_JSON_Resolver::get_style_variations( 'block' );
       
   637 		wp_register_block_style_variations_from_theme_json_partials( $partials );
       
   638 
   648 		$variations = WP_Theme_JSON_Resolver::get_style_variations();
   639 		$variations = WP_Theme_JSON_Resolver::get_style_variations();
   649 		$response   = rest_ensure_response( $variations );
   640 		foreach ( $variations as $variation ) {
   650 
   641 			$variation_theme_json = new WP_Theme_JSON( $variation );
   651 		return $response;
   642 			$resolved_theme_uris  = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $variation_theme_json );
       
   643 			$data                 = rest_ensure_response( $variation );
       
   644 			if ( ! empty( $resolved_theme_uris ) ) {
       
   645 				$data->add_links(
       
   646 					array(
       
   647 						'https://api.w.org/theme-file' => $resolved_theme_uris,
       
   648 					)
       
   649 				);
       
   650 			}
       
   651 			$response[] = $this->prepare_response_for_collection( $data );
       
   652 		}
       
   653 
       
   654 		return rest_ensure_response( $response );
       
   655 	}
       
   656 
       
   657 	/**
       
   658 	 * Validate style.css as valid CSS.
       
   659 	 *
       
   660 	 * Currently just checks for invalid markup.
       
   661 	 *
       
   662 	 * @since 6.2.0
       
   663 	 * @since 6.4.0 Changed method visibility to protected.
       
   664 	 *
       
   665 	 * @param string $css CSS to validate.
       
   666 	 * @return true|WP_Error True if the input was validated, otherwise WP_Error.
       
   667 	 */
       
   668 	protected function validate_custom_css( $css ) {
       
   669 		if ( preg_match( '#</?\w+#', $css ) ) {
       
   670 			return new WP_Error(
       
   671 				'rest_custom_css_illegal_markup',
       
   672 				__( 'Markup is not allowed in CSS.' ),
       
   673 				array( 'status' => 400 )
       
   674 			);
       
   675 		}
       
   676 		return true;
   652 	}
   677 	}
   653 }
   678 }