angelovcom.net

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

rest-api.php (93374B)


      1 <?php
      2 /**
      3  * REST API functions.
      4  *
      5  * @package WordPress
      6  * @subpackage REST_API
      7  * @since 4.4.0
      8  */
      9 
     10 /**
     11  * Version number for our API.
     12  *
     13  * @var string
     14  */
     15 define( 'REST_API_VERSION', '2.0' );
     16 
     17 /**
     18  * Registers a REST API route.
     19  *
     20  * Note: Do not use before the {@see 'rest_api_init'} hook.
     21  *
     22  * @since 4.4.0
     23  * @since 5.1.0 Added a `_doing_it_wrong()` notice when not called on or after the `rest_api_init` hook.
     24  * @since 5.5.0 Added a `_doing_it_wrong()` notice when the required `permission_callback` argument is not set.
     25  *
     26  * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin.
     27  * @param string $route     The base URL for route you are adding.
     28  * @param array  $args      Optional. Either an array of options for the endpoint, or an array of arrays for
     29  *                          multiple methods. Default empty array.
     30  * @param bool   $override  Optional. If the route already exists, should we override it? True overrides,
     31  *                          false merges (with newer overriding if duplicate keys exist). Default false.
     32  * @return bool True on success, false on error.
     33  */
     34 function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
     35 	if ( empty( $namespace ) ) {
     36 		/*
     37 		 * Non-namespaced routes are not allowed, with the exception of the main
     38 		 * and namespace indexes. If you really need to register a
     39 		 * non-namespaced route, call `WP_REST_Server::register_route` directly.
     40 		 */
     41 		_doing_it_wrong( 'register_rest_route', __( 'Routes must be namespaced with plugin or theme name and version.' ), '4.4.0' );
     42 		return false;
     43 	} elseif ( empty( $route ) ) {
     44 		_doing_it_wrong( 'register_rest_route', __( 'Route must be specified.' ), '4.4.0' );
     45 		return false;
     46 	}
     47 
     48 	$clean_namespace = trim( $namespace, '/' );
     49 
     50 	if ( $clean_namespace !== $namespace ) {
     51 		_doing_it_wrong( __FUNCTION__, __( 'Namespace must not start or end with a slash.' ), '5.4.2' );
     52 	}
     53 
     54 	if ( ! did_action( 'rest_api_init' ) ) {
     55 		_doing_it_wrong(
     56 			'register_rest_route',
     57 			sprintf(
     58 				/* translators: %s: rest_api_init */
     59 				__( 'REST API routes must be registered on the %s action.' ),
     60 				'<code>rest_api_init</code>'
     61 			),
     62 			'5.1.0'
     63 		);
     64 	}
     65 
     66 	if ( isset( $args['args'] ) ) {
     67 		$common_args = $args['args'];
     68 		unset( $args['args'] );
     69 	} else {
     70 		$common_args = array();
     71 	}
     72 
     73 	if ( isset( $args['callback'] ) ) {
     74 		// Upgrade a single set to multiple.
     75 		$args = array( $args );
     76 	}
     77 
     78 	$defaults = array(
     79 		'methods'  => 'GET',
     80 		'callback' => null,
     81 		'args'     => array(),
     82 	);
     83 
     84 	foreach ( $args as $key => &$arg_group ) {
     85 		if ( ! is_numeric( $key ) ) {
     86 			// Route option, skip here.
     87 			continue;
     88 		}
     89 
     90 		$arg_group         = array_merge( $defaults, $arg_group );
     91 		$arg_group['args'] = array_merge( $common_args, $arg_group['args'] );
     92 
     93 		if ( ! isset( $arg_group['permission_callback'] ) ) {
     94 			_doing_it_wrong(
     95 				__FUNCTION__,
     96 				sprintf(
     97 					/* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */
     98 					__( 'The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback.' ),
     99 					'<code>' . $clean_namespace . '/' . trim( $route, '/' ) . '</code>',
    100 					'<code>permission_callback</code>',
    101 					'<code>__return_true</code>'
    102 				),
    103 				'5.5.0'
    104 			);
    105 		}
    106 	}
    107 
    108 	$full_route = '/' . $clean_namespace . '/' . trim( $route, '/' );
    109 	rest_get_server()->register_route( $clean_namespace, $full_route, $args, $override );
    110 	return true;
    111 }
    112 
    113 /**
    114  * Registers a new field on an existing WordPress object type.
    115  *
    116  * @since 4.7.0
    117  *
    118  * @global array $wp_rest_additional_fields Holds registered fields, organized
    119  *                                          by object type.
    120  *
    121  * @param string|array $object_type Object(s) the field is being registered
    122  *                                  to, "post"|"term"|"comment" etc.
    123  * @param string       $attribute   The attribute name.
    124  * @param array        $args {
    125  *     Optional. An array of arguments used to handle the registered field.
    126  *
    127  *     @type callable|null $get_callback    Optional. The callback function used to retrieve the field value. Default is
    128  *                                          'null', the field will not be returned in the response. The function will
    129  *                                          be passed the prepared object data.
    130  *     @type callable|null $update_callback Optional. The callback function used to set and update the field value. Default
    131  *                                          is 'null', the value cannot be set or updated. The function will be passed
    132  *                                          the model object, like WP_Post.
    133  *     @type array|null $schema             Optional. The schema for this field.
    134  *                                          Default is 'null', no schema entry will be returned.
    135  * }
    136  */
    137 function register_rest_field( $object_type, $attribute, $args = array() ) {
    138 	$defaults = array(
    139 		'get_callback'    => null,
    140 		'update_callback' => null,
    141 		'schema'          => null,
    142 	);
    143 
    144 	$args = wp_parse_args( $args, $defaults );
    145 
    146 	global $wp_rest_additional_fields;
    147 
    148 	$object_types = (array) $object_type;
    149 
    150 	foreach ( $object_types as $object_type ) {
    151 		$wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
    152 	}
    153 }
    154 
    155 /**
    156  * Registers rewrite rules for the REST API.
    157  *
    158  * @since 4.4.0
    159  *
    160  * @see rest_api_register_rewrites()
    161  * @global WP $wp Current WordPress environment instance.
    162  */
    163 function rest_api_init() {
    164 	rest_api_register_rewrites();
    165 
    166 	global $wp;
    167 	$wp->add_query_var( 'rest_route' );
    168 }
    169 
    170 /**
    171  * Adds REST rewrite rules.
    172  *
    173  * @since 4.4.0
    174  *
    175  * @see add_rewrite_rule()
    176  * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
    177  */
    178 function rest_api_register_rewrites() {
    179 	global $wp_rewrite;
    180 
    181 	add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' );
    182 	add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' );
    183 	add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/?$', 'index.php?rest_route=/', 'top' );
    184 	add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/(.*)?', 'index.php?rest_route=/$matches[1]', 'top' );
    185 }
    186 
    187 /**
    188  * Registers the default REST API filters.
    189  *
    190  * Attached to the {@see 'rest_api_init'} action
    191  * to make testing and disabling these filters easier.
    192  *
    193  * @since 4.4.0
    194  */
    195 function rest_api_default_filters() {
    196 	if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
    197 		// Deprecated reporting.
    198 		add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
    199 		add_filter( 'deprecated_function_trigger_error', '__return_false' );
    200 		add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
    201 		add_filter( 'deprecated_argument_trigger_error', '__return_false' );
    202 		add_action( 'doing_it_wrong_run', 'rest_handle_doing_it_wrong', 10, 3 );
    203 		add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
    204 	}
    205 
    206 	// Default serving.
    207 	add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
    208 	add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
    209 	add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 );
    210 
    211 	add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
    212 	add_filter( 'rest_index', 'rest_add_application_passwords_to_index' );
    213 }
    214 
    215 /**
    216  * Registers default REST API routes.
    217  *
    218  * @since 4.7.0
    219  */
    220 function create_initial_rest_routes() {
    221 	foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
    222 		$controller = $post_type->get_rest_controller();
    223 
    224 		if ( ! $controller ) {
    225 			continue;
    226 		}
    227 
    228 		$controller->register_routes();
    229 
    230 		if ( post_type_supports( $post_type->name, 'revisions' ) ) {
    231 			$revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
    232 			$revisions_controller->register_routes();
    233 		}
    234 
    235 		if ( 'attachment' !== $post_type->name ) {
    236 			$autosaves_controller = new WP_REST_Autosaves_Controller( $post_type->name );
    237 			$autosaves_controller->register_routes();
    238 		}
    239 	}
    240 
    241 	// Post types.
    242 	$controller = new WP_REST_Post_Types_Controller;
    243 	$controller->register_routes();
    244 
    245 	// Post statuses.
    246 	$controller = new WP_REST_Post_Statuses_Controller;
    247 	$controller->register_routes();
    248 
    249 	// Taxonomies.
    250 	$controller = new WP_REST_Taxonomies_Controller;
    251 	$controller->register_routes();
    252 
    253 	// Terms.
    254 	foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
    255 		$controller = $taxonomy->get_rest_controller();
    256 
    257 		if ( ! $controller ) {
    258 			continue;
    259 		}
    260 
    261 		$controller->register_routes();
    262 	}
    263 
    264 	// Users.
    265 	$controller = new WP_REST_Users_Controller;
    266 	$controller->register_routes();
    267 
    268 	// Application Passwords
    269 	$controller = new WP_REST_Application_Passwords_Controller();
    270 	$controller->register_routes();
    271 
    272 	// Comments.
    273 	$controller = new WP_REST_Comments_Controller;
    274 	$controller->register_routes();
    275 
    276 	$search_handlers = array(
    277 		new WP_REST_Post_Search_Handler(),
    278 		new WP_REST_Term_Search_Handler(),
    279 		new WP_REST_Post_Format_Search_Handler(),
    280 	);
    281 
    282 	/**
    283 	 * Filters the search handlers to use in the REST search controller.
    284 	 *
    285 	 * @since 5.0.0
    286 	 *
    287 	 * @param array $search_handlers List of search handlers to use in the controller. Each search
    288 	 *                               handler instance must extend the `WP_REST_Search_Handler` class.
    289 	 *                               Default is only a handler for posts.
    290 	 */
    291 	$search_handlers = apply_filters( 'wp_rest_search_handlers', $search_handlers );
    292 
    293 	$controller = new WP_REST_Search_Controller( $search_handlers );
    294 	$controller->register_routes();
    295 
    296 	// Block Renderer.
    297 	$controller = new WP_REST_Block_Renderer_Controller;
    298 	$controller->register_routes();
    299 
    300 	// Block Types.
    301 	$controller = new WP_REST_Block_Types_Controller();
    302 	$controller->register_routes();
    303 
    304 	// Settings.
    305 	$controller = new WP_REST_Settings_Controller;
    306 	$controller->register_routes();
    307 
    308 	// Themes.
    309 	$controller = new WP_REST_Themes_Controller;
    310 	$controller->register_routes();
    311 
    312 	// Plugins.
    313 	$controller = new WP_REST_Plugins_Controller();
    314 	$controller->register_routes();
    315 
    316 	// Sidebars.
    317 	$controller = new WP_REST_Sidebars_Controller();
    318 	$controller->register_routes();
    319 
    320 	// Widget Types.
    321 	$controller = new WP_REST_Widget_Types_Controller();
    322 	$controller->register_routes();
    323 
    324 	// Widgets.
    325 	$controller = new WP_REST_Widgets_Controller();
    326 	$controller->register_routes();
    327 
    328 	// Block Directory.
    329 	$controller = new WP_REST_Block_Directory_Controller();
    330 	$controller->register_routes();
    331 
    332 	// Pattern Directory.
    333 	$controller = new WP_REST_Pattern_Directory_Controller();
    334 	$controller->register_routes();
    335 
    336 	// Site Health.
    337 	$site_health = WP_Site_Health::get_instance();
    338 	$controller  = new WP_REST_Site_Health_Controller( $site_health );
    339 	$controller->register_routes();
    340 }
    341 
    342 /**
    343  * Loads the REST API.
    344  *
    345  * @since 4.4.0
    346  *
    347  * @global WP $wp Current WordPress environment instance.
    348  */
    349 function rest_api_loaded() {
    350 	if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
    351 		return;
    352 	}
    353 
    354 	/**
    355 	 * Whether this is a REST Request.
    356 	 *
    357 	 * @since 4.4.0
    358 	 * @var bool
    359 	 */
    360 	define( 'REST_REQUEST', true );
    361 
    362 	// Initialize the server.
    363 	$server = rest_get_server();
    364 
    365 	// Fire off the request.
    366 	$route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] );
    367 	if ( empty( $route ) ) {
    368 		$route = '/';
    369 	}
    370 	$server->serve_request( $route );
    371 
    372 	// We're done.
    373 	die();
    374 }
    375 
    376 /**
    377  * Retrieves the URL prefix for any API resource.
    378  *
    379  * @since 4.4.0
    380  *
    381  * @return string Prefix.
    382  */
    383 function rest_get_url_prefix() {
    384 	/**
    385 	 * Filters the REST URL prefix.
    386 	 *
    387 	 * @since 4.4.0
    388 	 *
    389 	 * @param string $prefix URL prefix. Default 'wp-json'.
    390 	 */
    391 	return apply_filters( 'rest_url_prefix', 'wp-json' );
    392 }
    393 
    394 /**
    395  * Retrieves the URL to a REST endpoint on a site.
    396  *
    397  * Note: The returned URL is NOT escaped.
    398  *
    399  * @since 4.4.0
    400  *
    401  * @todo Check if this is even necessary
    402  * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
    403  *
    404  * @param int|null $blog_id Optional. Blog ID. Default of null returns URL for current blog.
    405  * @param string   $path    Optional. REST route. Default '/'.
    406  * @param string   $scheme  Optional. Sanitization scheme. Default 'rest'.
    407  * @return string Full URL to the endpoint.
    408  */
    409 function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) {
    410 	if ( empty( $path ) ) {
    411 		$path = '/';
    412 	}
    413 
    414 	$path = '/' . ltrim( $path, '/' );
    415 
    416 	if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) {
    417 		global $wp_rewrite;
    418 
    419 		if ( $wp_rewrite->using_index_permalinks() ) {
    420 			$url = get_home_url( $blog_id, $wp_rewrite->index . '/' . rest_get_url_prefix(), $scheme );
    421 		} else {
    422 			$url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme );
    423 		}
    424 
    425 		$url .= $path;
    426 	} else {
    427 		$url = trailingslashit( get_home_url( $blog_id, '', $scheme ) );
    428 		// nginx only allows HTTP/1.0 methods when redirecting from / to /index.php.
    429 		// To work around this, we manually add index.php to the URL, avoiding the redirect.
    430 		if ( 'index.php' !== substr( $url, 9 ) ) {
    431 			$url .= 'index.php';
    432 		}
    433 
    434 		$url = add_query_arg( 'rest_route', $path, $url );
    435 	}
    436 
    437 	if ( is_ssl() && isset( $_SERVER['SERVER_NAME'] ) ) {
    438 		// If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS.
    439 		if ( parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) === $_SERVER['SERVER_NAME'] ) {
    440 			$url = set_url_scheme( $url, 'https' );
    441 		}
    442 	}
    443 
    444 	if ( is_admin() && force_ssl_admin() ) {
    445 		/*
    446 		 * In this situation the home URL may be http:, and `is_ssl()` may be false,
    447 		 * but the admin is served over https: (one way or another), so REST API usage
    448 		 * will be blocked by browsers unless it is also served over HTTPS.
    449 		 */
    450 		$url = set_url_scheme( $url, 'https' );
    451 	}
    452 
    453 	/**
    454 	 * Filters the REST URL.
    455 	 *
    456 	 * Use this filter to adjust the url returned by the get_rest_url() function.
    457 	 *
    458 	 * @since 4.4.0
    459 	 *
    460 	 * @param string   $url     REST URL.
    461 	 * @param string   $path    REST route.
    462 	 * @param int|null $blog_id Blog ID.
    463 	 * @param string   $scheme  Sanitization scheme.
    464 	 */
    465 	return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
    466 }
    467 
    468 /**
    469  * Retrieves the URL to a REST endpoint.
    470  *
    471  * Note: The returned URL is NOT escaped.
    472  *
    473  * @since 4.4.0
    474  *
    475  * @param string $path   Optional. REST route. Default empty.
    476  * @param string $scheme Optional. Sanitization scheme. Default 'rest'.
    477  * @return string Full URL to the endpoint.
    478  */
    479 function rest_url( $path = '', $scheme = 'rest' ) {
    480 	return get_rest_url( null, $path, $scheme );
    481 }
    482 
    483 /**
    484  * Do a REST request.
    485  *
    486  * Used primarily to route internal requests through WP_REST_Server.
    487  *
    488  * @since 4.4.0
    489  *
    490  * @param WP_REST_Request|string $request Request.
    491  * @return WP_REST_Response REST response.
    492  */
    493 function rest_do_request( $request ) {
    494 	$request = rest_ensure_request( $request );
    495 	return rest_get_server()->dispatch( $request );
    496 }
    497 
    498 /**
    499  * Retrieves the current REST server instance.
    500  *
    501  * Instantiates a new instance if none exists already.
    502  *
    503  * @since 4.5.0
    504  *
    505  * @global WP_REST_Server $wp_rest_server REST server instance.
    506  *
    507  * @return WP_REST_Server REST server instance.
    508  */
    509 function rest_get_server() {
    510 	/* @var WP_REST_Server $wp_rest_server */
    511 	global $wp_rest_server;
    512 
    513 	if ( empty( $wp_rest_server ) ) {
    514 		/**
    515 		 * Filters the REST Server Class.
    516 		 *
    517 		 * This filter allows you to adjust the server class used by the REST API, using a
    518 		 * different class to handle requests.
    519 		 *
    520 		 * @since 4.4.0
    521 		 *
    522 		 * @param string $class_name The name of the server class. Default 'WP_REST_Server'.
    523 		 */
    524 		$wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' );
    525 		$wp_rest_server       = new $wp_rest_server_class;
    526 
    527 		/**
    528 		 * Fires when preparing to serve a REST API request.
    529 		 *
    530 		 * Endpoint objects should be created and register their hooks on this action rather
    531 		 * than another action to ensure they're only loaded when needed.
    532 		 *
    533 		 * @since 4.4.0
    534 		 *
    535 		 * @param WP_REST_Server $wp_rest_server Server object.
    536 		 */
    537 		do_action( 'rest_api_init', $wp_rest_server );
    538 	}
    539 
    540 	return $wp_rest_server;
    541 }
    542 
    543 /**
    544  * Ensures request arguments are a request object (for consistency).
    545  *
    546  * @since 4.4.0
    547  * @since 5.3.0 Accept string argument for the request path.
    548  *
    549  * @param array|string|WP_REST_Request $request Request to check.
    550  * @return WP_REST_Request REST request instance.
    551  */
    552 function rest_ensure_request( $request ) {
    553 	if ( $request instanceof WP_REST_Request ) {
    554 		return $request;
    555 	}
    556 
    557 	if ( is_string( $request ) ) {
    558 		return new WP_REST_Request( 'GET', $request );
    559 	}
    560 
    561 	return new WP_REST_Request( 'GET', '', $request );
    562 }
    563 
    564 /**
    565  * Ensures a REST response is a response object (for consistency).
    566  *
    567  * This implements WP_REST_Response, allowing usage of `set_status`/`header`/etc
    568  * without needing to double-check the object. Will also allow WP_Error to indicate error
    569  * responses, so users should immediately check for this value.
    570  *
    571  * @since 4.4.0
    572  *
    573  * @param WP_REST_Response|WP_Error|WP_HTTP_Response|mixed $response Response to check.
    574  * @return WP_REST_Response|WP_Error If response generated an error, WP_Error, if response
    575  *                                   is already an instance, WP_REST_Response, otherwise
    576  *                                   returns a new WP_REST_Response instance.
    577  */
    578 function rest_ensure_response( $response ) {
    579 	if ( is_wp_error( $response ) ) {
    580 		return $response;
    581 	}
    582 
    583 	if ( $response instanceof WP_REST_Response ) {
    584 		return $response;
    585 	}
    586 
    587 	// While WP_HTTP_Response is the base class of WP_REST_Response, it doesn't provide
    588 	// all the required methods used in WP_REST_Server::dispatch().
    589 	if ( $response instanceof WP_HTTP_Response ) {
    590 		return new WP_REST_Response(
    591 			$response->get_data(),
    592 			$response->get_status(),
    593 			$response->get_headers()
    594 		);
    595 	}
    596 
    597 	return new WP_REST_Response( $response );
    598 }
    599 
    600 /**
    601  * Handles _deprecated_function() errors.
    602  *
    603  * @since 4.4.0
    604  *
    605  * @param string $function    The function that was called.
    606  * @param string $replacement The function that should have been called.
    607  * @param string $version     Version.
    608  */
    609 function rest_handle_deprecated_function( $function, $replacement, $version ) {
    610 	if ( ! WP_DEBUG || headers_sent() ) {
    611 		return;
    612 	}
    613 	if ( ! empty( $replacement ) ) {
    614 		/* translators: 1: Function name, 2: WordPress version number, 3: New function name. */
    615 		$string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement );
    616 	} else {
    617 		/* translators: 1: Function name, 2: WordPress version number. */
    618 		$string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
    619 	}
    620 
    621 	header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) );
    622 }
    623 
    624 /**
    625  * Handles _deprecated_argument() errors.
    626  *
    627  * @since 4.4.0
    628  *
    629  * @param string $function    The function that was called.
    630  * @param string $message     A message regarding the change.
    631  * @param string $version     Version.
    632  */
    633 function rest_handle_deprecated_argument( $function, $message, $version ) {
    634 	if ( ! WP_DEBUG || headers_sent() ) {
    635 		return;
    636 	}
    637 	if ( $message ) {
    638 		/* translators: 1: Function name, 2: WordPress version number, 3: Error message. */
    639 		$string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $message );
    640 	} else {
    641 		/* translators: 1: Function name, 2: WordPress version number. */
    642 		$string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
    643 	}
    644 
    645 	header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
    646 }
    647 
    648 /**
    649  * Handles _doing_it_wrong errors.
    650  *
    651  * @since 5.5.0
    652  *
    653  * @param string      $function The function that was called.
    654  * @param string      $message  A message explaining what has been done incorrectly.
    655  * @param string|null $version  The version of WordPress where the message was added.
    656  */
    657 function rest_handle_doing_it_wrong( $function, $message, $version ) {
    658 	if ( ! WP_DEBUG || headers_sent() ) {
    659 		return;
    660 	}
    661 
    662 	if ( $version ) {
    663 		/* translators: Developer debugging message. 1: PHP function name, 2: WordPress version number, 3: Explanatory message. */
    664 		$string = __( '%1$s (since %2$s; %3$s)' );
    665 		$string = sprintf( $string, $function, $version, $message );
    666 	} else {
    667 		/* translators: Developer debugging message. 1: PHP function name, 2: Explanatory message. */
    668 		$string = __( '%1$s (%2$s)' );
    669 		$string = sprintf( $string, $function, $message );
    670 	}
    671 
    672 	header( sprintf( 'X-WP-DoingItWrong: %s', $string ) );
    673 }
    674 
    675 /**
    676  * Sends Cross-Origin Resource Sharing headers with API requests.
    677  *
    678  * @since 4.4.0
    679  *
    680  * @param mixed $value Response data.
    681  * @return mixed Response data.
    682  */
    683 function rest_send_cors_headers( $value ) {
    684 	$origin = get_http_origin();
    685 
    686 	if ( $origin ) {
    687 		// Requests from file:// and data: URLs send "Origin: null".
    688 		if ( 'null' !== $origin ) {
    689 			$origin = esc_url_raw( $origin );
    690 		}
    691 		header( 'Access-Control-Allow-Origin: ' . $origin );
    692 		header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
    693 		header( 'Access-Control-Allow-Credentials: true' );
    694 		header( 'Vary: Origin', false );
    695 	} elseif ( ! headers_sent() && 'GET' === $_SERVER['REQUEST_METHOD'] && ! is_user_logged_in() ) {
    696 		header( 'Vary: Origin', false );
    697 	}
    698 
    699 	return $value;
    700 }
    701 
    702 /**
    703  * Handles OPTIONS requests for the server.
    704  *
    705  * This is handled outside of the server code, as it doesn't obey normal route
    706  * mapping.
    707  *
    708  * @since 4.4.0
    709  *
    710  * @param mixed           $response Current response, either response or `null` to indicate pass-through.
    711  * @param WP_REST_Server  $handler  ResponseHandler instance (usually WP_REST_Server).
    712  * @param WP_REST_Request $request  The request that was used to make current response.
    713  * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through.
    714  */
    715 function rest_handle_options_request( $response, $handler, $request ) {
    716 	if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) {
    717 		return $response;
    718 	}
    719 
    720 	$response = new WP_REST_Response();
    721 	$data     = array();
    722 
    723 	foreach ( $handler->get_routes() as $route => $endpoints ) {
    724 		$match = preg_match( '@^' . $route . '$@i', $request->get_route(), $matches );
    725 
    726 		if ( ! $match ) {
    727 			continue;
    728 		}
    729 
    730 		$args = array();
    731 		foreach ( $matches as $param => $value ) {
    732 			if ( ! is_int( $param ) ) {
    733 				$args[ $param ] = $value;
    734 			}
    735 		}
    736 
    737 		foreach ( $endpoints as $endpoint ) {
    738 			// Remove the redundant preg_match() argument.
    739 			unset( $args[0] );
    740 
    741 			$request->set_url_params( $args );
    742 			$request->set_attributes( $endpoint );
    743 		}
    744 
    745 		$data = $handler->get_data_for_route( $route, $endpoints, 'help' );
    746 		$response->set_matched_route( $route );
    747 		break;
    748 	}
    749 
    750 	$response->set_data( $data );
    751 	return $response;
    752 }
    753 
    754 /**
    755  * Sends the "Allow" header to state all methods that can be sent to the current route.
    756  *
    757  * @since 4.4.0
    758  *
    759  * @param WP_REST_Response $response Current response being served.
    760  * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
    761  * @param WP_REST_Request  $request  The request that was used to make current response.
    762  * @return WP_REST_Response Response to be served, with "Allow" header if route has allowed methods.
    763  */
    764 function rest_send_allow_header( $response, $server, $request ) {
    765 	$matched_route = $response->get_matched_route();
    766 
    767 	if ( ! $matched_route ) {
    768 		return $response;
    769 	}
    770 
    771 	$routes = $server->get_routes();
    772 
    773 	$allowed_methods = array();
    774 
    775 	// Get the allowed methods across the routes.
    776 	foreach ( $routes[ $matched_route ] as $_handler ) {
    777 		foreach ( $_handler['methods'] as $handler_method => $value ) {
    778 
    779 			if ( ! empty( $_handler['permission_callback'] ) ) {
    780 
    781 				$permission = call_user_func( $_handler['permission_callback'], $request );
    782 
    783 				$allowed_methods[ $handler_method ] = true === $permission;
    784 			} else {
    785 				$allowed_methods[ $handler_method ] = true;
    786 			}
    787 		}
    788 	}
    789 
    790 	// Strip out all the methods that are not allowed (false values).
    791 	$allowed_methods = array_filter( $allowed_methods );
    792 
    793 	if ( $allowed_methods ) {
    794 		$response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) );
    795 	}
    796 
    797 	return $response;
    798 }
    799 
    800 /**
    801  * Recursively computes the intersection of arrays using keys for comparison.
    802  *
    803  * @since 5.3.0
    804  *
    805  * @param array $array1 The array with master keys to check.
    806  * @param array $array2 An array to compare keys against.
    807  * @return array An associative array containing all the entries of array1 which have keys
    808  *               that are present in all arguments.
    809  */
    810 function _rest_array_intersect_key_recursive( $array1, $array2 ) {
    811 	$array1 = array_intersect_key( $array1, $array2 );
    812 	foreach ( $array1 as $key => $value ) {
    813 		if ( is_array( $value ) && is_array( $array2[ $key ] ) ) {
    814 			$array1[ $key ] = _rest_array_intersect_key_recursive( $value, $array2[ $key ] );
    815 		}
    816 	}
    817 	return $array1;
    818 }
    819 
    820 /**
    821  * Filters the REST API response to include only a white-listed set of response object fields.
    822  *
    823  * @since 4.8.0
    824  *
    825  * @param WP_REST_Response $response Current response being served.
    826  * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
    827  * @param WP_REST_Request  $request  The request that was used to make current response.
    828  * @return WP_REST_Response Response to be served, trimmed down to contain a subset of fields.
    829  */
    830 function rest_filter_response_fields( $response, $server, $request ) {
    831 	if ( ! isset( $request['_fields'] ) || $response->is_error() ) {
    832 		return $response;
    833 	}
    834 
    835 	$data = $response->get_data();
    836 
    837 	$fields = wp_parse_list( $request['_fields'] );
    838 
    839 	if ( 0 === count( $fields ) ) {
    840 		return $response;
    841 	}
    842 
    843 	// Trim off outside whitespace from the comma delimited list.
    844 	$fields = array_map( 'trim', $fields );
    845 
    846 	// Create nested array of accepted field hierarchy.
    847 	$fields_as_keyed = array();
    848 	foreach ( $fields as $field ) {
    849 		$parts = explode( '.', $field );
    850 		$ref   = &$fields_as_keyed;
    851 		while ( count( $parts ) > 1 ) {
    852 			$next = array_shift( $parts );
    853 			if ( isset( $ref[ $next ] ) && true === $ref[ $next ] ) {
    854 				// Skip any sub-properties if their parent prop is already marked for inclusion.
    855 				break 2;
    856 			}
    857 			$ref[ $next ] = isset( $ref[ $next ] ) ? $ref[ $next ] : array();
    858 			$ref          = &$ref[ $next ];
    859 		}
    860 		$last         = array_shift( $parts );
    861 		$ref[ $last ] = true;
    862 	}
    863 
    864 	if ( wp_is_numeric_array( $data ) ) {
    865 		$new_data = array();
    866 		foreach ( $data as $item ) {
    867 			$new_data[] = _rest_array_intersect_key_recursive( $item, $fields_as_keyed );
    868 		}
    869 	} else {
    870 		$new_data = _rest_array_intersect_key_recursive( $data, $fields_as_keyed );
    871 	}
    872 
    873 	$response->set_data( $new_data );
    874 
    875 	return $response;
    876 }
    877 
    878 /**
    879  * Given an array of fields to include in a response, some of which may be
    880  * `nested.fields`, determine whether the provided field should be included
    881  * in the response body.
    882  *
    883  * If a parent field is passed in, the presence of any nested field within
    884  * that parent will cause the method to return `true`. For example "title"
    885  * will return true if any of `title`, `title.raw` or `title.rendered` is
    886  * provided.
    887  *
    888  * @since 5.3.0
    889  *
    890  * @param string $field  A field to test for inclusion in the response body.
    891  * @param array  $fields An array of string fields supported by the endpoint.
    892  * @return bool Whether to include the field or not.
    893  */
    894 function rest_is_field_included( $field, $fields ) {
    895 	if ( in_array( $field, $fields, true ) ) {
    896 		return true;
    897 	}
    898 
    899 	foreach ( $fields as $accepted_field ) {
    900 		// Check to see if $field is the parent of any item in $fields.
    901 		// A field "parent" should be accepted if "parent.child" is accepted.
    902 		if ( strpos( $accepted_field, "$field." ) === 0 ) {
    903 			return true;
    904 		}
    905 		// Conversely, if "parent" is accepted, all "parent.child" fields
    906 		// should also be accepted.
    907 		if ( strpos( $field, "$accepted_field." ) === 0 ) {
    908 			return true;
    909 		}
    910 	}
    911 
    912 	return false;
    913 }
    914 
    915 /**
    916  * Adds the REST API URL to the WP RSD endpoint.
    917  *
    918  * @since 4.4.0
    919  *
    920  * @see get_rest_url()
    921  */
    922 function rest_output_rsd() {
    923 	$api_root = get_rest_url();
    924 
    925 	if ( empty( $api_root ) ) {
    926 		return;
    927 	}
    928 	?>
    929 	<api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" />
    930 	<?php
    931 }
    932 
    933 /**
    934  * Outputs the REST API link tag into page header.
    935  *
    936  * @since 4.4.0
    937  *
    938  * @see get_rest_url()
    939  */
    940 function rest_output_link_wp_head() {
    941 	$api_root = get_rest_url();
    942 
    943 	if ( empty( $api_root ) ) {
    944 		return;
    945 	}
    946 
    947 	printf( '<link rel="https://api.w.org/" href="%s" />', esc_url( $api_root ) );
    948 
    949 	$resource = rest_get_queried_resource_route();
    950 
    951 	if ( $resource ) {
    952 		printf( '<link rel="alternate" type="application/json" href="%s" />', esc_url( rest_url( $resource ) ) );
    953 	}
    954 }
    955 
    956 /**
    957  * Sends a Link header for the REST API.
    958  *
    959  * @since 4.4.0
    960  */
    961 function rest_output_link_header() {
    962 	if ( headers_sent() ) {
    963 		return;
    964 	}
    965 
    966 	$api_root = get_rest_url();
    967 
    968 	if ( empty( $api_root ) ) {
    969 		return;
    970 	}
    971 
    972 	header( sprintf( 'Link: <%s>; rel="https://api.w.org/"', esc_url_raw( $api_root ) ), false );
    973 
    974 	$resource = rest_get_queried_resource_route();
    975 
    976 	if ( $resource ) {
    977 		header( sprintf( 'Link: <%s>; rel="alternate"; type="application/json"', esc_url_raw( rest_url( $resource ) ) ), false );
    978 	}
    979 }
    980 
    981 /**
    982  * Checks for errors when using cookie-based authentication.
    983  *
    984  * WordPress' built-in cookie authentication is always active
    985  * for logged in users. However, the API has to check nonces
    986  * for each request to ensure users are not vulnerable to CSRF.
    987  *
    988  * @since 4.4.0
    989  *
    990  * @global mixed          $wp_rest_auth_cookie
    991  *
    992  * @param WP_Error|mixed $result Error from another authentication handler,
    993  *                               null if we should handle it, or another value if not.
    994  * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true.
    995  */
    996 function rest_cookie_check_errors( $result ) {
    997 	if ( ! empty( $result ) ) {
    998 		return $result;
    999 	}
   1000 
   1001 	global $wp_rest_auth_cookie;
   1002 
   1003 	/*
   1004 	 * Is cookie authentication being used? (If we get an auth
   1005 	 * error, but we're still logged in, another authentication
   1006 	 * must have been used).
   1007 	 */
   1008 	if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
   1009 		return $result;
   1010 	}
   1011 
   1012 	// Determine if there is a nonce.
   1013 	$nonce = null;
   1014 
   1015 	if ( isset( $_REQUEST['_wpnonce'] ) ) {
   1016 		$nonce = $_REQUEST['_wpnonce'];
   1017 	} elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
   1018 		$nonce = $_SERVER['HTTP_X_WP_NONCE'];
   1019 	}
   1020 
   1021 	if ( null === $nonce ) {
   1022 		// No nonce at all, so act as if it's an unauthenticated request.
   1023 		wp_set_current_user( 0 );
   1024 		return true;
   1025 	}
   1026 
   1027 	// Check the nonce.
   1028 	$result = wp_verify_nonce( $nonce, 'wp_rest' );
   1029 
   1030 	if ( ! $result ) {
   1031 		return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie check failed' ), array( 'status' => 403 ) );
   1032 	}
   1033 
   1034 	// Send a refreshed nonce in header.
   1035 	rest_get_server()->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) );
   1036 
   1037 	return true;
   1038 }
   1039 
   1040 /**
   1041  * Collects cookie authentication status.
   1042  *
   1043  * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors.
   1044  *
   1045  * @since 4.4.0
   1046  *
   1047  * @see current_action()
   1048  * @global mixed $wp_rest_auth_cookie
   1049  */
   1050 function rest_cookie_collect_status() {
   1051 	global $wp_rest_auth_cookie;
   1052 
   1053 	$status_type = current_action();
   1054 
   1055 	if ( 'auth_cookie_valid' !== $status_type ) {
   1056 		$wp_rest_auth_cookie = substr( $status_type, 12 );
   1057 		return;
   1058 	}
   1059 
   1060 	$wp_rest_auth_cookie = true;
   1061 }
   1062 
   1063 /**
   1064  * Collects the status of authenticating with an application password.
   1065  *
   1066  * @since 5.6.0
   1067  * @since 5.7.0 Added the `$app_password` parameter.
   1068  *
   1069  * @global WP_User|WP_Error|null $wp_rest_application_password_status
   1070  * @global string|null $wp_rest_application_password_uuid
   1071  *
   1072  * @param WP_Error $user_or_error The authenticated user or error instance.
   1073  * @param array    $app_password  The Application Password used to authenticate.
   1074  */
   1075 function rest_application_password_collect_status( $user_or_error, $app_password = array() ) {
   1076 	global $wp_rest_application_password_status, $wp_rest_application_password_uuid;
   1077 
   1078 	$wp_rest_application_password_status = $user_or_error;
   1079 
   1080 	if ( empty( $app_password['uuid'] ) ) {
   1081 		$wp_rest_application_password_uuid = null;
   1082 	} else {
   1083 		$wp_rest_application_password_uuid = $app_password['uuid'];
   1084 	}
   1085 }
   1086 
   1087 /**
   1088  * Gets the Application Password used for authenticating the request.
   1089  *
   1090  * @since 5.7.0
   1091  *
   1092  * @global string|null $wp_rest_application_password_uuid
   1093  *
   1094  * @return string|null The App Password UUID, or null if Application Passwords was not used.
   1095  */
   1096 function rest_get_authenticated_app_password() {
   1097 	global $wp_rest_application_password_uuid;
   1098 
   1099 	return $wp_rest_application_password_uuid;
   1100 }
   1101 
   1102 /**
   1103  * Checks for errors when using application password-based authentication.
   1104  *
   1105  * @since 5.6.0
   1106  *
   1107  * @global WP_User|WP_Error|null $wp_rest_application_password_status
   1108  *
   1109  * @param WP_Error|null|true $result Error from another authentication handler,
   1110  *                                   null if we should handle it, or another value if not.
   1111  * @return WP_Error|null|true WP_Error if the application password is invalid, the $result, otherwise true.
   1112  */
   1113 function rest_application_password_check_errors( $result ) {
   1114 	global $wp_rest_application_password_status;
   1115 
   1116 	if ( ! empty( $result ) ) {
   1117 		return $result;
   1118 	}
   1119 
   1120 	if ( is_wp_error( $wp_rest_application_password_status ) ) {
   1121 		$data = $wp_rest_application_password_status->get_error_data();
   1122 
   1123 		if ( ! isset( $data['status'] ) ) {
   1124 			$data['status'] = 401;
   1125 		}
   1126 
   1127 		$wp_rest_application_password_status->add_data( $data );
   1128 
   1129 		return $wp_rest_application_password_status;
   1130 	}
   1131 
   1132 	if ( $wp_rest_application_password_status instanceof WP_User ) {
   1133 		return true;
   1134 	}
   1135 
   1136 	return $result;
   1137 }
   1138 
   1139 /**
   1140  * Adds Application Passwords info to the REST API index.
   1141  *
   1142  * @since 5.6.0
   1143  *
   1144  * @param WP_REST_Response $response The index response object.
   1145  * @return WP_REST_Response
   1146  */
   1147 function rest_add_application_passwords_to_index( $response ) {
   1148 	if ( ! wp_is_application_passwords_available() ) {
   1149 		return $response;
   1150 	}
   1151 
   1152 	$response->data['authentication']['application-passwords'] = array(
   1153 		'endpoints' => array(
   1154 			'authorization' => admin_url( 'authorize-application.php' ),
   1155 		),
   1156 	);
   1157 
   1158 	return $response;
   1159 }
   1160 
   1161 /**
   1162  * Retrieves the avatar urls in various sizes.
   1163  *
   1164  * @since 4.7.0
   1165  *
   1166  * @see get_avatar_url()
   1167  *
   1168  * @param mixed $id_or_email The Gravatar to retrieve a URL for. Accepts a user_id, gravatar md5 hash,
   1169  *                           user email, WP_User object, WP_Post object, or WP_Comment object.
   1170  * @return array Avatar URLs keyed by size. Each value can be a URL string or boolean false.
   1171  */
   1172 function rest_get_avatar_urls( $id_or_email ) {
   1173 	$avatar_sizes = rest_get_avatar_sizes();
   1174 
   1175 	$urls = array();
   1176 	foreach ( $avatar_sizes as $size ) {
   1177 		$urls[ $size ] = get_avatar_url( $id_or_email, array( 'size' => $size ) );
   1178 	}
   1179 
   1180 	return $urls;
   1181 }
   1182 
   1183 /**
   1184  * Retrieves the pixel sizes for avatars.
   1185  *
   1186  * @since 4.7.0
   1187  *
   1188  * @return int[] List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
   1189  */
   1190 function rest_get_avatar_sizes() {
   1191 	/**
   1192 	 * Filters the REST avatar sizes.
   1193 	 *
   1194 	 * Use this filter to adjust the array of sizes returned by the
   1195 	 * `rest_get_avatar_sizes` function.
   1196 	 *
   1197 	 * @since 4.4.0
   1198 	 *
   1199 	 * @param int[] $sizes An array of int values that are the pixel sizes for avatars.
   1200 	 *                     Default `[ 24, 48, 96 ]`.
   1201 	 */
   1202 	return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
   1203 }
   1204 
   1205 /**
   1206  * Parses an RFC3339 time into a Unix timestamp.
   1207  *
   1208  * @since 4.4.0
   1209  *
   1210  * @param string $date      RFC3339 timestamp.
   1211  * @param bool   $force_utc Optional. Whether to force UTC timezone instead of using
   1212  *                          the timestamp's timezone. Default false.
   1213  * @return int Unix timestamp.
   1214  */
   1215 function rest_parse_date( $date, $force_utc = false ) {
   1216 	if ( $force_utc ) {
   1217 		$date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
   1218 	}
   1219 
   1220 	$regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
   1221 
   1222 	if ( ! preg_match( $regex, $date, $matches ) ) {
   1223 		return false;
   1224 	}
   1225 
   1226 	return strtotime( $date );
   1227 }
   1228 
   1229 /**
   1230  * Parses a 3 or 6 digit hex color (with #).
   1231  *
   1232  * @since 5.4.0
   1233  *
   1234  * @param string $color 3 or 6 digit hex color (with #).
   1235  * @return string|false
   1236  */
   1237 function rest_parse_hex_color( $color ) {
   1238 	$regex = '|^#([A-Fa-f0-9]{3}){1,2}$|';
   1239 	if ( ! preg_match( $regex, $color, $matches ) ) {
   1240 		return false;
   1241 	}
   1242 
   1243 	return $color;
   1244 }
   1245 
   1246 /**
   1247  * Parses a date into both its local and UTC equivalent, in MySQL datetime format.
   1248  *
   1249  * @since 4.4.0
   1250  *
   1251  * @see rest_parse_date()
   1252  *
   1253  * @param string $date   RFC3339 timestamp.
   1254  * @param bool   $is_utc Whether the provided date should be interpreted as UTC. Default false.
   1255  * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
   1256  *                    null on failure.
   1257  */
   1258 function rest_get_date_with_gmt( $date, $is_utc = false ) {
   1259 	/*
   1260 	 * Whether or not the original date actually has a timezone string
   1261 	 * changes the way we need to do timezone conversion.
   1262 	 * Store this info before parsing the date, and use it later.
   1263 	 */
   1264 	$has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date );
   1265 
   1266 	$date = rest_parse_date( $date );
   1267 
   1268 	if ( empty( $date ) ) {
   1269 		return null;
   1270 	}
   1271 
   1272 	/*
   1273 	 * At this point $date could either be a local date (if we were passed
   1274 	 * a *local* date without a timezone offset) or a UTC date (otherwise).
   1275 	 * Timezone conversion needs to be handled differently between these two cases.
   1276 	 */
   1277 	if ( ! $is_utc && ! $has_timezone ) {
   1278 		$local = gmdate( 'Y-m-d H:i:s', $date );
   1279 		$utc   = get_gmt_from_date( $local );
   1280 	} else {
   1281 		$utc   = gmdate( 'Y-m-d H:i:s', $date );
   1282 		$local = get_date_from_gmt( $utc );
   1283 	}
   1284 
   1285 	return array( $local, $utc );
   1286 }
   1287 
   1288 /**
   1289  * Returns a contextual HTTP error code for authorization failure.
   1290  *
   1291  * @since 4.7.0
   1292  *
   1293  * @return int 401 if the user is not logged in, 403 if the user is logged in.
   1294  */
   1295 function rest_authorization_required_code() {
   1296 	return is_user_logged_in() ? 403 : 401;
   1297 }
   1298 
   1299 /**
   1300  * Validate a request argument based on details registered to the route.
   1301  *
   1302  * @since 4.7.0
   1303  *
   1304  * @param mixed           $value
   1305  * @param WP_REST_Request $request
   1306  * @param string          $param
   1307  * @return true|WP_Error
   1308  */
   1309 function rest_validate_request_arg( $value, $request, $param ) {
   1310 	$attributes = $request->get_attributes();
   1311 	if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
   1312 		return true;
   1313 	}
   1314 	$args = $attributes['args'][ $param ];
   1315 
   1316 	return rest_validate_value_from_schema( $value, $args, $param );
   1317 }
   1318 
   1319 /**
   1320  * Sanitize a request argument based on details registered to the route.
   1321  *
   1322  * @since 4.7.0
   1323  *
   1324  * @param mixed           $value
   1325  * @param WP_REST_Request $request
   1326  * @param string          $param
   1327  * @return mixed
   1328  */
   1329 function rest_sanitize_request_arg( $value, $request, $param ) {
   1330 	$attributes = $request->get_attributes();
   1331 	if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
   1332 		return $value;
   1333 	}
   1334 	$args = $attributes['args'][ $param ];
   1335 
   1336 	return rest_sanitize_value_from_schema( $value, $args, $param );
   1337 }
   1338 
   1339 /**
   1340  * Parse a request argument based on details registered to the route.
   1341  *
   1342  * Runs a validation check and sanitizes the value, primarily to be used via
   1343  * the `sanitize_callback` arguments in the endpoint args registration.
   1344  *
   1345  * @since 4.7.0
   1346  *
   1347  * @param mixed           $value
   1348  * @param WP_REST_Request $request
   1349  * @param string          $param
   1350  * @return mixed
   1351  */
   1352 function rest_parse_request_arg( $value, $request, $param ) {
   1353 	$is_valid = rest_validate_request_arg( $value, $request, $param );
   1354 
   1355 	if ( is_wp_error( $is_valid ) ) {
   1356 		return $is_valid;
   1357 	}
   1358 
   1359 	$value = rest_sanitize_request_arg( $value, $request, $param );
   1360 
   1361 	return $value;
   1362 }
   1363 
   1364 /**
   1365  * Determines if an IP address is valid.
   1366  *
   1367  * Handles both IPv4 and IPv6 addresses.
   1368  *
   1369  * @since 4.7.0
   1370  *
   1371  * @param string $ip IP address.
   1372  * @return string|false The valid IP address, otherwise false.
   1373  */
   1374 function rest_is_ip_address( $ip ) {
   1375 	$ipv4_pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/';
   1376 
   1377 	if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) {
   1378 		return false;
   1379 	}
   1380 
   1381 	return $ip;
   1382 }
   1383 
   1384 /**
   1385  * Changes a boolean-like value into the proper boolean value.
   1386  *
   1387  * @since 4.7.0
   1388  *
   1389  * @param bool|string|int $value The value being evaluated.
   1390  * @return bool Returns the proper associated boolean value.
   1391  */
   1392 function rest_sanitize_boolean( $value ) {
   1393 	// String values are translated to `true`; make sure 'false' is false.
   1394 	if ( is_string( $value ) ) {
   1395 		$value = strtolower( $value );
   1396 		if ( in_array( $value, array( 'false', '0' ), true ) ) {
   1397 			$value = false;
   1398 		}
   1399 	}
   1400 
   1401 	// Everything else will map nicely to boolean.
   1402 	return (bool) $value;
   1403 }
   1404 
   1405 /**
   1406  * Determines if a given value is boolean-like.
   1407  *
   1408  * @since 4.7.0
   1409  *
   1410  * @param bool|string $maybe_bool The value being evaluated.
   1411  * @return bool True if a boolean, otherwise false.
   1412  */
   1413 function rest_is_boolean( $maybe_bool ) {
   1414 	if ( is_bool( $maybe_bool ) ) {
   1415 		return true;
   1416 	}
   1417 
   1418 	if ( is_string( $maybe_bool ) ) {
   1419 		$maybe_bool = strtolower( $maybe_bool );
   1420 
   1421 		$valid_boolean_values = array(
   1422 			'false',
   1423 			'true',
   1424 			'0',
   1425 			'1',
   1426 		);
   1427 
   1428 		return in_array( $maybe_bool, $valid_boolean_values, true );
   1429 	}
   1430 
   1431 	if ( is_int( $maybe_bool ) ) {
   1432 		return in_array( $maybe_bool, array( 0, 1 ), true );
   1433 	}
   1434 
   1435 	return false;
   1436 }
   1437 
   1438 /**
   1439  * Determines if a given value is integer-like.
   1440  *
   1441  * @since 5.5.0
   1442  *
   1443  * @param mixed $maybe_integer The value being evaluated.
   1444  * @return bool True if an integer, otherwise false.
   1445  */
   1446 function rest_is_integer( $maybe_integer ) {
   1447 	return is_numeric( $maybe_integer ) && round( (float) $maybe_integer ) === (float) $maybe_integer;
   1448 }
   1449 
   1450 /**
   1451  * Determines if a given value is array-like.
   1452  *
   1453  * @since 5.5.0
   1454  *
   1455  * @param mixed $maybe_array The value being evaluated.
   1456  * @return bool
   1457  */
   1458 function rest_is_array( $maybe_array ) {
   1459 	if ( is_scalar( $maybe_array ) ) {
   1460 		$maybe_array = wp_parse_list( $maybe_array );
   1461 	}
   1462 
   1463 	return wp_is_numeric_array( $maybe_array );
   1464 }
   1465 
   1466 /**
   1467  * Converts an array-like value to an array.
   1468  *
   1469  * @since 5.5.0
   1470  *
   1471  * @param mixed $maybe_array The value being evaluated.
   1472  * @return array Returns the array extracted from the value.
   1473  */
   1474 function rest_sanitize_array( $maybe_array ) {
   1475 	if ( is_scalar( $maybe_array ) ) {
   1476 		return wp_parse_list( $maybe_array );
   1477 	}
   1478 
   1479 	if ( ! is_array( $maybe_array ) ) {
   1480 		return array();
   1481 	}
   1482 
   1483 	// Normalize to numeric array so nothing unexpected is in the keys.
   1484 	return array_values( $maybe_array );
   1485 }
   1486 
   1487 /**
   1488  * Determines if a given value is object-like.
   1489  *
   1490  * @since 5.5.0
   1491  *
   1492  * @param mixed $maybe_object The value being evaluated.
   1493  * @return bool True if object like, otherwise false.
   1494  */
   1495 function rest_is_object( $maybe_object ) {
   1496 	if ( '' === $maybe_object ) {
   1497 		return true;
   1498 	}
   1499 
   1500 	if ( $maybe_object instanceof stdClass ) {
   1501 		return true;
   1502 	}
   1503 
   1504 	if ( $maybe_object instanceof JsonSerializable ) {
   1505 		$maybe_object = $maybe_object->jsonSerialize();
   1506 	}
   1507 
   1508 	return is_array( $maybe_object );
   1509 }
   1510 
   1511 /**
   1512  * Converts an object-like value to an object.
   1513  *
   1514  * @since 5.5.0
   1515  *
   1516  * @param mixed $maybe_object The value being evaluated.
   1517  * @return array Returns the object extracted from the value.
   1518  */
   1519 function rest_sanitize_object( $maybe_object ) {
   1520 	if ( '' === $maybe_object ) {
   1521 		return array();
   1522 	}
   1523 
   1524 	if ( $maybe_object instanceof stdClass ) {
   1525 		return (array) $maybe_object;
   1526 	}
   1527 
   1528 	if ( $maybe_object instanceof JsonSerializable ) {
   1529 		$maybe_object = $maybe_object->jsonSerialize();
   1530 	}
   1531 
   1532 	if ( ! is_array( $maybe_object ) ) {
   1533 		return array();
   1534 	}
   1535 
   1536 	return $maybe_object;
   1537 }
   1538 
   1539 /**
   1540  * Gets the best type for a value.
   1541  *
   1542  * @since 5.5.0
   1543  *
   1544  * @param mixed $value The value to check.
   1545  * @param array $types The list of possible types.
   1546  * @return string The best matching type, an empty string if no types match.
   1547  */
   1548 function rest_get_best_type_for_value( $value, $types ) {
   1549 	static $checks = array(
   1550 		'array'   => 'rest_is_array',
   1551 		'object'  => 'rest_is_object',
   1552 		'integer' => 'rest_is_integer',
   1553 		'number'  => 'is_numeric',
   1554 		'boolean' => 'rest_is_boolean',
   1555 		'string'  => 'is_string',
   1556 		'null'    => 'is_null',
   1557 	);
   1558 
   1559 	// Both arrays and objects allow empty strings to be converted to their types.
   1560 	// But the best answer for this type is a string.
   1561 	if ( '' === $value && in_array( 'string', $types, true ) ) {
   1562 		return 'string';
   1563 	}
   1564 
   1565 	foreach ( $types as $type ) {
   1566 		if ( isset( $checks[ $type ] ) && $checks[ $type ]( $value ) ) {
   1567 			return $type;
   1568 		}
   1569 	}
   1570 
   1571 	return '';
   1572 }
   1573 
   1574 /**
   1575  * Handles getting the best type for a multi-type schema.
   1576  *
   1577  * This is a wrapper for {@see rest_get_best_type_for_value()} that handles
   1578  * backward compatibility for schemas that use invalid types.
   1579  *
   1580  * @since 5.5.0
   1581  *
   1582  * @param mixed  $value The value to check.
   1583  * @param array  $args  The schema array to use.
   1584  * @param string $param The parameter name, used in error messages.
   1585  * @return string
   1586  */
   1587 function rest_handle_multi_type_schema( $value, $args, $param = '' ) {
   1588 	$allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
   1589 	$invalid_types = array_diff( $args['type'], $allowed_types );
   1590 
   1591 	if ( $invalid_types ) {
   1592 		_doing_it_wrong(
   1593 			__FUNCTION__,
   1594 			/* translators: 1: Parameter, 2: List of allowed types. */
   1595 			wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ),
   1596 			'5.5.0'
   1597 		);
   1598 	}
   1599 
   1600 	$best_type = rest_get_best_type_for_value( $value, $args['type'] );
   1601 
   1602 	if ( ! $best_type ) {
   1603 		if ( ! $invalid_types ) {
   1604 			return '';
   1605 		}
   1606 
   1607 		// Backward compatibility for previous behavior which allowed the value if there was an invalid type used.
   1608 		$best_type = reset( $invalid_types );
   1609 	}
   1610 
   1611 	return $best_type;
   1612 }
   1613 
   1614 /**
   1615  * Checks if an array is made up of unique items.
   1616  *
   1617  * @since 5.5.0
   1618  *
   1619  * @param array $array The array to check.
   1620  * @return bool True if the array contains unique items, false otherwise.
   1621  */
   1622 function rest_validate_array_contains_unique_items( $array ) {
   1623 	$seen = array();
   1624 
   1625 	foreach ( $array as $item ) {
   1626 		$stabilized = rest_stabilize_value( $item );
   1627 		$key        = serialize( $stabilized );
   1628 
   1629 		if ( ! isset( $seen[ $key ] ) ) {
   1630 			$seen[ $key ] = true;
   1631 
   1632 			continue;
   1633 		}
   1634 
   1635 		return false;
   1636 	}
   1637 
   1638 	return true;
   1639 }
   1640 
   1641 /**
   1642  * Stabilizes a value following JSON Schema semantics.
   1643  *
   1644  * For lists, order is preserved. For objects, properties are reordered alphabetically.
   1645  *
   1646  * @since 5.5.0
   1647  *
   1648  * @param mixed $value The value to stabilize. Must already be sanitized. Objects should have been converted to arrays.
   1649  * @return mixed The stabilized value.
   1650  */
   1651 function rest_stabilize_value( $value ) {
   1652 	if ( is_scalar( $value ) || is_null( $value ) ) {
   1653 		return $value;
   1654 	}
   1655 
   1656 	if ( is_object( $value ) ) {
   1657 		_doing_it_wrong( __FUNCTION__, __( 'Cannot stabilize objects. Convert the object to an array first.' ), '5.5.0' );
   1658 
   1659 		return $value;
   1660 	}
   1661 
   1662 	ksort( $value );
   1663 
   1664 	foreach ( $value as $k => $v ) {
   1665 		$value[ $k ] = rest_stabilize_value( $v );
   1666 	}
   1667 
   1668 	return $value;
   1669 }
   1670 
   1671 /**
   1672  * Validates if the JSON Schema pattern matches a value.
   1673  *
   1674  * @since 5.6.0
   1675  *
   1676  * @param string $pattern The pattern to match against.
   1677  * @param string $value   The value to check.
   1678  * @return bool           True if the pattern matches the given value, false otherwise.
   1679  */
   1680 function rest_validate_json_schema_pattern( $pattern, $value ) {
   1681 	$escaped_pattern = str_replace( '#', '\\#', $pattern );
   1682 
   1683 	return 1 === preg_match( '#' . $escaped_pattern . '#u', $value );
   1684 }
   1685 
   1686 /**
   1687  * Finds the schema for a property using the patternProperties keyword.
   1688  *
   1689  * @since 5.6.0
   1690  *
   1691  * @param string $property The property name to check.
   1692  * @param array  $args     The schema array to use.
   1693  * @return array|null      The schema of matching pattern property, or null if no patterns match.
   1694  */
   1695 function rest_find_matching_pattern_property_schema( $property, $args ) {
   1696 	if ( isset( $args['patternProperties'] ) ) {
   1697 		foreach ( $args['patternProperties'] as $pattern => $child_schema ) {
   1698 			if ( rest_validate_json_schema_pattern( $pattern, $property ) ) {
   1699 				return $child_schema;
   1700 			}
   1701 		}
   1702 	}
   1703 
   1704 	return null;
   1705 }
   1706 
   1707 /**
   1708  * Formats a combining operation error into a WP_Error object.
   1709  *
   1710  * @since 5.6.0
   1711  *
   1712  * @param string $param The parameter name.
   1713  * @param array $error  The error details.
   1714  * @return WP_Error
   1715  */
   1716 function rest_format_combining_operation_error( $param, $error ) {
   1717 	$position = $error['index'];
   1718 	$reason   = $error['error_object']->get_error_message();
   1719 
   1720 	if ( isset( $error['schema']['title'] ) ) {
   1721 		$title = $error['schema']['title'];
   1722 
   1723 		return new WP_Error(
   1724 			'rest_no_matching_schema',
   1725 			/* translators: 1: Parameter, 2: Schema title, 3: Reason. */
   1726 			sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ),
   1727 			array( 'position' => $position )
   1728 		);
   1729 	}
   1730 
   1731 	return new WP_Error(
   1732 		'rest_no_matching_schema',
   1733 		/* translators: 1: Parameter, 2: Reason. */
   1734 		sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ),
   1735 		array( 'position' => $position )
   1736 	);
   1737 }
   1738 
   1739 /**
   1740  * Gets the error of combining operation.
   1741  *
   1742  * @since 5.6.0
   1743  *
   1744  * @param array  $value  The value to validate.
   1745  * @param string $param  The parameter name, used in error messages.
   1746  * @param array  $errors The errors array, to search for possible error.
   1747  * @return WP_Error      The combining operation error.
   1748  */
   1749 function rest_get_combining_operation_error( $value, $param, $errors ) {
   1750 	// If there is only one error, simply return it.
   1751 	if ( 1 === count( $errors ) ) {
   1752 		return rest_format_combining_operation_error( $param, $errors[0] );
   1753 	}
   1754 
   1755 	// Filter out all errors related to type validation.
   1756 	$filtered_errors = array();
   1757 	foreach ( $errors as $error ) {
   1758 		$error_code = $error['error_object']->get_error_code();
   1759 		$error_data = $error['error_object']->get_error_data();
   1760 
   1761 		if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) {
   1762 			$filtered_errors[] = $error;
   1763 		}
   1764 	}
   1765 
   1766 	// If there is only one error left, simply return it.
   1767 	if ( 1 === count( $filtered_errors ) ) {
   1768 		return rest_format_combining_operation_error( $param, $filtered_errors[0] );
   1769 	}
   1770 
   1771 	// If there are only errors related to object validation, try choosing the most appropriate one.
   1772 	if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) {
   1773 		$result = null;
   1774 		$number = 0;
   1775 
   1776 		foreach ( $filtered_errors as $error ) {
   1777 			if ( isset( $error['schema']['properties'] ) ) {
   1778 				$n = count( array_intersect_key( $error['schema']['properties'], $value ) );
   1779 				if ( $n > $number ) {
   1780 					$result = $error;
   1781 					$number = $n;
   1782 				}
   1783 			}
   1784 		}
   1785 
   1786 		if ( null !== $result ) {
   1787 			return rest_format_combining_operation_error( $param, $result );
   1788 		}
   1789 	}
   1790 
   1791 	// If each schema has a title, include those titles in the error message.
   1792 	$schema_titles = array();
   1793 	foreach ( $errors as $error ) {
   1794 		if ( isset( $error['schema']['title'] ) ) {
   1795 			$schema_titles[] = $error['schema']['title'];
   1796 		}
   1797 	}
   1798 
   1799 	if ( count( $schema_titles ) === count( $errors ) ) {
   1800 		/* translators: 1: Parameter, 2: Schema titles. */
   1801 		return new WP_Error( 'rest_no_matching_schema', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) );
   1802 	}
   1803 
   1804 	/* translators: %s: Parameter. */
   1805 	return new WP_Error( 'rest_no_matching_schema', sprintf( __( '%s does not match any of the expected formats.' ), $param ) );
   1806 }
   1807 
   1808 /**
   1809  * Finds the matching schema among the "anyOf" schemas.
   1810  *
   1811  * @since 5.6.0
   1812  *
   1813  * @param mixed  $value   The value to validate.
   1814  * @param array  $args    The schema array to use.
   1815  * @param string $param   The parameter name, used in error messages.
   1816  * @return array|WP_Error The matching schema or WP_Error instance if all schemas do not match.
   1817  */
   1818 function rest_find_any_matching_schema( $value, $args, $param ) {
   1819 	$errors = array();
   1820 
   1821 	foreach ( $args['anyOf'] as $index => $schema ) {
   1822 		if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
   1823 			$schema['type'] = $args['type'];
   1824 		}
   1825 
   1826 		$is_valid = rest_validate_value_from_schema( $value, $schema, $param );
   1827 		if ( ! is_wp_error( $is_valid ) ) {
   1828 			return $schema;
   1829 		}
   1830 
   1831 		$errors[] = array(
   1832 			'error_object' => $is_valid,
   1833 			'schema'       => $schema,
   1834 			'index'        => $index,
   1835 		);
   1836 	}
   1837 
   1838 	return rest_get_combining_operation_error( $value, $param, $errors );
   1839 }
   1840 
   1841 /**
   1842  * Finds the matching schema among the "oneOf" schemas.
   1843  *
   1844  * @since 5.6.0
   1845  *
   1846  * @param mixed  $value                  The value to validate.
   1847  * @param array  $args                   The schema array to use.
   1848  * @param string $param                  The parameter name, used in error messages.
   1849  * @param bool   $stop_after_first_match Optional. Whether the process should stop after the first successful match.
   1850  * @return array|WP_Error                The matching schema or WP_Error instance if the number of matching schemas is not equal to one.
   1851  */
   1852 function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) {
   1853 	$matching_schemas = array();
   1854 	$errors           = array();
   1855 
   1856 	foreach ( $args['oneOf'] as $index => $schema ) {
   1857 		if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
   1858 			$schema['type'] = $args['type'];
   1859 		}
   1860 
   1861 		$is_valid = rest_validate_value_from_schema( $value, $schema, $param );
   1862 		if ( ! is_wp_error( $is_valid ) ) {
   1863 			if ( $stop_after_first_match ) {
   1864 				return $schema;
   1865 			}
   1866 
   1867 			$matching_schemas[] = array(
   1868 				'schema_object' => $schema,
   1869 				'index'         => $index,
   1870 			);
   1871 		} else {
   1872 			$errors[] = array(
   1873 				'error_object' => $is_valid,
   1874 				'schema'       => $schema,
   1875 				'index'        => $index,
   1876 			);
   1877 		}
   1878 	}
   1879 
   1880 	if ( ! $matching_schemas ) {
   1881 		return rest_get_combining_operation_error( $value, $param, $errors );
   1882 	}
   1883 
   1884 	if ( count( $matching_schemas ) > 1 ) {
   1885 		$schema_positions = array();
   1886 		$schema_titles    = array();
   1887 
   1888 		foreach ( $matching_schemas as $schema ) {
   1889 			$schema_positions[] = $schema['index'];
   1890 
   1891 			if ( isset( $schema['schema_object']['title'] ) ) {
   1892 				$schema_titles[] = $schema['schema_object']['title'];
   1893 			}
   1894 		}
   1895 
   1896 		// If each schema has a title, include those titles in the error message.
   1897 		if ( count( $schema_titles ) === count( $matching_schemas ) ) {
   1898 			return new WP_Error(
   1899 				'rest_one_of_multiple_matches',
   1900 				/* translators: 1: Parameter, 2: Schema titles. */
   1901 				wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ),
   1902 				array( 'positions' => $schema_positions )
   1903 			);
   1904 		}
   1905 
   1906 		return new WP_Error(
   1907 			'rest_one_of_multiple_matches',
   1908 			/* translators: %s: Parameter. */
   1909 			sprintf( __( '%s matches more than one of the expected formats.' ), $param ),
   1910 			array( 'positions' => $schema_positions )
   1911 		);
   1912 	}
   1913 
   1914 	return $matching_schemas[0]['schema_object'];
   1915 }
   1916 
   1917 /**
   1918  * Checks the equality of two values, following JSON Schema semantics.
   1919  *
   1920  * Property order is ignored for objects.
   1921  *
   1922  * Values must have been previously sanitized/coerced to their native types.
   1923  *
   1924  * @since 5.7.0
   1925  *
   1926  * @param mixed $value1 The first value to check.
   1927  * @param mixed $value2 The second value to check.
   1928  * @return bool True if the values are equal or false otherwise.
   1929  */
   1930 function rest_are_values_equal( $value1, $value2 ) {
   1931 	if ( is_array( $value1 ) && is_array( $value2 ) ) {
   1932 		if ( count( $value1 ) !== count( $value2 ) ) {
   1933 			return false;
   1934 		}
   1935 
   1936 		foreach ( $value1 as $index => $value ) {
   1937 			if ( ! array_key_exists( $index, $value2 ) || ! rest_are_values_equal( $value, $value2[ $index ] ) ) {
   1938 				return false;
   1939 			}
   1940 		}
   1941 
   1942 		return true;
   1943 	}
   1944 
   1945 	if ( is_int( $value1 ) && is_float( $value2 )
   1946 		|| is_float( $value1 ) && is_int( $value2 )
   1947 	) {
   1948 		return (float) $value1 === (float) $value2;
   1949 	}
   1950 
   1951 	return $value1 === $value2;
   1952 }
   1953 
   1954 /**
   1955  * Validates that the given value is a member of the JSON Schema "enum".
   1956  *
   1957  * @since 5.7.0
   1958  *
   1959  * @param mixed  $value  The value to validate.
   1960  * @param array  $args   The schema array to use.
   1961  * @param string $param  The parameter name, used in error messages.
   1962  * @return true|WP_Error True if the "enum" contains the value or a WP_Error instance otherwise.
   1963  */
   1964 function rest_validate_enum( $value, $args, $param ) {
   1965 	$sanitized_value = rest_sanitize_value_from_schema( $value, $args, $param );
   1966 	if ( is_wp_error( $sanitized_value ) ) {
   1967 		return $sanitized_value;
   1968 	}
   1969 
   1970 	foreach ( $args['enum'] as $enum_value ) {
   1971 		if ( rest_are_values_equal( $sanitized_value, $enum_value ) ) {
   1972 			return true;
   1973 		}
   1974 	}
   1975 
   1976 	$encoded_enum_values = array();
   1977 	foreach ( $args['enum'] as $enum_value ) {
   1978 		$encoded_enum_values[] = is_scalar( $enum_value ) ? $enum_value : wp_json_encode( $enum_value );
   1979 	}
   1980 
   1981 	if ( count( $encoded_enum_values ) === 1 ) {
   1982 		/* translators: 1: Parameter, 2: Valid values. */
   1983 		return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not %2$s.' ), $param, $encoded_enum_values[0] ) );
   1984 	}
   1985 
   1986 	/* translators: 1: Parameter, 2: List of valid values. */
   1987 	return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not one of %2$l.' ), $param, $encoded_enum_values ) );
   1988 }
   1989 
   1990 /**
   1991  * Get all valid JSON schema properties.
   1992  *
   1993  * @since 5.6.0
   1994  *
   1995  * @return string[] All valid JSON schema properties.
   1996  */
   1997 function rest_get_allowed_schema_keywords() {
   1998 	return array(
   1999 		'title',
   2000 		'description',
   2001 		'default',
   2002 		'type',
   2003 		'format',
   2004 		'enum',
   2005 		'items',
   2006 		'properties',
   2007 		'additionalProperties',
   2008 		'patternProperties',
   2009 		'minProperties',
   2010 		'maxProperties',
   2011 		'minimum',
   2012 		'maximum',
   2013 		'exclusiveMinimum',
   2014 		'exclusiveMaximum',
   2015 		'multipleOf',
   2016 		'minLength',
   2017 		'maxLength',
   2018 		'pattern',
   2019 		'minItems',
   2020 		'maxItems',
   2021 		'uniqueItems',
   2022 		'anyOf',
   2023 		'oneOf',
   2024 	);
   2025 }
   2026 
   2027 /**
   2028  * Validate a value based on a schema.
   2029  *
   2030  * @since 4.7.0
   2031  * @since 4.9.0 Support the "object" type.
   2032  * @since 5.2.0 Support validating "additionalProperties" against a schema.
   2033  * @since 5.3.0 Support multiple types.
   2034  * @since 5.4.0 Convert an empty string to an empty object.
   2035  * @since 5.5.0 Add the "uuid" and "hex-color" formats.
   2036  *              Support the "minLength", "maxLength" and "pattern" keywords for strings.
   2037  *              Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays.
   2038  *              Validate required properties.
   2039  * @since 5.6.0 Support the "minProperties" and "maxProperties" keywords for objects.
   2040  *              Support the "multipleOf" keyword for numbers and integers.
   2041  *              Support the "patternProperties" keyword for objects.
   2042  *              Support the "anyOf" and "oneOf" keywords.
   2043  *
   2044  * @param mixed  $value The value to validate.
   2045  * @param array  $args  Schema array to use for validation.
   2046  * @param string $param The parameter name, used in error messages.
   2047  * @return true|WP_Error
   2048  */
   2049 function rest_validate_value_from_schema( $value, $args, $param = '' ) {
   2050 	if ( isset( $args['anyOf'] ) ) {
   2051 		$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
   2052 		if ( is_wp_error( $matching_schema ) ) {
   2053 			return $matching_schema;
   2054 		}
   2055 
   2056 		if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
   2057 			$args['type'] = $matching_schema['type'];
   2058 		}
   2059 	}
   2060 
   2061 	if ( isset( $args['oneOf'] ) ) {
   2062 		$matching_schema = rest_find_one_matching_schema( $value, $args, $param );
   2063 		if ( is_wp_error( $matching_schema ) ) {
   2064 			return $matching_schema;
   2065 		}
   2066 
   2067 		if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
   2068 			$args['type'] = $matching_schema['type'];
   2069 		}
   2070 	}
   2071 
   2072 	$allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
   2073 
   2074 	if ( ! isset( $args['type'] ) ) {
   2075 		/* translators: %s: Parameter. */
   2076 		_doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
   2077 	}
   2078 
   2079 	if ( is_array( $args['type'] ) ) {
   2080 		$best_type = rest_handle_multi_type_schema( $value, $args, $param );
   2081 
   2082 		if ( ! $best_type ) {
   2083 			return new WP_Error(
   2084 				'rest_invalid_type',
   2085 				/* translators: 1: Parameter, 2: List of types. */
   2086 				sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ),
   2087 				array( 'param' => $param )
   2088 			);
   2089 		}
   2090 
   2091 		$args['type'] = $best_type;
   2092 	}
   2093 
   2094 	if ( ! in_array( $args['type'], $allowed_types, true ) ) {
   2095 		_doing_it_wrong(
   2096 			__FUNCTION__,
   2097 			/* translators: 1: Parameter, 2: The list of allowed types. */
   2098 			wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
   2099 			'5.5.0'
   2100 		);
   2101 	}
   2102 
   2103 	switch ( $args['type'] ) {
   2104 		case 'null':
   2105 			$is_valid = rest_validate_null_value_from_schema( $value, $param );
   2106 			break;
   2107 		case 'boolean':
   2108 			$is_valid = rest_validate_boolean_value_from_schema( $value, $param );
   2109 			break;
   2110 		case 'object':
   2111 			$is_valid = rest_validate_object_value_from_schema( $value, $args, $param );
   2112 			break;
   2113 		case 'array':
   2114 			$is_valid = rest_validate_array_value_from_schema( $value, $args, $param );
   2115 			break;
   2116 		case 'number':
   2117 			$is_valid = rest_validate_number_value_from_schema( $value, $args, $param );
   2118 			break;
   2119 		case 'string':
   2120 			$is_valid = rest_validate_string_value_from_schema( $value, $args, $param );
   2121 			break;
   2122 		case 'integer':
   2123 			$is_valid = rest_validate_integer_value_from_schema( $value, $args, $param );
   2124 			break;
   2125 		default:
   2126 			$is_valid = true;
   2127 			break;
   2128 	}
   2129 
   2130 	if ( is_wp_error( $is_valid ) ) {
   2131 		return $is_valid;
   2132 	}
   2133 
   2134 	if ( ! empty( $args['enum'] ) ) {
   2135 		$enum_contains_value = rest_validate_enum( $value, $args, $param );
   2136 		if ( is_wp_error( $enum_contains_value ) ) {
   2137 			return $enum_contains_value;
   2138 		}
   2139 	}
   2140 
   2141 	// The "format" keyword should only be applied to strings. However, for backward compatibility,
   2142 	// we allow the "format" keyword if the type keyword was not specified, or was set to an invalid value.
   2143 	if ( isset( $args['format'] )
   2144 		&& ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
   2145 	) {
   2146 		switch ( $args['format'] ) {
   2147 			case 'hex-color':
   2148 				if ( ! rest_parse_hex_color( $value ) ) {
   2149 					return new WP_Error( 'rest_invalid_hex_color', __( 'Invalid hex color.' ) );
   2150 				}
   2151 				break;
   2152 
   2153 			case 'date-time':
   2154 				if ( ! rest_parse_date( $value ) ) {
   2155 					return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) );
   2156 				}
   2157 				break;
   2158 
   2159 			case 'email':
   2160 				if ( ! is_email( $value ) ) {
   2161 					return new WP_Error( 'rest_invalid_email', __( 'Invalid email address.' ) );
   2162 				}
   2163 				break;
   2164 			case 'ip':
   2165 				if ( ! rest_is_ip_address( $value ) ) {
   2166 					/* translators: %s: IP address. */
   2167 					return new WP_Error( 'rest_invalid_ip', sprintf( __( '%s is not a valid IP address.' ), $param ) );
   2168 				}
   2169 				break;
   2170 			case 'uuid':
   2171 				if ( ! wp_is_uuid( $value ) ) {
   2172 					/* translators: %s: The name of a JSON field expecting a valid UUID. */
   2173 					return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) );
   2174 				}
   2175 				break;
   2176 		}
   2177 	}
   2178 
   2179 	return true;
   2180 }
   2181 
   2182 /**
   2183  * Validates a null value based on a schema.
   2184  *
   2185  * @since 5.7.0
   2186  *
   2187  * @param mixed  $value The value to validate.
   2188  * @param string $param The parameter name, used in error messages.
   2189  * @return true|WP_Error
   2190  */
   2191 function rest_validate_null_value_from_schema( $value, $param ) {
   2192 	if ( null !== $value ) {
   2193 		return new WP_Error(
   2194 			'rest_invalid_type',
   2195 			/* translators: 1: Parameter, 2: Type name. */
   2196 			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ),
   2197 			array( 'param' => $param )
   2198 		);
   2199 	}
   2200 
   2201 	return true;
   2202 }
   2203 
   2204 /**
   2205  * Validates a boolean value based on a schema.
   2206  *
   2207  * @since 5.7.0
   2208  *
   2209  * @param mixed  $value The value to validate.
   2210  * @param string $param The parameter name, used in error messages.
   2211  * @return true|WP_Error
   2212  */
   2213 function rest_validate_boolean_value_from_schema( $value, $param ) {
   2214 	if ( ! rest_is_boolean( $value ) ) {
   2215 		return new WP_Error(
   2216 			'rest_invalid_type',
   2217 			/* translators: 1: Parameter, 2: Type name. */
   2218 			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ),
   2219 			array( 'param' => $param )
   2220 		);
   2221 	}
   2222 
   2223 	return true;
   2224 }
   2225 
   2226 /**
   2227  * Validates an object value based on a schema.
   2228  *
   2229  * @since 5.7.0
   2230  *
   2231  * @param mixed  $value The value to validate.
   2232  * @param array  $args  Schema array to use for validation.
   2233  * @param string $param The parameter name, used in error messages.
   2234  * @return true|WP_Error
   2235  */
   2236 function rest_validate_object_value_from_schema( $value, $args, $param ) {
   2237 	if ( ! rest_is_object( $value ) ) {
   2238 		return new WP_Error(
   2239 			'rest_invalid_type',
   2240 			/* translators: 1: Parameter, 2: Type name. */
   2241 			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ),
   2242 			array( 'param' => $param )
   2243 		);
   2244 	}
   2245 
   2246 	$value = rest_sanitize_object( $value );
   2247 
   2248 	if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4
   2249 		foreach ( $args['required'] as $name ) {
   2250 			if ( ! array_key_exists( $name, $value ) ) {
   2251 				return new WP_Error(
   2252 					'rest_property_required',
   2253 					/* translators: 1: Property of an object, 2: Parameter. */
   2254 					sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param )
   2255 				);
   2256 			}
   2257 		}
   2258 	} elseif ( isset( $args['properties'] ) ) { // schema version 3
   2259 		foreach ( $args['properties'] as $name => $property ) {
   2260 			if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) {
   2261 				return new WP_Error(
   2262 					'rest_property_required',
   2263 					/* translators: 1: Property of an object, 2: Parameter. */
   2264 					sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param )
   2265 				);
   2266 			}
   2267 		}
   2268 	}
   2269 
   2270 	foreach ( $value as $property => $v ) {
   2271 		if ( isset( $args['properties'][ $property ] ) ) {
   2272 			$is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
   2273 			if ( is_wp_error( $is_valid ) ) {
   2274 				return $is_valid;
   2275 			}
   2276 			continue;
   2277 		}
   2278 
   2279 		$pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args );
   2280 		if ( null !== $pattern_property_schema ) {
   2281 			$is_valid = rest_validate_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' );
   2282 			if ( is_wp_error( $is_valid ) ) {
   2283 				return $is_valid;
   2284 			}
   2285 			continue;
   2286 		}
   2287 
   2288 		if ( isset( $args['additionalProperties'] ) ) {
   2289 			if ( false === $args['additionalProperties'] ) {
   2290 				return new WP_Error(
   2291 					'rest_additional_properties_forbidden',
   2292 					/* translators: %s: Property of an object. */
   2293 					sprintf( __( '%1$s is not a valid property of Object.' ), $property )
   2294 				);
   2295 			}
   2296 
   2297 			if ( is_array( $args['additionalProperties'] ) ) {
   2298 				$is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
   2299 				if ( is_wp_error( $is_valid ) ) {
   2300 					return $is_valid;
   2301 				}
   2302 			}
   2303 		}
   2304 	}
   2305 
   2306 	if ( isset( $args['minProperties'] ) && count( $value ) < $args['minProperties'] ) {
   2307 		return new WP_Error(
   2308 			'rest_too_few_properties',
   2309 			sprintf(
   2310 				/* translators: 1: Parameter, 2: Number. */
   2311 				_n(
   2312 					'%1$s must contain at least %2$s property.',
   2313 					'%1$s must contain at least %2$s properties.',
   2314 					$args['minProperties']
   2315 				),
   2316 				$param,
   2317 				number_format_i18n( $args['minProperties'] )
   2318 			)
   2319 		);
   2320 	}
   2321 
   2322 	if ( isset( $args['maxProperties'] ) && count( $value ) > $args['maxProperties'] ) {
   2323 		return new WP_Error(
   2324 			'rest_too_many_properties',
   2325 			sprintf(
   2326 				/* translators: 1: Parameter, 2: Number. */
   2327 				_n(
   2328 					'%1$s must contain at most %2$s property.',
   2329 					'%1$s must contain at most %2$s properties.',
   2330 					$args['maxProperties']
   2331 				),
   2332 				$param,
   2333 				number_format_i18n( $args['maxProperties'] )
   2334 			)
   2335 		);
   2336 	}
   2337 
   2338 	return true;
   2339 }
   2340 
   2341 /**
   2342  * Validates an array value based on a schema.
   2343  *
   2344  * @since 5.7.0
   2345  *
   2346  * @param mixed  $value The value to validate.
   2347  * @param array  $args  Schema array to use for validation.
   2348  * @param string $param The parameter name, used in error messages.
   2349  * @return true|WP_Error
   2350  */
   2351 function rest_validate_array_value_from_schema( $value, $args, $param ) {
   2352 	if ( ! rest_is_array( $value ) ) {
   2353 		return new WP_Error(
   2354 			'rest_invalid_type',
   2355 			/* translators: 1: Parameter, 2: Type name. */
   2356 			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ),
   2357 			array( 'param' => $param )
   2358 		);
   2359 	}
   2360 
   2361 	$value = rest_sanitize_array( $value );
   2362 
   2363 	if ( isset( $args['items'] ) ) {
   2364 		foreach ( $value as $index => $v ) {
   2365 			$is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
   2366 			if ( is_wp_error( $is_valid ) ) {
   2367 				return $is_valid;
   2368 			}
   2369 		}
   2370 	}
   2371 
   2372 	if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) {
   2373 		return new WP_Error(
   2374 			'rest_too_few_items',
   2375 			sprintf(
   2376 				/* translators: 1: Parameter, 2: Number. */
   2377 				_n(
   2378 					'%1$s must contain at least %2$s item.',
   2379 					'%1$s must contain at least %2$s items.',
   2380 					$args['minItems']
   2381 				),
   2382 				$param,
   2383 				number_format_i18n( $args['minItems'] )
   2384 			)
   2385 		);
   2386 	}
   2387 
   2388 	if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) {
   2389 		return new WP_Error(
   2390 			'rest_too_many_items',
   2391 			sprintf(
   2392 				/* translators: 1: Parameter, 2: Number. */
   2393 				_n(
   2394 					'%1$s must contain at most %2$s item.',
   2395 					'%1$s must contain at most %2$s items.',
   2396 					$args['maxItems']
   2397 				),
   2398 				$param,
   2399 				number_format_i18n( $args['maxItems'] )
   2400 			)
   2401 		);
   2402 	}
   2403 
   2404 	if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
   2405 		/* translators: %s: Parameter. */
   2406 		return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) );
   2407 	}
   2408 
   2409 	return true;
   2410 }
   2411 
   2412 /**
   2413  * Validates a number value based on a schema.
   2414  *
   2415  * @since 5.7.0
   2416  *
   2417  * @param mixed  $value The value to validate.
   2418  * @param array  $args  Schema array to use for validation.
   2419  * @param string $param The parameter name, used in error messages.
   2420  * @return true|WP_Error
   2421  */
   2422 function rest_validate_number_value_from_schema( $value, $args, $param ) {
   2423 	if ( ! is_numeric( $value ) ) {
   2424 		return new WP_Error(
   2425 			'rest_invalid_type',
   2426 			/* translators: 1: Parameter, 2: Type name. */
   2427 			sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ),
   2428 			array( 'param' => $param )
   2429 		);
   2430 	}
   2431 
   2432 	if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) {
   2433 		return new WP_Error(
   2434 			'rest_invalid_multiple',
   2435 			/* translators: 1: Parameter, 2: Multiplier. */
   2436 			sprintf( __( '%1$s must be a multiple of %2$s.' ), $param, $args['multipleOf'] )
   2437 		);
   2438 	}
   2439 
   2440 	if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
   2441 		if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
   2442 			return new WP_Error(
   2443 				'rest_out_of_bounds',
   2444 				/* translators: 1: Parameter, 2: Minimum number. */
   2445 				sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] )
   2446 			);
   2447 		}
   2448 
   2449 		if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
   2450 			return new WP_Error(
   2451 				'rest_out_of_bounds',
   2452 				/* translators: 1: Parameter, 2: Minimum number. */
   2453 				sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] )
   2454 			);
   2455 		}
   2456 	}
   2457 
   2458 	if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
   2459 		if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
   2460 			return new WP_Error(
   2461 				'rest_out_of_bounds',
   2462 				/* translators: 1: Parameter, 2: Maximum number. */
   2463 				sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] )
   2464 			);
   2465 		}
   2466 
   2467 		if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
   2468 			return new WP_Error(
   2469 				'rest_out_of_bounds',
   2470 				/* translators: 1: Parameter, 2: Maximum number. */
   2471 				sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] )
   2472 			);
   2473 		}
   2474 	}
   2475 
   2476 	if ( isset( $args['minimum'], $args['maximum'] ) ) {
   2477 		if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
   2478 			if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
   2479 				return new WP_Error(
   2480 					'rest_out_of_bounds',
   2481 					sprintf(
   2482 						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
   2483 						__( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ),
   2484 						$param,
   2485 						$args['minimum'],
   2486 						$args['maximum']
   2487 					)
   2488 				);
   2489 			}
   2490 		}
   2491 
   2492 		if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
   2493 			if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
   2494 				return new WP_Error(
   2495 					'rest_out_of_bounds',
   2496 					sprintf(
   2497 						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
   2498 						__( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ),
   2499 						$param,
   2500 						$args['minimum'],
   2501 						$args['maximum']
   2502 					)
   2503 				);
   2504 			}
   2505 		}
   2506 
   2507 		if ( ! empty( $args['exclusiveMaximum'] ) && empty( $args['exclusiveMinimum'] ) ) {
   2508 			if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
   2509 				return new WP_Error(
   2510 					'rest_out_of_bounds',
   2511 					sprintf(
   2512 						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
   2513 						__( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ),
   2514 						$param,
   2515 						$args['minimum'],
   2516 						$args['maximum']
   2517 					)
   2518 				);
   2519 			}
   2520 		}
   2521 
   2522 		if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
   2523 			if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
   2524 				return new WP_Error(
   2525 					'rest_out_of_bounds',
   2526 					sprintf(
   2527 						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
   2528 						__( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ),
   2529 						$param,
   2530 						$args['minimum'],
   2531 						$args['maximum']
   2532 					)
   2533 				);
   2534 			}
   2535 		}
   2536 	}
   2537 
   2538 	return true;
   2539 }
   2540 
   2541 /**
   2542  * Validates a string value based on a schema.
   2543  *
   2544  * @since 5.7.0
   2545  *
   2546  * @param mixed  $value The value to validate.
   2547  * @param array  $args  Schema array to use for validation.
   2548  * @param string $param The parameter name, used in error messages.
   2549  * @return true|WP_Error
   2550  */
   2551 function rest_validate_string_value_from_schema( $value, $args, $param ) {
   2552 	if ( ! is_string( $value ) ) {
   2553 		return new WP_Error(
   2554 			'rest_invalid_type',
   2555 			/* translators: 1: Parameter, 2: Type name. */
   2556 			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ),
   2557 			array( 'param' => $param )
   2558 		);
   2559 	}
   2560 
   2561 	if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) {
   2562 		return new WP_Error(
   2563 			'rest_too_short',
   2564 			sprintf(
   2565 				/* translators: 1: Parameter, 2: Number of characters. */
   2566 				_n(
   2567 					'%1$s must be at least %2$s character long.',
   2568 					'%1$s must be at least %2$s characters long.',
   2569 					$args['minLength']
   2570 				),
   2571 				$param,
   2572 				number_format_i18n( $args['minLength'] )
   2573 			)
   2574 		);
   2575 	}
   2576 
   2577 	if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) {
   2578 		return new WP_Error(
   2579 			'rest_too_long',
   2580 			sprintf(
   2581 				/* translators: 1: Parameter, 2: Number of characters. */
   2582 				_n(
   2583 					'%1$s must be at most %2$s character long.',
   2584 					'%1$s must be at most %2$s characters long.',
   2585 					$args['maxLength']
   2586 				),
   2587 				$param,
   2588 				number_format_i18n( $args['maxLength'] )
   2589 			)
   2590 		);
   2591 	}
   2592 
   2593 	if ( isset( $args['pattern'] ) && ! rest_validate_json_schema_pattern( $args['pattern'], $value ) ) {
   2594 		return new WP_Error(
   2595 			'rest_invalid_pattern',
   2596 			/* translators: 1: Parameter, 2: Pattern. */
   2597 			sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] )
   2598 		);
   2599 	}
   2600 
   2601 	return true;
   2602 }
   2603 
   2604 /**
   2605  * Validates an integer value based on a schema.
   2606  *
   2607  * @since 5.7.0
   2608  *
   2609  * @param mixed  $value The value to validate.
   2610  * @param array  $args  Schema array to use for validation.
   2611  * @param string $param The parameter name, used in error messages.
   2612  * @return true|WP_Error
   2613  */
   2614 function rest_validate_integer_value_from_schema( $value, $args, $param ) {
   2615 	$is_valid_number = rest_validate_number_value_from_schema( $value, $args, $param );
   2616 	if ( is_wp_error( $is_valid_number ) ) {
   2617 		return $is_valid_number;
   2618 	}
   2619 
   2620 	if ( ! rest_is_integer( $value ) ) {
   2621 		return new WP_Error(
   2622 			'rest_invalid_type',
   2623 			/* translators: 1: Parameter, 2: Type name. */
   2624 			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ),
   2625 			array( 'param' => $param )
   2626 		);
   2627 	}
   2628 
   2629 	return true;
   2630 }
   2631 
   2632 /**
   2633  * Sanitize a value based on a schema.
   2634  *
   2635  * @since 4.7.0
   2636  * @since 5.5.0 Added the `$param` parameter.
   2637  * @since 5.6.0 Support the "anyOf" and "oneOf" keywords.
   2638  *
   2639  * @param mixed  $value The value to sanitize.
   2640  * @param array  $args  Schema array to use for sanitization.
   2641  * @param string $param The parameter name, used in error messages.
   2642  * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized.
   2643  */
   2644 function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
   2645 	if ( isset( $args['anyOf'] ) ) {
   2646 		$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
   2647 		if ( is_wp_error( $matching_schema ) ) {
   2648 			return $matching_schema;
   2649 		}
   2650 
   2651 		if ( ! isset( $args['type'] ) ) {
   2652 			$args['type'] = $matching_schema['type'];
   2653 		}
   2654 
   2655 		$value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
   2656 	}
   2657 
   2658 	if ( isset( $args['oneOf'] ) ) {
   2659 		$matching_schema = rest_find_one_matching_schema( $value, $args, $param );
   2660 		if ( is_wp_error( $matching_schema ) ) {
   2661 			return $matching_schema;
   2662 		}
   2663 
   2664 		if ( ! isset( $args['type'] ) ) {
   2665 			$args['type'] = $matching_schema['type'];
   2666 		}
   2667 
   2668 		$value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
   2669 	}
   2670 
   2671 	$allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
   2672 
   2673 	if ( ! isset( $args['type'] ) ) {
   2674 		/* translators: %s: Parameter. */
   2675 		_doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
   2676 	}
   2677 
   2678 	if ( is_array( $args['type'] ) ) {
   2679 		$best_type = rest_handle_multi_type_schema( $value, $args, $param );
   2680 
   2681 		if ( ! $best_type ) {
   2682 			return null;
   2683 		}
   2684 
   2685 		$args['type'] = $best_type;
   2686 	}
   2687 
   2688 	if ( ! in_array( $args['type'], $allowed_types, true ) ) {
   2689 		_doing_it_wrong(
   2690 			__FUNCTION__,
   2691 			/* translators: 1: Parameter, 2: The list of allowed types. */
   2692 			wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
   2693 			'5.5.0'
   2694 		);
   2695 	}
   2696 
   2697 	if ( 'array' === $args['type'] ) {
   2698 		$value = rest_sanitize_array( $value );
   2699 
   2700 		if ( ! empty( $args['items'] ) ) {
   2701 			foreach ( $value as $index => $v ) {
   2702 				$value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
   2703 			}
   2704 		}
   2705 
   2706 		if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
   2707 			/* translators: %s: Parameter. */
   2708 			return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) );
   2709 		}
   2710 
   2711 		return $value;
   2712 	}
   2713 
   2714 	if ( 'object' === $args['type'] ) {
   2715 		$value = rest_sanitize_object( $value );
   2716 
   2717 		foreach ( $value as $property => $v ) {
   2718 			if ( isset( $args['properties'][ $property ] ) ) {
   2719 				$value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
   2720 				continue;
   2721 			}
   2722 
   2723 			$pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args );
   2724 			if ( null !== $pattern_property_schema ) {
   2725 				$value[ $property ] = rest_sanitize_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' );
   2726 				continue;
   2727 			}
   2728 
   2729 			if ( isset( $args['additionalProperties'] ) ) {
   2730 				if ( false === $args['additionalProperties'] ) {
   2731 					unset( $value[ $property ] );
   2732 				} elseif ( is_array( $args['additionalProperties'] ) ) {
   2733 					$value[ $property ] = rest_sanitize_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
   2734 				}
   2735 			}
   2736 		}
   2737 
   2738 		return $value;
   2739 	}
   2740 
   2741 	if ( 'null' === $args['type'] ) {
   2742 		return null;
   2743 	}
   2744 
   2745 	if ( 'integer' === $args['type'] ) {
   2746 		return (int) $value;
   2747 	}
   2748 
   2749 	if ( 'number' === $args['type'] ) {
   2750 		return (float) $value;
   2751 	}
   2752 
   2753 	if ( 'boolean' === $args['type'] ) {
   2754 		return rest_sanitize_boolean( $value );
   2755 	}
   2756 
   2757 	// This behavior matches rest_validate_value_from_schema().
   2758 	if ( isset( $args['format'] )
   2759 		&& ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
   2760 	) {
   2761 		switch ( $args['format'] ) {
   2762 			case 'hex-color':
   2763 				return (string) sanitize_hex_color( $value );
   2764 
   2765 			case 'date-time':
   2766 				return sanitize_text_field( $value );
   2767 
   2768 			case 'email':
   2769 				// sanitize_email() validates, which would be unexpected.
   2770 				return sanitize_text_field( $value );
   2771 
   2772 			case 'uri':
   2773 				return esc_url_raw( $value );
   2774 
   2775 			case 'ip':
   2776 				return sanitize_text_field( $value );
   2777 
   2778 			case 'uuid':
   2779 				return sanitize_text_field( $value );
   2780 		}
   2781 	}
   2782 
   2783 	if ( 'string' === $args['type'] ) {
   2784 		return (string) $value;
   2785 	}
   2786 
   2787 	return $value;
   2788 }
   2789 
   2790 /**
   2791  * Append result of internal request to REST API for purpose of preloading data to be attached to a page.
   2792  * Expected to be called in the context of `array_reduce`.
   2793  *
   2794  * @since 5.0.0
   2795  *
   2796  * @param array  $memo Reduce accumulator.
   2797  * @param string $path REST API path to preload.
   2798  * @return array Modified reduce accumulator.
   2799  */
   2800 function rest_preload_api_request( $memo, $path ) {
   2801 	// array_reduce() doesn't support passing an array in PHP 5.2,
   2802 	// so we need to make sure we start with one.
   2803 	if ( ! is_array( $memo ) ) {
   2804 		$memo = array();
   2805 	}
   2806 
   2807 	if ( empty( $path ) ) {
   2808 		return $memo;
   2809 	}
   2810 
   2811 	$method = 'GET';
   2812 	if ( is_array( $path ) && 2 === count( $path ) ) {
   2813 		$method = end( $path );
   2814 		$path   = reset( $path );
   2815 
   2816 		if ( ! in_array( $method, array( 'GET', 'OPTIONS' ), true ) ) {
   2817 			$method = 'GET';
   2818 		}
   2819 	}
   2820 
   2821 	$path_parts = parse_url( $path );
   2822 	if ( false === $path_parts ) {
   2823 		return $memo;
   2824 	}
   2825 
   2826 	$request = new WP_REST_Request( $method, $path_parts['path'] );
   2827 	if ( ! empty( $path_parts['query'] ) ) {
   2828 		parse_str( $path_parts['query'], $query_params );
   2829 		$request->set_query_params( $query_params );
   2830 	}
   2831 
   2832 	$response = rest_do_request( $request );
   2833 	if ( 200 === $response->status ) {
   2834 		$server = rest_get_server();
   2835 		$embed  = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false;
   2836 		$data   = (array) $server->response_to_data( $response, $embed );
   2837 
   2838 		if ( 'OPTIONS' === $method ) {
   2839 			$response = rest_send_allow_header( $response, $server, $request );
   2840 
   2841 			$memo[ $method ][ $path ] = array(
   2842 				'body'    => $data,
   2843 				'headers' => $response->headers,
   2844 			);
   2845 		} else {
   2846 			$memo[ $path ] = array(
   2847 				'body'    => $data,
   2848 				'headers' => $response->headers,
   2849 			);
   2850 		}
   2851 	}
   2852 
   2853 	return $memo;
   2854 }
   2855 
   2856 /**
   2857  * Parses the "_embed" parameter into the list of resources to embed.
   2858  *
   2859  * @since 5.4.0
   2860  *
   2861  * @param string|array $embed Raw "_embed" parameter value.
   2862  * @return true|string[] Either true to embed all embeds, or a list of relations to embed.
   2863  */
   2864 function rest_parse_embed_param( $embed ) {
   2865 	if ( ! $embed || 'true' === $embed || '1' === $embed ) {
   2866 		return true;
   2867 	}
   2868 
   2869 	$rels = wp_parse_list( $embed );
   2870 
   2871 	if ( ! $rels ) {
   2872 		return true;
   2873 	}
   2874 
   2875 	return $rels;
   2876 }
   2877 
   2878 /**
   2879  * Filters the response to remove any fields not available in the given context.
   2880  *
   2881  * @since 5.5.0
   2882  * @since 5.6.0 Support the "patternProperties" keyword for objects.
   2883  *              Support the "anyOf" and "oneOf" keywords.
   2884  *
   2885  * @param array|object $data    The response data to modify.
   2886  * @param array        $schema  The schema for the endpoint used to filter the response.
   2887  * @param string       $context The requested context.
   2888  * @return array|object The filtered response data.
   2889  */
   2890 function rest_filter_response_by_context( $data, $schema, $context ) {
   2891 	if ( isset( $schema['anyOf'] ) ) {
   2892 		$matching_schema = rest_find_any_matching_schema( $data, $schema, '' );
   2893 		if ( ! is_wp_error( $matching_schema ) ) {
   2894 			if ( ! isset( $schema['type'] ) ) {
   2895 				$schema['type'] = $matching_schema['type'];
   2896 			}
   2897 
   2898 			$data = rest_filter_response_by_context( $data, $matching_schema, $context );
   2899 		}
   2900 	}
   2901 
   2902 	if ( isset( $schema['oneOf'] ) ) {
   2903 		$matching_schema = rest_find_one_matching_schema( $data, $schema, '', true );
   2904 		if ( ! is_wp_error( $matching_schema ) ) {
   2905 			if ( ! isset( $schema['type'] ) ) {
   2906 				$schema['type'] = $matching_schema['type'];
   2907 			}
   2908 
   2909 			$data = rest_filter_response_by_context( $data, $matching_schema, $context );
   2910 		}
   2911 	}
   2912 
   2913 	if ( ! is_array( $data ) && ! is_object( $data ) ) {
   2914 		return $data;
   2915 	}
   2916 
   2917 	if ( isset( $schema['type'] ) ) {
   2918 		$type = $schema['type'];
   2919 	} elseif ( isset( $schema['properties'] ) ) {
   2920 		$type = 'object'; // Back compat if a developer accidentally omitted the type.
   2921 	} else {
   2922 		return $data;
   2923 	}
   2924 
   2925 	$is_array_type  = 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) );
   2926 	$is_object_type = 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) );
   2927 
   2928 	if ( $is_array_type && $is_object_type ) {
   2929 		if ( rest_is_array( $data ) ) {
   2930 			$is_object_type = false;
   2931 		} else {
   2932 			$is_array_type = false;
   2933 		}
   2934 	}
   2935 
   2936 	$has_additional_properties = $is_object_type && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] );
   2937 
   2938 	foreach ( $data as $key => $value ) {
   2939 		$check = array();
   2940 
   2941 		if ( $is_array_type ) {
   2942 			$check = isset( $schema['items'] ) ? $schema['items'] : array();
   2943 		} elseif ( $is_object_type ) {
   2944 			if ( isset( $schema['properties'][ $key ] ) ) {
   2945 				$check = $schema['properties'][ $key ];
   2946 			} else {
   2947 				$pattern_property_schema = rest_find_matching_pattern_property_schema( $key, $schema );
   2948 				if ( null !== $pattern_property_schema ) {
   2949 					$check = $pattern_property_schema;
   2950 				} elseif ( $has_additional_properties ) {
   2951 					$check = $schema['additionalProperties'];
   2952 				}
   2953 			}
   2954 		}
   2955 
   2956 		if ( ! isset( $check['context'] ) ) {
   2957 			continue;
   2958 		}
   2959 
   2960 		if ( ! in_array( $context, $check['context'], true ) ) {
   2961 			if ( $is_array_type ) {
   2962 				// All array items share schema, so there's no need to check each one.
   2963 				$data = array();
   2964 				break;
   2965 			}
   2966 
   2967 			if ( is_object( $data ) ) {
   2968 				unset( $data->$key );
   2969 			} else {
   2970 				unset( $data[ $key ] );
   2971 			}
   2972 		} elseif ( is_array( $value ) || is_object( $value ) ) {
   2973 			$new_value = rest_filter_response_by_context( $value, $check, $context );
   2974 
   2975 			if ( is_object( $data ) ) {
   2976 				$data->$key = $new_value;
   2977 			} else {
   2978 				$data[ $key ] = $new_value;
   2979 			}
   2980 		}
   2981 	}
   2982 
   2983 	return $data;
   2984 }
   2985 
   2986 /**
   2987  * Sets the "additionalProperties" to false by default for all object definitions in the schema.
   2988  *
   2989  * @since 5.5.0
   2990  * @since 5.6.0 Support the "patternProperties" keyword.
   2991  *
   2992  * @param array $schema The schema to modify.
   2993  * @return array The modified schema.
   2994  */
   2995 function rest_default_additional_properties_to_false( $schema ) {
   2996 	$type = (array) $schema['type'];
   2997 
   2998 	if ( in_array( 'object', $type, true ) ) {
   2999 		if ( isset( $schema['properties'] ) ) {
   3000 			foreach ( $schema['properties'] as $key => $child_schema ) {
   3001 				$schema['properties'][ $key ] = rest_default_additional_properties_to_false( $child_schema );
   3002 			}
   3003 		}
   3004 
   3005 		if ( isset( $schema['patternProperties'] ) ) {
   3006 			foreach ( $schema['patternProperties'] as $key => $child_schema ) {
   3007 				$schema['patternProperties'][ $key ] = rest_default_additional_properties_to_false( $child_schema );
   3008 			}
   3009 		}
   3010 
   3011 		if ( ! isset( $schema['additionalProperties'] ) ) {
   3012 			$schema['additionalProperties'] = false;
   3013 		}
   3014 	}
   3015 
   3016 	if ( in_array( 'array', $type, true ) ) {
   3017 		if ( isset( $schema['items'] ) ) {
   3018 			$schema['items'] = rest_default_additional_properties_to_false( $schema['items'] );
   3019 		}
   3020 	}
   3021 
   3022 	return $schema;
   3023 }
   3024 
   3025 /**
   3026  * Gets the REST API route for a post.
   3027  *
   3028  * @since 5.5.0
   3029  *
   3030  * @param int|WP_Post $post Post ID or post object.
   3031  * @return string The route path with a leading slash for the given post, or an empty string if there is not a route.
   3032  */
   3033 function rest_get_route_for_post( $post ) {
   3034 	$post = get_post( $post );
   3035 
   3036 	if ( ! $post instanceof WP_Post ) {
   3037 		return '';
   3038 	}
   3039 
   3040 	$post_type = get_post_type_object( $post->post_type );
   3041 	if ( ! $post_type ) {
   3042 		return '';
   3043 	}
   3044 
   3045 	$controller = $post_type->get_rest_controller();
   3046 	if ( ! $controller ) {
   3047 		return '';
   3048 	}
   3049 
   3050 	$route = '';
   3051 
   3052 	// The only two controllers that we can detect are the Attachments and Posts controllers.
   3053 	if ( in_array( get_class( $controller ), array( 'WP_REST_Attachments_Controller', 'WP_REST_Posts_Controller' ), true ) ) {
   3054 		$namespace = 'wp/v2';
   3055 		$rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;
   3056 		$route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $post->ID );
   3057 	}
   3058 
   3059 	/**
   3060 	 * Filters the REST API route for a post.
   3061 	 *
   3062 	 * @since 5.5.0
   3063 	 *
   3064 	 * @param string  $route The route path.
   3065 	 * @param WP_Post $post  The post object.
   3066 	 */
   3067 	return apply_filters( 'rest_route_for_post', $route, $post );
   3068 }
   3069 
   3070 /**
   3071  * Gets the REST API route for a term.
   3072  *
   3073  * @since 5.5.0
   3074  *
   3075  * @param int|WP_Term $term Term ID or term object.
   3076  * @return string The route path with a leading slash for the given term, or an empty string if there is not a route.
   3077  */
   3078 function rest_get_route_for_term( $term ) {
   3079 	$term = get_term( $term );
   3080 
   3081 	if ( ! $term instanceof WP_Term ) {
   3082 		return '';
   3083 	}
   3084 
   3085 	$taxonomy = get_taxonomy( $term->taxonomy );
   3086 	if ( ! $taxonomy ) {
   3087 		return '';
   3088 	}
   3089 
   3090 	$controller = $taxonomy->get_rest_controller();
   3091 	if ( ! $controller ) {
   3092 		return '';
   3093 	}
   3094 
   3095 	$route = '';
   3096 
   3097 	// The only controller that works is the Terms controller.
   3098 	if ( $controller instanceof WP_REST_Terms_Controller ) {
   3099 		$namespace = 'wp/v2';
   3100 		$rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
   3101 		$route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id );
   3102 	}
   3103 
   3104 	/**
   3105 	 * Filters the REST API route for a term.
   3106 	 *
   3107 	 * @since 5.5.0
   3108 	 *
   3109 	 * @param string  $route The route path.
   3110 	 * @param WP_Term $term  The term object.
   3111 	 */
   3112 	return apply_filters( 'rest_route_for_term', $route, $term );
   3113 }
   3114 
   3115 /**
   3116  * Gets the REST route for the currently queried object.
   3117  *
   3118  * @since 5.5.0
   3119  *
   3120  * @return string The REST route of the resource, or an empty string if no resource identified.
   3121  */
   3122 function rest_get_queried_resource_route() {
   3123 	if ( is_singular() ) {
   3124 		$route = rest_get_route_for_post( get_queried_object() );
   3125 	} elseif ( is_category() || is_tag() || is_tax() ) {
   3126 		$route = rest_get_route_for_term( get_queried_object() );
   3127 	} elseif ( is_author() ) {
   3128 		$route = '/wp/v2/users/' . get_queried_object_id();
   3129 	} else {
   3130 		$route = '';
   3131 	}
   3132 
   3133 	/**
   3134 	 * Filters the REST route for the currently queried object.
   3135 	 *
   3136 	 * @since 5.5.0
   3137 	 *
   3138 	 * @param string $link The route with a leading slash, or an empty string.
   3139 	 */
   3140 	return apply_filters( 'rest_queried_resource_route', $route );
   3141 }
   3142 
   3143 /**
   3144  * Retrieves an array of endpoint arguments from the item schema and endpoint method.
   3145  *
   3146  * @since 5.6.0
   3147  *
   3148  * @param array  $schema The full JSON schema for the endpoint.
   3149  * @param string $method Optional. HTTP method of the endpoint. The arguments for `CREATABLE` endpoints are
   3150  *                       checked for required values and may fall-back to a given default, this is not done
   3151  *                       on `EDITABLE` endpoints. Default WP_REST_Server::CREATABLE.
   3152  * @return array The endpoint arguments.
   3153  */
   3154 function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::CREATABLE ) {
   3155 
   3156 	$schema_properties       = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
   3157 	$endpoint_args           = array();
   3158 	$valid_schema_properties = rest_get_allowed_schema_keywords();
   3159 	$valid_schema_properties = array_diff( $valid_schema_properties, array( 'default', 'required' ) );
   3160 
   3161 	foreach ( $schema_properties as $field_id => $params ) {
   3162 
   3163 		// Arguments specified as `readonly` are not allowed to be set.
   3164 		if ( ! empty( $params['readonly'] ) ) {
   3165 			continue;
   3166 		}
   3167 
   3168 		$endpoint_args[ $field_id ] = array(
   3169 			'validate_callback' => 'rest_validate_request_arg',
   3170 			'sanitize_callback' => 'rest_sanitize_request_arg',
   3171 		);
   3172 
   3173 		if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
   3174 			$endpoint_args[ $field_id ]['default'] = $params['default'];
   3175 		}
   3176 
   3177 		if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
   3178 			$endpoint_args[ $field_id ]['required'] = true;
   3179 		}
   3180 
   3181 		foreach ( $valid_schema_properties as $schema_prop ) {
   3182 			if ( isset( $params[ $schema_prop ] ) ) {
   3183 				$endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
   3184 			}
   3185 		}
   3186 
   3187 		// Merge in any options provided by the schema property.
   3188 		if ( isset( $params['arg_options'] ) ) {
   3189 
   3190 			// Only use required / default from arg_options on CREATABLE endpoints.
   3191 			if ( WP_REST_Server::CREATABLE !== $method ) {
   3192 				$params['arg_options'] = array_diff_key(
   3193 					$params['arg_options'],
   3194 					array(
   3195 						'required' => '',
   3196 						'default'  => '',
   3197 					)
   3198 				);
   3199 			}
   3200 
   3201 			$endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
   3202 		}
   3203 	}
   3204 
   3205 	return $endpoint_args;
   3206 }
   3207 
   3208 
   3209 /**
   3210  * Converts an error to a response object.
   3211  *
   3212  * This iterates over all error codes and messages to change it into a flat
   3213  * array. This enables simpler client behaviour, as it is represented as a
   3214  * list in JSON rather than an object/map.
   3215  *
   3216  * @since 5.7.0
   3217  *
   3218  * @param WP_Error $error WP_Error instance.
   3219  *
   3220  * @return WP_REST_Response List of associative arrays with code and message keys.
   3221  */
   3222 function rest_convert_error_to_response( $error ) {
   3223 	$status = array_reduce(
   3224 		$error->get_all_error_data(),
   3225 		function ( $status, $error_data ) {
   3226 			return is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : $status;
   3227 		},
   3228 		500
   3229 	);
   3230 
   3231 	$errors = array();
   3232 
   3233 	foreach ( (array) $error->errors as $code => $messages ) {
   3234 		$all_data  = $error->get_all_error_data( $code );
   3235 		$last_data = array_pop( $all_data );
   3236 
   3237 		foreach ( (array) $messages as $message ) {
   3238 			$formatted = array(
   3239 				'code'    => $code,
   3240 				'message' => $message,
   3241 				'data'    => $last_data,
   3242 			);
   3243 
   3244 			if ( $all_data ) {
   3245 				$formatted['additional_data'] = $all_data;
   3246 			}
   3247 
   3248 			$errors[] = $formatted;
   3249 		}
   3250 	}
   3251 
   3252 	$data = $errors[0];
   3253 	if ( count( $errors ) > 1 ) {
   3254 		// Remove the primary error.
   3255 		array_shift( $errors );
   3256 		$data['additional_errors'] = $errors;
   3257 	}
   3258 
   3259 	return new WP_REST_Response( $data, $status );
   3260 }