Issues (4967)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/wp-includes/rest-api.php (13 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
 * @since 4.4.0
21
 *
22
 * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
23
 *
24
 * @param string $namespace The first URL segment after core prefix. Should be unique to your package/plugin.
25
 * @param string $route     The base URL for route you are adding.
26
 * @param array  $args      Optional. Either an array of options for the endpoint, or an array of arrays for
27
 *                          multiple methods. Default empty array.
28
 * @param bool   $override  Optional. If the route already exists, should we override it? True overrides,
29
 *                          false merges (with newer overriding if duplicate keys exist). Default false.
30
 * @return bool True on success, false on error.
31
 */
32
function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
33
	/** @var WP_REST_Server $wp_rest_server */
34
	global $wp_rest_server;
35
36
	if ( empty( $namespace ) ) {
37
		/*
38
		 * Non-namespaced routes are not allowed, with the exception of the main
39
		 * and namespace indexes. If you really need to register a
40
		 * non-namespaced route, call `WP_REST_Server::register_route` directly.
41
		 */
42
		_doing_it_wrong( 'register_rest_route', __( 'Routes must be namespaced with plugin or theme name and version.' ), '4.4.0' );
43
		return false;
44
	} else if ( empty( $route ) ) {
45
		_doing_it_wrong( 'register_rest_route', __( 'Route must be specified.' ), '4.4.0' );
46
		return false;
47
	}
48
49
	if ( isset( $args['args'] ) ) {
50
		$common_args = $args['args'];
51
		unset( $args['args'] );
52
	} else {
53
		$common_args = array();
54
	}
55
56
	if ( isset( $args['callback'] ) ) {
57
		// Upgrade a single set to multiple.
58
		$args = array( $args );
59
	}
60
61
	$defaults = array(
62
		'methods'         => 'GET',
63
		'callback'        => null,
64
		'args'            => array(),
65
	);
66
	foreach ( $args as $key => &$arg_group ) {
67
		if ( ! is_numeric( $key ) ) {
68
			// Route option, skip here.
69
			continue;
70
		}
71
72
		$arg_group = array_merge( $defaults, $arg_group );
73
		$arg_group['args'] = array_merge( $common_args, $arg_group['args'] );
74
	}
75
76
	$full_route = '/' . trim( $namespace, '/' ) . '/' . trim( $route, '/' );
77
	$wp_rest_server->register_route( $namespace, $full_route, $args, $override );
78
	return true;
79
}
80
81
/**
82
 * Registers a new field on an existing WordPress object type.
83
 *
84
 * @since 4.7.0
85
 *
86
 * @global array $wp_rest_additional_fields Holds registered fields, organized
87
 *                                          by object type.
88
 *
89
 * @param string|array $object_type Object(s) the field is being registered
90
 *                                  to, "post"|"term"|"comment" etc.
91
 * @param string $attribute         The attribute name.
92
 * @param array  $args {
93
 *     Optional. An array of arguments used to handle the registered field.
94
 *
95
 *     @type string|array|null $get_callback    Optional. The callback function used to retrieve the field
96
 *                                              value. Default is 'null', the field will not be returned in
97
 *                                              the response.
98
 *     @type string|array|null $update_callback Optional. The callback function used to set and update the
99
 *                                              field value. Default is 'null', the value cannot be set or
100
 *                                              updated.
101
 *     @type string|array|null $schema          Optional. The callback function used to create the schema for
102
 *                                              this field. Default is 'null', no schema entry will be returned.
103
 * }
104
 */
105
function register_rest_field( $object_type, $attribute, $args = array() ) {
106
	$defaults = array(
107
		'get_callback'    => null,
108
		'update_callback' => null,
109
		'schema'          => null,
110
	);
111
112
	$args = wp_parse_args( $args, $defaults );
113
114
	global $wp_rest_additional_fields;
115
116
	$object_types = (array) $object_type;
117
118
	foreach ( $object_types as $object_type ) {
119
		$wp_rest_additional_fields[ $object_type ][ $attribute ] = $args;
120
	}
121
}
122
123
/**
124
 * Registers rewrite rules for the API.
125
 *
126
 * @since 4.4.0
127
 *
128
 * @see rest_api_register_rewrites()
129
 * @global WP $wp Current WordPress environment instance.
130
 */
131
function rest_api_init() {
132
	rest_api_register_rewrites();
133
134
	global $wp;
135
	$wp->add_query_var( 'rest_route' );
136
}
137
138
/**
139
 * Adds REST rewrite rules.
140
 *
141
 * @since 4.4.0
142
 *
143
 * @see add_rewrite_rule()
144
 * @global WP_Rewrite $wp_rewrite
145
 */
146
function rest_api_register_rewrites() {
147
	global $wp_rewrite;
148
149
	add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' );
150
	add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?','index.php?rest_route=/$matches[1]','top' );
151
	add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' );
152
	add_rewrite_rule( '^' . $wp_rewrite->index . '/' . rest_get_url_prefix() . '/(.*)?','index.php?rest_route=/$matches[1]','top' );
153
}
154
155
/**
156
 * Registers the default REST API filters.
157
 *
158
 * Attached to the {@see 'rest_api_init'} action
159
 * to make testing and disabling these filters easier.
160
 *
161
 * @since 4.4.0
162
 */
163
function rest_api_default_filters() {
164
	// Deprecated reporting.
165
	add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
166
	add_filter( 'deprecated_function_trigger_error', '__return_false' );
167
	add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
168
	add_filter( 'deprecated_argument_trigger_error', '__return_false' );
169
170
	// Default serving.
171
	add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
172
	add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
173
174
	add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
175
}
176
177
/**
178
 * Registers default REST API routes.
179
 *
180
 * @since 4.7.0
181
 */
182
function create_initial_rest_routes() {
183
	foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
184
		$class = ! empty( $post_type->rest_controller_class ) ? $post_type->rest_controller_class : 'WP_REST_Posts_Controller';
185
186
		if ( ! class_exists( $class ) ) {
187
			continue;
188
		}
189
		$controller = new $class( $post_type->name );
190
		if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
191
			continue;
192
		}
193
194
		$controller->register_routes();
195
196
		if ( post_type_supports( $post_type->name, 'revisions' ) ) {
197
			$revisions_controller = new WP_REST_Revisions_Controller( $post_type->name );
198
			$revisions_controller->register_routes();
199
		}
200
	}
201
202
	// Post types.
203
	$controller = new WP_REST_Post_Types_Controller;
204
	$controller->register_routes();
205
206
	// Post statuses.
207
	$controller = new WP_REST_Post_Statuses_Controller;
208
	$controller->register_routes();
209
210
	// Taxonomies.
211
	$controller = new WP_REST_Taxonomies_Controller;
212
	$controller->register_routes();
213
214
	// Terms.
215
	foreach ( get_taxonomies( array( 'show_in_rest' => true ), 'object' ) as $taxonomy ) {
216
		$class = ! empty( $taxonomy->rest_controller_class ) ? $taxonomy->rest_controller_class : 'WP_REST_Terms_Controller';
217
218
		if ( ! class_exists( $class ) ) {
219
			continue;
220
		}
221
		$controller = new $class( $taxonomy->name );
222
		if ( ! is_subclass_of( $controller, 'WP_REST_Controller' ) ) {
223
			continue;
224
		}
225
226
		$controller->register_routes();
227
	}
228
229
	// Users.
230
	$controller = new WP_REST_Users_Controller;
231
	$controller->register_routes();
232
233
	// Comments.
234
	$controller = new WP_REST_Comments_Controller;
235
	$controller->register_routes();
236
237
	// Settings.
238
	$controller = new WP_REST_Settings_Controller;
239
	$controller->register_routes();
240
}
241
242
/**
243
 * Loads the REST API.
244
 *
245
 * @since 4.4.0
246
 *
247
 * @global WP             $wp             Current WordPress environment instance.
248
 * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
249
 */
250
function rest_api_loaded() {
251
	if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
252
		return;
253
	}
254
255
	/**
256
	 * Whether this is a REST Request.
257
	 *
258
	 * @since 4.4.0
259
	 * @var bool
260
	 */
261
	define( 'REST_REQUEST', true );
262
263
	// Initialize the server.
264
	$server = rest_get_server();
265
266
	// Fire off the request.
267
	$route = untrailingslashit( $GLOBALS['wp']->query_vars['rest_route'] );
268
	if ( empty( $route ) ) {
269
		$route = '/';
270
	}
271
	$server->serve_request( $route );
272
273
	// We're done.
274
	die();
0 ignored issues
show
Coding Style Compatibility introduced by
The function rest_api_loaded() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
275
}
276
277
/**
278
 * Retrieves the URL prefix for any API resource.
279
 *
280
 * @since 4.4.0
281
 *
282
 * @return string Prefix.
283
 */
284
function rest_get_url_prefix() {
285
	/**
286
	 * Filters the REST URL prefix.
287
	 *
288
	 * @since 4.4.0
289
	 *
290
	 * @param string $prefix URL prefix. Default 'wp-json'.
291
	 */
292
	return apply_filters( 'rest_url_prefix', 'wp-json' );
293
}
294
295
/**
296
 * Retrieves the URL to a REST endpoint on a site.
297
 *
298
 * Note: The returned URL is NOT escaped.
299
 *
300
 * @since 4.4.0
301
 *
302
 * @todo Check if this is even necessary
303
 * @global WP_Rewrite $wp_rewrite
304
 *
305
 * @param int    $blog_id Optional. Blog ID. Default of null returns URL for current blog.
306
 * @param string $path    Optional. REST route. Default '/'.
307
 * @param string $scheme  Optional. Sanitization scheme. Default 'rest'.
308
 * @return string Full URL to the endpoint.
309
 */
310
function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) {
311
	if ( empty( $path ) ) {
312
		$path = '/';
313
	}
314
315
	if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) {
316
		global $wp_rewrite;
317
318
		if ( $wp_rewrite->using_index_permalinks() ) {
319
			$url = get_home_url( $blog_id, $wp_rewrite->index . '/' . rest_get_url_prefix(), $scheme );
320
		} else {
321
			$url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme );
322
		}
323
324
		$url .= '/' . ltrim( $path, '/' );
325
	} else {
326
		$url = trailingslashit( get_home_url( $blog_id, '', $scheme ) );
327
328
		$path = '/' . ltrim( $path, '/' );
329
330
		$url = add_query_arg( 'rest_route', $path, $url );
331
	}
332
333
	if ( is_ssl() ) {
334
		// If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS.
335
		if ( $_SERVER['SERVER_NAME'] === parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) ) {
336
			$url = set_url_scheme( $url, 'https' );
337
		}
338
	}
339
340
	if ( is_admin() && force_ssl_admin() ) {
341
		// In this situation the home URL may be http:, and `is_ssl()` may be
342
		// false, but the admin is served over https: (one way or another), so
343
		// REST API usage will be blocked by browsers unless it is also served
344
		// over HTTPS.
345
		$url = set_url_scheme( $url, 'https' );
346
	}
347
348
	/**
349
	 * Filters the REST URL.
350
	 *
351
	 * Use this filter to adjust the url returned by the get_rest_url() function.
352
	 *
353
	 * @since 4.4.0
354
	 *
355
	 * @param string $url     REST URL.
356
	 * @param string $path    REST route.
357
	 * @param int    $blog_id Blog ID.
358
	 * @param string $scheme  Sanitization scheme.
359
	 */
360
	return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
361
}
362
363
/**
364
 * Retrieves the URL to a REST endpoint.
365
 *
366
 * Note: The returned URL is NOT escaped.
367
 *
368
 * @since 4.4.0
369
 *
370
 * @param string $path   Optional. REST route. Default empty.
371
 * @param string $scheme Optional. Sanitization scheme. Default 'json'.
372
 * @return string Full URL to the endpoint.
373
 */
374
function rest_url( $path = '', $scheme = 'json' ) {
375
	return get_rest_url( null, $path, $scheme );
376
}
377
378
/**
379
 * Do a REST request.
380
 *
381
 * Used primarily to route internal requests through WP_REST_Server.
382
 *
383
 * @since 4.4.0
384
 *
385
 * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
386
 *
387
 * @param WP_REST_Request|string $request Request.
388
 * @return WP_REST_Response REST response.
389
 */
390
function rest_do_request( $request ) {
391
	$request = rest_ensure_request( $request );
0 ignored issues
show
It seems like $request can also be of type string; however, rest_ensure_request() does only seem to accept array|object<WP_REST_Request>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
392
	return rest_get_server()->dispatch( $request );
393
}
394
395
/**
396
 * Retrieves the current REST server instance.
397
 *
398
 * Instantiates a new instance if none exists already.
399
 *
400
 * @since 4.5.0
401
 *
402
 * @global WP_REST_Server $wp_rest_server REST server instance.
403
 *
404
 * @return WP_REST_Server REST server instance.
405
 */
406
function rest_get_server() {
407
	/* @var WP_REST_Server $wp_rest_server */
408
	global $wp_rest_server;
409
410
	if ( empty( $wp_rest_server ) ) {
411
		/**
412
		 * Filters the REST Server Class.
413
		 *
414
		 * This filter allows you to adjust the server class used by the API, using a
415
		 * different class to handle requests.
416
		 *
417
		 * @since 4.4.0
418
		 *
419
		 * @param string $class_name The name of the server class. Default 'WP_REST_Server'.
420
		 */
421
		$wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' );
422
		$wp_rest_server = new $wp_rest_server_class;
423
424
		/**
425
		 * Fires when preparing to serve an API request.
426
		 *
427
		 * Endpoint objects should be created and register their hooks on this action rather
428
		 * than another action to ensure they're only loaded when needed.
429
		 *
430
		 * @since 4.4.0
431
		 *
432
		 * @param WP_REST_Server $wp_rest_server Server object.
433
		 */
434
		do_action( 'rest_api_init', $wp_rest_server );
435
	}
436
437
	return $wp_rest_server;
438
}
439
440
/**
441
 * Ensures request arguments are a request object (for consistency).
442
 *
443
 * @since 4.4.0
444
 *
445
 * @param array|WP_REST_Request $request Request to check.
446
 * @return WP_REST_Request REST request instance.
447
 */
448
function rest_ensure_request( $request ) {
449
	if ( $request instanceof WP_REST_Request ) {
450
		return $request;
451
	}
452
453
	return new WP_REST_Request( 'GET', '', $request );
454
}
455
456
/**
457
 * Ensures a REST response is a response object (for consistency).
458
 *
459
 * This implements WP_HTTP_Response, allowing usage of `set_status`/`header`/etc
460
 * without needing to double-check the object. Will also allow WP_Error to indicate error
461
 * responses, so users should immediately check for this value.
462
 *
463
 * @since 4.4.0
464
 *
465
 * @param WP_Error|WP_HTTP_Response|mixed $response Response to check.
466
 * @return WP_REST_Response|mixed If response generated an error, WP_Error, if response
467
 *                                is already an instance, WP_HTTP_Response, otherwise
468
 *                                returns a new WP_REST_Response instance.
469
 */
470
function rest_ensure_response( $response ) {
471
	if ( is_wp_error( $response ) ) {
472
		return $response;
473
	}
474
475
	if ( $response instanceof WP_HTTP_Response ) {
476
		return $response;
477
	}
478
479
	return new WP_REST_Response( $response );
480
}
481
482
/**
483
 * Handles _deprecated_function() errors.
484
 *
485
 * @since 4.4.0
486
 *
487
 * @param string $function    The function that was called.
488
 * @param string $replacement The function that should have been called.
489
 * @param string $version     Version.
490
 */
491 View Code Duplication
function rest_handle_deprecated_function( $function, $replacement, $version ) {
0 ignored issues
show
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
492
	if ( ! WP_DEBUG || headers_sent() ) {
493
		return;
494
	}
495
	if ( ! empty( $replacement ) ) {
496
		/* translators: 1: function name, 2: WordPress version number, 3: new function name */
497
		$string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement );
498
	} else {
499
		/* translators: 1: function name, 2: WordPress version number */
500
		$string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
501
	}
502
503
	header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) );
504
}
505
506
/**
507
 * Handles _deprecated_argument() errors.
508
 *
509
 * @since 4.4.0
510
 *
511
 * @param string $function    The function that was called.
512
 * @param string $message     A message regarding the change.
513
 * @param string $version     Version.
514
 */
515 View Code Duplication
function rest_handle_deprecated_argument( $function, $message, $version ) {
0 ignored issues
show
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
516
	if ( ! WP_DEBUG || headers_sent() ) {
517
		return;
518
	}
519
	if ( ! empty( $message ) ) {
520
		/* translators: 1: function name, 2: WordPress version number, 3: error message */
521
		$string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $message );
522
	} else {
523
		/* translators: 1: function name, 2: WordPress version number */
524
		$string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
525
	}
526
527
	header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
528
}
529
530
/**
531
 * Sends Cross-Origin Resource Sharing headers with API requests.
532
 *
533
 * @since 4.4.0
534
 *
535
 * @param mixed $value Response data.
536
 * @return mixed Response data.
537
 */
538
function rest_send_cors_headers( $value ) {
539
	$origin = get_http_origin();
540
541
	if ( $origin ) {
542
		// Requests from file:// and data: URLs send "Origin: null"
543
		if ( 'null' !== $origin ) {
544
			$origin = esc_url_raw( $origin );
545
		}
546
		header( 'Access-Control-Allow-Origin: ' . $origin );
547
		header( 'Access-Control-Allow-Methods: OPTIONS, GET, POST, PUT, PATCH, DELETE' );
548
		header( 'Access-Control-Allow-Credentials: true' );
549
		header( 'Vary: Origin' );
550
	}
551
552
	return $value;
553
}
554
555
/**
556
 * Handles OPTIONS requests for the server.
557
 *
558
 * This is handled outside of the server code, as it doesn't obey normal route
559
 * mapping.
560
 *
561
 * @since 4.4.0
562
 *
563
 * @param mixed           $response Current response, either response or `null` to indicate pass-through.
564
 * @param WP_REST_Server  $handler  ResponseHandler instance (usually WP_REST_Server).
565
 * @param WP_REST_Request $request  The request that was used to make current response.
566
 * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through.
567
 */
568
function rest_handle_options_request( $response, $handler, $request ) {
569
	if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) {
570
		return $response;
571
	}
572
573
	$response = new WP_REST_Response();
574
	$data = array();
575
576
	foreach ( $handler->get_routes() as $route => $endpoints ) {
577
		$match = preg_match( '@^' . $route . '$@i', $request->get_route() );
578
579
		if ( ! $match ) {
580
			continue;
581
		}
582
583
		$data = $handler->get_data_for_route( $route, $endpoints, 'help' );
584
		$response->set_matched_route( $route );
585
		break;
586
	}
587
588
	$response->set_data( $data );
589
	return $response;
590
}
591
592
/**
593
 * Sends the "Allow" header to state all methods that can be sent to the current route.
594
 *
595
 * @since 4.4.0
596
 *
597
 * @param WP_REST_Response $response Current response being served.
598
 * @param WP_REST_Server   $server   ResponseHandler instance (usually WP_REST_Server).
599
 * @param WP_REST_Request  $request  The request that was used to make current response.
600
 * @return WP_REST_Response Response to be served, with "Allow" header if route has allowed methods.
601
 */
602
function rest_send_allow_header( $response, $server, $request ) {
603
	$matched_route = $response->get_matched_route();
604
605
	if ( ! $matched_route ) {
606
		return $response;
607
	}
608
609
	$routes = $server->get_routes();
610
611
	$allowed_methods = array();
612
613
	// Get the allowed methods across the routes.
614
	foreach ( $routes[ $matched_route ] as $_handler ) {
615
		foreach ( $_handler['methods'] as $handler_method => $value ) {
616
617
			if ( ! empty( $_handler['permission_callback'] ) ) {
618
619
				$permission = call_user_func( $_handler['permission_callback'], $request );
620
621
				$allowed_methods[ $handler_method ] = true === $permission;
622
			} else {
623
				$allowed_methods[ $handler_method ] = true;
624
			}
625
		}
626
	}
627
628
	// Strip out all the methods that are not allowed (false values).
629
	$allowed_methods = array_filter( $allowed_methods );
630
631
	if ( $allowed_methods ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allowed_methods of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
632
		$response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) );
633
	}
634
635
	return $response;
636
}
637
638
/**
639
 * Adds the REST API URL to the WP RSD endpoint.
640
 *
641
 * @since 4.4.0
642
 *
643
 * @see get_rest_url()
644
 */
645
function rest_output_rsd() {
646
	$api_root = get_rest_url();
647
648
	if ( empty( $api_root ) ) {
649
		return;
650
	}
651
	?>
652
	<api name="WP-API" blogID="1" preferred="false" apiLink="<?php echo esc_url( $api_root ); ?>" />
653
	<?php
654
}
655
656
/**
657
 * Outputs the REST API link tag into page header.
658
 *
659
 * @since 4.4.0
660
 *
661
 * @see get_rest_url()
662
 */
663
function rest_output_link_wp_head() {
664
	$api_root = get_rest_url();
665
666
	if ( empty( $api_root ) ) {
667
		return;
668
	}
669
670
	echo "<link rel='https://api.w.org/' href='" . esc_url( $api_root ) . "' />\n";
671
}
672
673
/**
674
 * Sends a Link header for the REST API.
675
 *
676
 * @since 4.4.0
677
 */
678
function rest_output_link_header() {
679
	if ( headers_sent() ) {
680
		return;
681
	}
682
683
	$api_root = get_rest_url();
684
685
	if ( empty( $api_root ) ) {
686
		return;
687
	}
688
689
	header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"', false );
690
}
691
692
/**
693
 * Checks for errors when using cookie-based authentication.
694
 *
695
 * WordPress' built-in cookie authentication is always active
696
 * for logged in users. However, the API has to check nonces
697
 * for each request to ensure users are not vulnerable to CSRF.
698
 *
699
 * @since 4.4.0
700
 *
701
 * @global mixed          $wp_rest_auth_cookie
702
 * @global WP_REST_Server $wp_rest_server      REST server instance.
703
 *
704
 * @param WP_Error|mixed $result Error from another authentication handler,
705
 *                               null if we should handle it, or another value
706
 *                               if not.
707
 * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true.
708
 */
709
function rest_cookie_check_errors( $result ) {
710
	if ( ! empty( $result ) ) {
711
		return $result;
712
	}
713
714
	global $wp_rest_auth_cookie, $wp_rest_server;
715
716
	/*
717
	 * Is cookie authentication being used? (If we get an auth
718
	 * error, but we're still logged in, another authentication
719
	 * must have been used).
720
	 */
721
	if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
722
		return $result;
723
	}
724
725
	// Determine if there is a nonce.
726
	$nonce = null;
727
728
	if ( isset( $_REQUEST['_wpnonce'] ) ) {
729
		$nonce = $_REQUEST['_wpnonce'];
730
	} elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
731
		$nonce = $_SERVER['HTTP_X_WP_NONCE'];
732
	}
733
734
	if ( null === $nonce ) {
735
		// No nonce at all, so act as if it's an unauthenticated request.
736
		wp_set_current_user( 0 );
737
		return true;
738
	}
739
740
	// Check the nonce.
741
	$result = wp_verify_nonce( $nonce, 'wp_rest' );
742
743
	if ( ! $result ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type false|integer is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
744
		return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
745
	}
746
747
	// Send a refreshed nonce in header.
748
	$wp_rest_server->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) );
749
750
	return true;
751
}
752
753
/**
754
 * Collects cookie authentication status.
755
 *
756
 * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors.
757
 *
758
 * @since 4.4.0
759
 *
760
 * @see current_action()
761
 * @global mixed $wp_rest_auth_cookie
762
 */
763
function rest_cookie_collect_status() {
764
	global $wp_rest_auth_cookie;
765
766
	$status_type = current_action();
767
768
	if ( 'auth_cookie_valid' !== $status_type ) {
769
		$wp_rest_auth_cookie = substr( $status_type, 12 );
770
		return;
771
	}
772
773
	$wp_rest_auth_cookie = true;
774
}
775
776
/**
777
 * Parses an RFC3339 time into a Unix timestamp.
778
 *
779
 * @since 4.4.0
780
 *
781
 * @param string $date      RFC3339 timestamp.
782
 * @param bool   $force_utc Optional. Whether to force UTC timezone instead of using
783
 *                          the timestamp's timezone. Default false.
784
 * @return int Unix timestamp.
0 ignored issues
show
Should the return type not be false|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
785
 */
786
function rest_parse_date( $date, $force_utc = false ) {
787
	if ( $force_utc ) {
788
		$date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
789
	}
790
791
	$regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
792
793
	if ( ! preg_match( $regex, $date, $matches ) ) {
794
		return false;
795
	}
796
797
	return strtotime( $date );
798
}
799
800
/**
801
 * Parses a date into both its local and UTC equivalent, in MySQL datetime format.
802
 *
803
 * @since 4.4.0
804
 *
805
 * @see rest_parse_date()
806
 *
807
 * @param string $date   RFC3339 timestamp.
808
 * @param bool   $is_utc Whether the provided date should be interpreted as UTC. Default false.
809
 * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
0 ignored issues
show
Consider making the return type a bit more specific; maybe use null|string[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
810
 *                    null on failure.
811
 */
812
function rest_get_date_with_gmt( $date, $is_utc = false ) {
813
	// Whether or not the original date actually has a timezone string
814
	// changes the way we need to do timezone conversion.  Store this info
815
	// before parsing the date, and use it later.
816
	$has_timezone = preg_match( '#(Z|[+-]\d{2}(:\d{2})?)$#', $date );
817
818
	$date = rest_parse_date( $date );
819
820
	if ( empty( $date ) ) {
821
		return null;
822
	}
823
824
	// At this point $date could either be a local date (if we were passed a
825
	// *local* date without a timezone offset) or a UTC date (otherwise).
826
	// Timezone conversion needs to be handled differently between these two
827
	// cases.
828
	if ( ! $is_utc && ! $has_timezone ) {
829
		$local = date( 'Y-m-d H:i:s', $date );
830
		$utc = get_gmt_from_date( $local );
831
	} else {
832
		$utc = date( 'Y-m-d H:i:s', $date );
833
		$local = get_date_from_gmt( $utc );
834
	}
835
836
	return array( $local, $utc );
837
}
838
839
/**
840
 * Returns a contextual HTTP error code for authorization failure.
841
 *
842
 * @since 4.7.0
843
 *
844
 * @return integer 401 if the user is not logged in, 403 if the user is logged in.
845
 */
846
function rest_authorization_required_code() {
847
	return is_user_logged_in() ? 403 : 401;
848
}
849
850
/**
851
 * Validate a request argument based on details registered to the route.
852
 *
853
 * @since 4.7.0
854
 *
855
 * @param  mixed            $value
856
 * @param  WP_REST_Request  $request
857
 * @param  string           $param
858
 * @return WP_Error|boolean
0 ignored issues
show
Should the return type not be boolean|WP_Error|true?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
859
 */
860 View Code Duplication
function rest_validate_request_arg( $value, $request, $param ) {
0 ignored issues
show
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
861
	$attributes = $request->get_attributes();
862
	if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
863
		return true;
864
	}
865
	$args = $attributes['args'][ $param ];
866
867
	return rest_validate_value_from_schema( $value, $args, $param );
868
}
869
870
/**
871
 * Sanitize a request argument based on details registered to the route.
872
 *
873
 * @since 4.7.0
874
 *
875
 * @param  mixed            $value
876
 * @param  WP_REST_Request  $request
877
 * @param  string           $param
878
 * @return mixed
879
 */
880 View Code Duplication
function rest_sanitize_request_arg( $value, $request, $param ) {
0 ignored issues
show
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
881
	$attributes = $request->get_attributes();
882
	if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) {
883
		return $value;
884
	}
885
	$args = $attributes['args'][ $param ];
886
887
	return rest_sanitize_value_from_schema( $value, $args );
888
}
889
890
/**
891
 * Parse a request argument based on details registered to the route.
892
 *
893
 * Runs a validation check and sanitizes the value, primarily to be used via
894
 * the `sanitize_callback` arguments in the endpoint args registration.
895
 *
896
 * @since 4.7.0
897
 *
898
 * @param  mixed            $value
899
 * @param  WP_REST_Request  $request
900
 * @param  string           $param
901
 * @return mixed
902
 */
903 View Code Duplication
function rest_parse_request_arg( $value, $request, $param ) {
0 ignored issues
show
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
904
	$is_valid = rest_validate_request_arg( $value, $request, $param );
905
906
	if ( is_wp_error( $is_valid ) ) {
907
		return $is_valid;
908
	}
909
910
	$value = rest_sanitize_request_arg( $value, $request, $param );
911
912
	return $value;
913
}
914
915
/**
916
 * Determines if an IP address is valid.
917
 *
918
 * Handles both IPv4 and IPv6 addresses.
919
 *
920
 * @since 4.7.0
921
 *
922
 * @param  string $ip IP address.
923
 * @return string|false The valid IP address, otherwise false.
924
 */
925
function rest_is_ip_address( $ip ) {
926
	$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]?)$/';
927
928
	if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) {
929
		return false;
930
	}
931
932
	return $ip;
933
}
934
935
/**
936
 * Changes a boolean-like value into the proper boolean value.
937
 *
938
 * @since 4.7.0
939
 *
940
 * @param bool|string|int $value The value being evaluated.
941
 * @return boolean Returns the proper associated boolean value.
942
 */
943
function rest_sanitize_boolean( $value ) {
944
	// String values are translated to `true`; make sure 'false' is false.
945
	if ( is_string( $value )  ) {
946
		$value = strtolower( $value );
947
		if ( in_array( $value, array( 'false', '0' ), true ) ) {
948
			$value = false;
949
		}
950
	}
951
952
	// Everything else will map nicely to boolean.
953
	return (boolean) $value;
954
}
955
956
/**
957
 * Determines if a given value is boolean-like.
958
 *
959
 * @since 4.7.0
960
 *
961
 * @param bool|string $maybe_bool The value being evaluated.
962
 * @return boolean True if a boolean, otherwise false.
963
 */
964
function rest_is_boolean( $maybe_bool ) {
965
	if ( is_bool( $maybe_bool ) ) {
966
		return true;
967
	}
968
969
	if ( is_string( $maybe_bool ) ) {
970
		$maybe_bool = strtolower( $maybe_bool );
971
972
		$valid_boolean_values = array(
973
			'false',
974
			'true',
975
			'0',
976
			'1',
977
		);
978
979
		return in_array( $maybe_bool, $valid_boolean_values, true );
980
	}
981
982
	if ( is_int( $maybe_bool ) ) {
983
		return in_array( $maybe_bool, array( 0, 1 ), true );
984
	}
985
986
	return false;
987
}
988
989
/**
990
 * Retrieves the avatar urls in various sizes based on a given email address.
991
 *
992
 * @since 4.7.0
993
 *
994
 * @see get_avatar_url()
995
 *
996
 * @param string $email Email address.
997
 * @return array $urls Gravatar url for each size.
998
 */
999
function rest_get_avatar_urls( $email ) {
1000
	$avatar_sizes = rest_get_avatar_sizes();
1001
1002
	$urls = array();
1003
	foreach ( $avatar_sizes as $size ) {
1004
		$urls[ $size ] = get_avatar_url( $email, array( 'size' => $size ) );
1005
	}
1006
1007
	return $urls;
1008
}
1009
1010
/**
1011
 * Retrieves the pixel sizes for avatars.
1012
 *
1013
 * @since 4.7.0
1014
 *
1015
 * @return array List of pixel sizes for avatars. Default `[ 24, 48, 96 ]`.
1016
 */
1017
function rest_get_avatar_sizes() {
1018
	/**
1019
	 * Filters the REST avatar sizes.
1020
	 *
1021
	 * Use this filter to adjust the array of sizes returned by the
1022
	 * `rest_get_avatar_sizes` function.
1023
	 *
1024
	 * @since 4.4.0
1025
	 *
1026
	 * @param array $sizes An array of int values that are the pixel sizes for avatars.
1027
	 *                     Default `[ 24, 48, 96 ]`.
1028
	 */
1029
	return apply_filters( 'rest_avatar_sizes', array( 24, 48, 96 ) );
1030
}
1031
1032
/**
1033
 * Validate a value based on a schema.
1034
 *
1035
 * @since 4.7.0
1036
 *
1037
 * @param mixed  $value The value to validate.
1038
 * @param array  $args  Schema array to use for validation.
1039
 * @param string $param The parameter name, used in error messages.
1040
 * @return true|WP_Error
0 ignored issues
show
Should the return type not be WP_Error|true|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1041
 */
1042
function rest_validate_value_from_schema( $value, $args, $param = '' ) {
1043
	if ( 'array' === $args['type'] ) {
1044
		if ( ! is_array( $value ) ) {
1045
			$value = preg_split( '/[\s,]+/', $value );
1046
		}
1047
		if ( ! wp_is_numeric_array( $value ) ) {
1048
			/* translators: 1: parameter, 2: type name */
1049
			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
1050
		}
1051
		foreach ( $value as $index => $v ) {
1052
			$is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
1053
			if ( is_wp_error( $is_valid ) ) {
1054
				return $is_valid;
1055
			}
1056
		}
1057
	}
1058 View Code Duplication
	if ( ! empty( $args['enum'] ) ) {
1059
		if ( ! in_array( $value, $args['enum'], true ) ) {
1060
			/* translators: 1: parameter, 2: list of valid values */
1061
			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
1062
		}
1063
	}
1064
1065 View Code Duplication
	if ( in_array( $args['type'], array( 'integer', 'number' ) ) && ! is_numeric( $value ) ) {
1066
		/* translators: 1: parameter, 2: type name */
1067
		return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
1068
	}
1069
1070
	if ( 'integer' === $args['type'] && round( floatval( $value ) ) !== floatval( $value ) ) {
1071
		/* translators: 1: parameter, 2: type name */
1072
		return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
1073
	}
1074
1075 View Code Duplication
	if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
1076
		/* translators: 1: parameter, 2: type name */
1077
		return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $value, 'boolean' ) );
1078
	}
1079
1080 View Code Duplication
	if ( 'string' === $args['type'] && ! is_string( $value ) ) {
1081
		/* translators: 1: parameter, 2: type name */
1082
		return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
1083
	}
1084
1085
	if ( isset( $args['format'] ) ) {
1086
		switch ( $args['format'] ) {
1087
			case 'date-time' :
1088
				if ( ! rest_parse_date( $value ) ) {
1089
					return new WP_Error( 'rest_invalid_date', __( 'Invalid date.' ) );
1090
				}
1091
				break;
1092
1093
			case 'email' :
1094
				if ( ! is_email( $value ) ) {
1095
					return new WP_Error( 'rest_invalid_email', __( 'Invalid email address.' ) );
1096
				}
1097
				break;
1098
			case 'ip' :
1099
				if ( ! rest_is_ip_address( $value ) ) {
1100
					/* translators: %s: IP address */
1101
					return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $value ) );
1102
				}
1103
				break;
1104
		}
1105
	}
1106
1107
	if ( in_array( $args['type'], array( 'number', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
1108
		if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
1109 View Code Duplication
			if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
1110
				/* translators: 1: parameter, 2: minimum number */
1111
				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] ) );
1112
			} elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
1113
				/* translators: 1: parameter, 2: minimum number */
1114
				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] ) );
1115
			}
1116
		} elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
1117 View Code Duplication
			if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
1118
				/* translators: 1: parameter, 2: maximum number */
1119
				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] ) );
1120
			} elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
1121
				/* translators: 1: parameter, 2: maximum number */
1122
				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] ) );
1123
			}
1124
		} elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
1125
			if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
1126
				if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
1127
					/* translators: 1: parameter, 2: minimum number, 3: maximum number */
1128
					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1129
				}
1130 View Code Duplication
			} elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
1131
				if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
1132
					/* translators: 1: parameter, 2: minimum number, 3: maximum number */
1133
					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1134
				}
1135
			} elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
1136
				if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
1137
					/* translators: 1: parameter, 2: minimum number, 3: maximum number */
1138
					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1139
				}
1140 View Code Duplication
			} elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
1141
				if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
1142
					/* translators: 1: parameter, 2: minimum number, 3: maximum number */
1143
					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
1144
				}
1145
			}
1146
		}
1147
	}
1148
1149
	return true;
1150
}
1151
1152
/**
1153
 * Sanitize a value based on a schema.
1154
 *
1155
 * @since 4.7.0
1156
 *
1157
 * @param mixed $value The value to sanitize.
1158
 * @param array $args  Schema array to use for sanitization.
1159
 * @return true|WP_Error
1160
 */
1161
function rest_sanitize_value_from_schema( $value, $args ) {
1162
	if ( 'array' === $args['type'] ) {
1163
		if ( empty( $args['items'] ) ) {
1164
			return (array) $value;
1165
		}
1166
		if ( ! is_array( $value ) ) {
1167
			$value = preg_split( '/[\s,]+/', $value );
1168
		}
1169
		foreach ( $value as $index => $v ) {
1170
			$value[ $index ] = rest_sanitize_value_from_schema( $v, $args['items'] );
1171
		}
1172
		// Normalize to numeric array so nothing unexpected
1173
		// is in the keys.
1174
		$value = array_values( $value );
1175
		return $value;
1176
	}
1177
	if ( 'integer' === $args['type'] ) {
1178
		return (int) $value;
1179
	}
1180
1181
	if ( 'number' === $args['type'] ) {
1182
		return (float) $value;
1183
	}
1184
1185
	if ( 'boolean' === $args['type'] ) {
1186
		return rest_sanitize_boolean( $value );
1187
	}
1188
1189
	if ( isset( $args['format'] ) ) {
1190
		switch ( $args['format'] ) {
1191
			case 'date-time' :
1192
				return sanitize_text_field( $value );
1193
1194
			case 'email' :
1195
				/*
1196
				 * sanitize_email() validates, which would be unexpected.
1197
				 */
1198
				return sanitize_text_field( $value );
1199
1200
			case 'uri' :
1201
				return esc_url_raw( $value );
1202
1203
			case 'ip' :
1204
				return sanitize_text_field( $value );
1205
		}
1206
	}
1207
1208
	if ( 'string' === $args['type'] ) {
1209
		return strval( $value );
1210
	}
1211
1212
	return $value;
1213
}
1214