Issues (2010)

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.

wp-includes/rest-api/class-wp-rest-server.php (1 issue)

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: WP_REST_Server class
4
 *
5
 * @package WordPress
6
 * @subpackage REST_API
7
 * @since 4.4.0
8
 */
9
10
/**
11
 * Core class used to implement the WordPress REST API server.
12
 *
13
 * @since 4.4.0
14
 */
15
class WP_REST_Server {
16
17
	/**
18
	 * Alias for GET transport method.
19
	 *
20
	 * @since 4.4.0
21
	 * @var string
22
	 */
23
	const READABLE = 'GET';
24
25
	/**
26
	 * Alias for POST transport method.
27
	 *
28
	 * @since 4.4.0
29
	 * @var string
30
	 */
31
	const CREATABLE = 'POST';
32
33
	/**
34
	 * Alias for POST, PUT, PATCH transport methods together.
35
	 *
36
	 * @since 4.4.0
37
	 * @var string
38
	 */
39
	const EDITABLE = 'POST, PUT, PATCH';
40
41
	/**
42
	 * Alias for DELETE transport method.
43
	 *
44
	 * @since 4.4.0
45
	 * @var string
46
	 */
47
	const DELETABLE = 'DELETE';
48
49
	/**
50
	 * Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
51
	 *
52
	 * @since 4.4.0
53
	 * @var string
54
	 */
55
	const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
56
57
	/**
58
	 * Namespaces registered to the server.
59
	 *
60
	 * @since 4.4.0
61
	 * @access protected
62
	 * @var array
63
	 */
64
	protected $namespaces = array();
65
66
	/**
67
	 * Endpoints registered to the server.
68
	 *
69
	 * @since 4.4.0
70
	 * @access protected
71
	 * @var array
72
	 */
73
	protected $endpoints = array();
74
75
	/**
76
	 * Options defined for the routes.
77
	 *
78
	 * @since 4.4.0
79
	 * @access protected
80
	 * @var array
81
	 */
82
	protected $route_options = array();
83
84
	/**
85
	 * Instantiates the REST server.
86
	 *
87
	 * @since 4.4.0
88
	 * @access public
89
	 */
90
	public function __construct() {
91
		$this->endpoints = array(
92
			// Meta endpoints.
93
			'/' => array(
94
				'callback' => array( $this, 'get_index' ),
95
				'methods' => 'GET',
96
				'args' => array(
97
					'context' => array(
98
						'default' => 'view',
99
					),
100
				),
101
			),
102
		);
103
	}
104
105
106
	/**
107
	 * Checks the authentication headers if supplied.
108
	 *
109
	 * @since 4.4.0
110
	 * @access public
111
	 *
112
	 * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful
113
	 *                       or no authentication provided
114
	 */
115
	public function check_authentication() {
116
		/**
117
		 * Pass an authentication error to the API
118
		 *
119
		 * This is used to pass a WP_Error from an authentication method back to
120
		 * the API.
121
		 *
122
		 * Authentication methods should check first if they're being used, as
123
		 * multiple authentication methods can be enabled on a site (cookies,
124
		 * HTTP basic auth, OAuth). If the authentication method hooked in is
125
		 * not actually being attempted, null should be returned to indicate
126
		 * another authentication method should check instead. Similarly,
127
		 * callbacks should ensure the value is `null` before checking for
128
		 * errors.
129
		 *
130
		 * A WP_Error instance can be returned if an error occurs, and this should
131
		 * match the format used by API methods internally (that is, the `status`
132
		 * data should be used). A callback can return `true` to indicate that
133
		 * the authentication method was used, and it succeeded.
134
		 *
135
		 * @since 4.4.0
136
		 *
137
		 * @param WP_Error|null|bool WP_Error if authentication error, null if authentication
138
		 *                              method wasn't used, true if authentication succeeded.
139
		 */
140
		return apply_filters( 'rest_authentication_errors', null );
141
	}
142
143
	/**
144
	 * Converts an error to a response object.
145
	 *
146
	 * This iterates over all error codes and messages to change it into a flat
147
	 * array. This enables simpler client behaviour, as it is represented as a
148
	 * list in JSON rather than an object/map.
149
	 *
150
	 * @since 4.4.0
151
	 * @access protected
152
	 *
153
	 * @param WP_Error $error WP_Error instance.
154
	 * @return WP_REST_Response List of associative arrays with code and message keys.
155
	 */
156
	protected function error_to_response( $error ) {
157
		$error_data = $error->get_error_data();
158
159
		if ( is_array( $error_data ) && isset( $error_data['status'] ) ) {
160
			$status = $error_data['status'];
161
		} else {
162
			$status = 500;
163
		}
164
165
		$errors = array();
166
167
		foreach ( (array) $error->errors as $code => $messages ) {
168
			foreach ( (array) $messages as $message ) {
169
				$errors[] = array( 'code' => $code, 'message' => $message, 'data' => $error->get_error_data( $code ) );
170
			}
171
		}
172
173
		$data = $errors[0];
174
		if ( count( $errors ) > 1 ) {
175
			// Remove the primary error.
176
			array_shift( $errors );
177
			$data['additional_errors'] = $errors;
178
		}
179
180
		$response = new WP_REST_Response( $data, $status );
181
182
		return $response;
183
	}
184
185
	/**
186
	 * Retrieves an appropriate error representation in JSON.
187
	 *
188
	 * Note: This should only be used in WP_REST_Server::serve_request(), as it
189
	 * cannot handle WP_Error internally. All callbacks and other internal methods
190
	 * should instead return a WP_Error with the data set to an array that includes
191
	 * a 'status' key, with the value being the HTTP status to send.
192
	 *
193
	 * @since 4.4.0
194
	 * @access protected
195
	 *
196
	 * @param string $code    WP_Error-style code.
197
	 * @param string $message Human-readable message.
198
	 * @param int    $status  Optional. HTTP status code to send. Default null.
199
	 * @return string JSON representation of the error
200
	 */
201
	protected function json_error( $code, $message, $status = null ) {
202
		if ( $status ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $status of type integer|null is loosely compared to true; 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...
203
			$this->set_status( $status );
204
		}
205
206
		$error = compact( 'code', 'message' );
207
208
		return wp_json_encode( $error );
209
	}
210
211
	/**
212
	 * Handles serving an API request.
213
	 *
214
	 * Matches the current server URI to a route and runs the first matching
215
	 * callback then outputs a JSON representation of the returned value.
216
	 *
217
	 * @since 4.4.0
218
	 * @access public
219
	 *
220
	 * @see WP_REST_Server::dispatch()
221
	 *
222
	 * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
223
	 *                     Default null.
224
	 * @return false|null Null if not served and a HEAD request, false otherwise.
225
	 */
226
	public function serve_request( $path = null ) {
227
		$content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
228
		$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
229
		$this->send_header( 'X-Robots-Tag', 'noindex' );
230
231
		$api_root = get_rest_url();
232
		if ( ! empty( $api_root ) ) {
233
			$this->send_header( 'Link', '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"' );
234
		}
235
236
		/*
237
		 * Mitigate possible JSONP Flash attacks.
238
		 *
239
		 * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
240
		 */
241
		$this->send_header( 'X-Content-Type-Options', 'nosniff' );
242
		$this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
243
		$this->send_header( 'Access-Control-Allow-Headers', 'Authorization' );
244
245
		/**
246
		 * Send nocache headers on authenticated requests.
247
		 *
248
		 * @since 4.4.0
249
		 *
250
		 * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
251
		 */
252
		$send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
253
		if ( $send_no_cache_headers ) {
254
			foreach ( wp_get_nocache_headers() as $header => $header_value ) {
255
				$this->send_header( $header, $header_value );
256
			}
257
		}
258
259
		/**
260
		 * Filters whether the REST API is enabled.
261
		 *
262
		 * @since 4.4.0
263
		 *
264
		 * @param bool $rest_enabled Whether the REST API is enabled. Default true.
265
		 */
266
		$enabled = apply_filters( 'rest_enabled', true );
267
268
		/**
269
		 * Filters whether jsonp is enabled.
270
		 *
271
		 * @since 4.4.0
272
		 *
273
		 * @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
274
		 */
275
		$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
276
277
		$jsonp_callback = null;
278
279
		if ( ! $enabled ) {
280
			echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 );
281
			return false;
282
		}
283
		if ( isset( $_GET['_jsonp'] ) ) {
284
			if ( ! $jsonp_enabled ) {
285
				echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
286
				return false;
287
			}
288
289
			$jsonp_callback = $_GET['_jsonp'];
290
			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
291
				echo $this->json_error( 'rest_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 );
292
				return false;
293
			}
294
		}
295
296
		if ( empty( $path ) ) {
297
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
298
				$path = $_SERVER['PATH_INFO'];
299
			} else {
300
				$path = '/';
301
			}
302
		}
303
304
		$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
305
306
		$request->set_query_params( wp_unslash( $_GET ) );
307
		$request->set_body_params( wp_unslash( $_POST ) );
308
		$request->set_file_params( $_FILES );
309
		$request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
310
		$request->set_body( $this->get_raw_data() );
311
312
		/*
313
		 * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
314
		 * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
315
		 * header.
316
		 */
317
		if ( isset( $_GET['_method'] ) ) {
318
			$request->set_method( $_GET['_method'] );
319
		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
320
			$request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
321
		}
322
323
		$result = $this->check_authentication();
324
325
		if ( ! is_wp_error( $result ) ) {
326
			$result = $this->dispatch( $request );
327
		}
328
329
		// Normalize to either WP_Error or WP_REST_Response...
330
		$result = rest_ensure_response( $result );
331
332
		// ...then convert WP_Error across.
333
		if ( is_wp_error( $result ) ) {
334
			$result = $this->error_to_response( $result );
335
		}
336
337
		/**
338
		 * Filters the API response.
339
		 *
340
		 * Allows modification of the response before returning.
341
		 *
342
		 * @since 4.4.0
343
		 * @since 4.5.0 Applied to embedded responses.
344
		 *
345
		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
346
		 * @param WP_REST_Server   $this    Server instance.
347
		 * @param WP_REST_Request  $request Request used to generate the response.
348
		 */
349
		$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
350
351
		// Wrap the response in an envelope if asked for.
352
		if ( isset( $_GET['_envelope'] ) ) {
353
			$result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
354
		}
355
356
		// Send extra data from response objects.
357
		$headers = $result->get_headers();
358
		$this->send_headers( $headers );
359
360
		$code = $result->get_status();
361
		$this->set_status( $code );
362
363
		/**
364
		 * Filters whether the request has already been served.
365
		 *
366
		 * Allow sending the request manually - by returning true, the API result
367
		 * will not be sent to the client.
368
		 *
369
		 * @since 4.4.0
370
		 *
371
		 * @param bool             $served  Whether the request has already been served.
372
		 *                                           Default false.
373
		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
374
		 * @param WP_REST_Request  $request Request used to generate the response.
375
		 * @param WP_REST_Server   $this    Server instance.
376
		 */
377
		$served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
378
379
		if ( ! $served ) {
380
			if ( 'HEAD' === $request->get_method() ) {
381
				return null;
382
			}
383
384
			// Embed links inside the request.
385
			$result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
386
387
			$result = wp_json_encode( $result );
388
389
			$json_error_message = $this->get_json_last_error();
390
			if ( $json_error_message ) {
391
				$json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
392
				$result = $this->error_to_response( $json_error_obj );
393
				$result = wp_json_encode( $result->data[0] );
394
			}
395
396
			if ( $jsonp_callback ) {
397
				// Prepend '/**/' to mitigate possible JSONP Flash attacks
398
				// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
399
				echo '/**/' . $jsonp_callback . '(' . $result . ')';
400
			} else {
401
				echo $result;
402
			}
403
		}
404
		return null;
405
	}
406
407
	/**
408
	 * Converts a response to data to send.
409
	 *
410
	 * @since 4.4.0
411
	 * @access public
412
	 *
413
	 * @param WP_REST_Response $response Response object.
414
	 * @param bool             $embed    Whether links should be embedded.
415
	 * @return array {
416
	 *     Data with sub-requests embedded.
417
	 *
418
	 *     @type array [$_links]    Links.
419
	 *     @type array [$_embedded] Embeddeds.
420
	 * }
421
	 */
422
	public function response_to_data( $response, $embed ) {
423
		$data  = $response->get_data();
424
		$links = $this->get_compact_response_links( $response );
425
426
		if ( ! empty( $links ) ) {
427
			// Convert links to part of the data.
428
			$data['_links'] = $links;
429
		}
430
		if ( $embed ) {
431
			// Determine if this is a numeric array.
432
			if ( wp_is_numeric_array( $data ) ) {
433
				$data = array_map( array( $this, 'embed_links' ), $data );
434
			} else {
435
				$data = $this->embed_links( $data );
436
			}
437
		}
438
439
		return $data;
440
	}
441
442
	/**
443
	 * Retrieves links from a response.
444
	 *
445
	 * Extracts the links from a response into a structured hash, suitable for
446
	 * direct output.
447
	 *
448
	 * @since 4.4.0
449
	 * @access public
450
	 * @static
451
	 *
452
	 * @param WP_REST_Response $response Response to extract links from.
453
	 * @return array Map of link relation to list of link hashes.
454
	 */
455
	public static function get_response_links( $response ) {
456
		$links = $response->get_links();
457
		if ( empty( $links ) ) {
458
			return array();
459
		}
460
461
		// Convert links to part of the data.
462
		$data = array();
463
		foreach ( $links as $rel => $items ) {
464
			$data[ $rel ] = array();
465
466
			foreach ( $items as $item ) {
467
				$attributes = $item['attributes'];
468
				$attributes['href'] = $item['href'];
469
				$data[ $rel ][] = $attributes;
470
			}
471
		}
472
473
		return $data;
474
	}
475
476
	/**
477
	 * Retrieves the CURIEs (compact URIs) used for relations.
478
	 *
479
	 * Extracts the links from a response into a structured hash, suitable for
480
	 * direct output.
481
	 *
482
	 * @since 4.5.0
483
	 * @access public
484
	 * @static
485
	 *
486
	 * @param WP_REST_Response $response Response to extract links from.
487
	 * @return array Map of link relation to list of link hashes.
488
	 */
489
	public static function get_compact_response_links( $response ) {
490
		$links = self::get_response_links( $response );
491
492
		if ( empty( $links ) ) {
493
			return array();
494
		}
495
496
		$curies = $response->get_curies();
497
		$used_curies = array();
498
499
		foreach ( $links as $rel => $items ) {
500
501
			// Convert $rel URIs to their compact versions if they exist.
502
			foreach ( $curies as $curie ) {
503
				$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
504
				if ( strpos( $rel, $href_prefix ) !== 0 ) {
505
					continue;
506
				}
507
508
				// Relation now changes from '$uri' to '$curie:$relation'
509
				$rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
510
				preg_match( '!' . $rel_regex . '!', $rel, $matches );
511
				if ( $matches ) {
512
					$new_rel = $curie['name'] . ':' . $matches[1];
513
					$used_curies[ $curie['name'] ] = $curie;
514
					$links[ $new_rel ] = $items;
515
					unset( $links[ $rel ] );
516
					break;
517
				}
518
			}
519
		}
520
521
		// Push the curies onto the start of the links array.
522
		if ( $used_curies ) {
523
			$links['curies'] = array_values( $used_curies );
524
		}
525
526
		return $links;
527
	}
528
529
	/**
530
	 * Embeds the links from the data into the request.
531
	 *
532
	 * @since 4.4.0
533
	 * @access protected
534
	 *
535
	 * @param array $data Data from the request.
536
	 * @return array {
537
	 *     Data with sub-requests embedded.
538
	 *
539
	 *     @type array [$_links]    Links.
540
	 *     @type array [$_embedded] Embeddeds.
541
	 * }
542
	 */
543
	protected function embed_links( $data ) {
544
		if ( empty( $data['_links'] ) ) {
545
			return $data;
546
		}
547
548
		$embedded = array();
549
550
		foreach ( $data['_links'] as $rel => $links ) {
551
			// Ignore links to self, for obvious reasons.
552
			if ( 'self' === $rel ) {
553
				continue;
554
			}
555
556
			$embeds = array();
557
558
			foreach ( $links as $item ) {
559
				// Determine if the link is embeddable.
560
				if ( empty( $item['embeddable'] ) ) {
561
					// Ensure we keep the same order.
562
					$embeds[] = array();
563
					continue;
564
				}
565
566
				// Run through our internal routing and serve.
567
				$request = WP_REST_Request::from_url( $item['href'] );
568
				if ( ! $request ) {
569
					$embeds[] = array();
570
					continue;
571
				}
572
573
				// Embedded resources get passed context=embed.
574
				if ( empty( $request['context'] ) ) {
575
					$request['context'] = 'embed';
576
				}
577
578
				$response = $this->dispatch( $request );
579
580
				/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
581
				$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
582
583
				$embeds[] = $this->response_to_data( $response, false );
584
			}
585
586
			// Determine if any real links were found.
587
			$has_links = count( array_filter( $embeds ) );
588
			if ( $has_links ) {
589
				$embedded[ $rel ] = $embeds;
590
			}
591
		}
592
593
		if ( ! empty( $embedded ) ) {
594
			$data['_embedded'] = $embedded;
595
		}
596
597
		return $data;
598
	}
599
600
	/**
601
	 * Wraps the response in an envelope.
602
	 *
603
	 * The enveloping technique is used to work around browser/client
604
	 * compatibility issues. Essentially, it converts the full HTTP response to
605
	 * data instead.
606
	 *
607
	 * @since 4.4.0
608
	 * @access public
609
	 *
610
	 * @param WP_REST_Response $response Response object.
611
	 * @param bool             $embed    Whether links should be embedded.
612
	 * @return WP_REST_Response New response with wrapped data
613
	 */
614
	public function envelope_response( $response, $embed ) {
615
		$envelope = array(
616
			'body'    => $this->response_to_data( $response, $embed ),
617
			'status'  => $response->get_status(),
618
			'headers' => $response->get_headers(),
619
		);
620
621
		/**
622
		 * Filters the enveloped form of a response.
623
		 *
624
		 * @since 4.4.0
625
		 *
626
		 * @param array            $envelope Envelope data.
627
		 * @param WP_REST_Response $response Original response data.
628
		 */
629
		$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
630
631
		// Ensure it's still a response and return.
632
		return rest_ensure_response( $envelope );
633
	}
634
635
	/**
636
	 * Registers a route to the server.
637
	 *
638
	 * @since 4.4.0
639
	 * @access public
640
	 *
641
	 * @param string $namespace  Namespace.
642
	 * @param string $route      The REST route.
643
	 * @param array  $route_args Route arguments.
644
	 * @param bool   $override   Optional. Whether the route should be overriden if it already exists.
645
	 *                           Default false.
646
	 */
647
	public function register_route( $namespace, $route, $route_args, $override = false ) {
648
		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
649
			$this->namespaces[ $namespace ] = array();
650
651
			$this->register_route( $namespace, '/' . $namespace, array(
652
				array(
653
					'methods' => self::READABLE,
654
					'callback' => array( $this, 'get_namespace_index' ),
655
					'args' => array(
656
						'namespace' => array(
657
							'default' => $namespace,
658
						),
659
						'context' => array(
660
							'default' => 'view',
661
						),
662
					),
663
				),
664
			) );
665
		}
666
667
		// Associative to avoid double-registration.
668
		$this->namespaces[ $namespace ][ $route ] = true;
669
		$route_args['namespace'] = $namespace;
670
671
		if ( $override || empty( $this->endpoints[ $route ] ) ) {
672
			$this->endpoints[ $route ] = $route_args;
673
		} else {
674
			$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
675
		}
676
	}
677
678
	/**
679
	 * Retrieves the route map.
680
	 *
681
	 * The route map is an associative array with path regexes as the keys. The
682
	 * value is an indexed array with the callback function/method as the first
683
	 * item, and a bitmask of HTTP methods as the second item (see the class
684
	 * constants).
685
	 *
686
	 * Each route can be mapped to more than one callback by using an array of
687
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
688
	 * and POST requests to another.
689
	 *
690
	 * Note that the path regexes (array keys) must have @ escaped, as this is
691
	 * used as the delimiter with preg_match()
692
	 *
693
	 * @since 4.4.0
694
	 * @access public
695
	 *
696
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or
697
	 *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
698
	 */
699
	public function get_routes() {
700
701
		/**
702
		 * Filters the array of available endpoints.
703
		 *
704
		 * @since 4.4.0
705
		 *
706
		 * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
707
		 *                         to an array of callbacks for the endpoint. These take the format
708
		 *                         `'/path/regex' => array( $callback, $bitmask )` or
709
		 *                         `'/path/regex' => array( array( $callback, $bitmask ).
710
		 */
711
		$endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
712
713
		// Normalise the endpoints.
714
		$defaults = array(
715
			'methods'       => '',
716
			'accept_json'   => false,
717
			'accept_raw'    => false,
718
			'show_in_index' => true,
719
			'args'          => array(),
720
		);
721
722
		foreach ( $endpoints as $route => &$handlers ) {
723
724
			if ( isset( $handlers['callback'] ) ) {
725
				// Single endpoint, add one deeper.
726
				$handlers = array( $handlers );
727
			}
728
729
			if ( ! isset( $this->route_options[ $route ] ) ) {
730
				$this->route_options[ $route ] = array();
731
			}
732
733
			foreach ( $handlers as $key => &$handler ) {
734
735
				if ( ! is_numeric( $key ) ) {
736
					// Route option, move it to the options.
737
					$this->route_options[ $route ][ $key ] = $handler;
738
					unset( $handlers[ $key ] );
739
					continue;
740
				}
741
742
				$handler = wp_parse_args( $handler, $defaults );
743
744
				// Allow comma-separated HTTP methods.
745
				if ( is_string( $handler['methods'] ) ) {
746
					$methods = explode( ',', $handler['methods'] );
747
				} else if ( is_array( $handler['methods'] ) ) {
748
					$methods = $handler['methods'];
749
				} else {
750
					$methods = array();
751
				}
752
753
				$handler['methods'] = array();
754
755
				foreach ( $methods as $method ) {
756
					$method = strtoupper( trim( $method ) );
757
					$handler['methods'][ $method ] = true;
758
				}
759
			}
760
		}
761
		return $endpoints;
762
	}
763
764
	/**
765
	 * Retrieves namespaces registered on the server.
766
	 *
767
	 * @since 4.4.0
768
	 * @access public
769
	 *
770
	 * @return array List of registered namespaces.
771
	 */
772
	public function get_namespaces() {
773
		return array_keys( $this->namespaces );
774
	}
775
776
	/**
777
	 * Retrieves specified options for a route.
778
	 *
779
	 * @since 4.4.0
780
	 * @access public
781
	 *
782
	 * @param string $route Route pattern to fetch options for.
783
	 * @return array|null Data as an associative array if found, or null if not found.
784
	 */
785
	public function get_route_options( $route ) {
786
		if ( ! isset( $this->route_options[ $route ] ) ) {
787
			return null;
788
		}
789
790
		return $this->route_options[ $route ];
791
	}
792
793
	/**
794
	 * Matches the request to a callback and call it.
795
	 *
796
	 * @since 4.4.0
797
	 * @access public
798
	 *
799
	 * @param WP_REST_Request $request Request to attempt dispatching.
800
	 * @return WP_REST_Response Response returned by the callback.
801
	 */
802
	public function dispatch( $request ) {
803
		/**
804
		 * Filters the pre-calculated result of a REST dispatch request.
805
		 *
806
		 * Allow hijacking the request before dispatching by returning a non-empty. The returned value
807
		 * will be used to serve the request instead.
808
		 *
809
		 * @since 4.4.0
810
		 *
811
		 * @param mixed           $result  Response to replace the requested version with. Can be anything
812
		 *                                 a normal endpoint can return, or null to not hijack the request.
813
		 * @param WP_REST_Server  $this    Server instance.
814
		 * @param WP_REST_Request $request Request used to generate the response.
815
		 */
816
		$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
817
818
		if ( ! empty( $result ) ) {
819
			return $result;
820
		}
821
822
		$method = $request->get_method();
823
		$path   = $request->get_route();
824
825
		foreach ( $this->get_routes() as $route => $handlers ) {
826
			$match = preg_match( '@^' . $route . '$@i', $path, $args );
827
828
			if ( ! $match ) {
829
				continue;
830
			}
831
832
			foreach ( $handlers as $handler ) {
833
				$callback  = $handler['callback'];
834
				$response = null;
835
836
				// Fallback to GET method if no HEAD method is registered.
837
				$checked_method = $method;
838
				if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
839
					$checked_method = 'GET';
840
				}
841
				if ( empty( $handler['methods'][ $checked_method ] ) ) {
842
					continue;
843
				}
844
845
				if ( ! is_callable( $callback ) ) {
846
					$response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
847
				}
848
849
				if ( ! is_wp_error( $response ) ) {
850
					// Remove the redundant preg_match argument.
851
					unset( $args[0] );
852
853
					$request->set_url_params( $args );
854
					$request->set_attributes( $handler );
855
856
					$defaults = array();
857
858
					foreach ( $handler['args'] as $arg => $options ) {
859
						if ( isset( $options['default'] ) ) {
860
							$defaults[ $arg ] = $options['default'];
861
						}
862
					}
863
864
					$request->set_default_params( $defaults );
865
866
					$check_required = $request->has_valid_params();
867
					if ( is_wp_error( $check_required ) ) {
868
						$response = $check_required;
869
					}
870
871
					$request->sanitize_params();
872
				}
873
874
				if ( ! is_wp_error( $response ) ) {
875
					// Check permission specified on the route.
876
					if ( ! empty( $handler['permission_callback'] ) ) {
877
						$permission = call_user_func( $handler['permission_callback'], $request );
878
879
						if ( is_wp_error( $permission ) ) {
880
							$response = $permission;
881
						} else if ( false === $permission || null === $permission ) {
882
							$response = new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.' ), array( 'status' => 403 ) );
883
						}
884
					}
885
				}
886
887
				if ( ! is_wp_error( $response ) ) {
888
					/**
889
					 * Filters the REST dispatch request result.
890
					 *
891
					 * Allow plugins to override dispatching the request.
892
					 *
893
					 * @since 4.4.0
894
					 * @since 4.5.0 Added `$route` and `$handler` parameters.
895
					 *
896
					 * @param bool            $dispatch_result Dispatch result, will be used if not empty.
897
					 * @param WP_REST_Request $request         Request used to generate the response.
898
					 * @param string          $route           Route matched for the request.
899
					 * @param array           $handler         Route handler used for the request.
900
					 */
901
					$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
902
903
					// Allow plugins to halt the request via this filter.
904
					if ( null !== $dispatch_result ) {
905
						$response = $dispatch_result;
906
					} else {
907
						$response = call_user_func( $callback, $request );
908
					}
909
				}
910
911
				if ( is_wp_error( $response ) ) {
912
					$response = $this->error_to_response( $response );
913
				} else {
914
					$response = rest_ensure_response( $response );
915
				}
916
917
				$response->set_matched_route( $route );
918
				$response->set_matched_handler( $handler );
919
920
				return $response;
921
			}
922
		}
923
924
		return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
925
	}
926
927
	/**
928
	 * Returns if an error occurred during most recent JSON encode/decode.
929
	 *
930
	 * Strings to be translated will be in format like
931
	 * "Encoding error: Maximum stack depth exceeded".
932
	 *
933
	 * @since 4.4.0
934
	 * @access protected
935
	 *
936
	 * @return bool|string Boolean false or string error message.
937
	 */
938
	protected function get_json_last_error() {
939
		// See https://core.trac.wordpress.org/ticket/27799.
940
		if ( ! function_exists( 'json_last_error' ) ) {
941
			return false;
942
		}
943
944
		$last_error_code = json_last_error();
945
946
		if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
947
			return false;
948
		}
949
950
		return json_last_error_msg();
951
	}
952
953
	/**
954
	 * Retrieves the site index.
955
	 *
956
	 * This endpoint describes the capabilities of the site.
957
	 *
958
	 * @since 4.4.0
959
	 * @access public
960
	 *
961
	 * @param array $request {
962
	 *     Request.
963
	 *
964
	 *     @type string $context Context.
965
	 * }
966
	 * @return array Index entity
967
	 */
968
	public function get_index( $request ) {
969
		// General site data.
970
		$available = array(
971
			'name'           => get_option( 'blogname' ),
972
			'description'    => get_option( 'blogdescription' ),
973
			'url'            => get_option( 'siteurl' ),
974
			'home'           => home_url(),
975
			'namespaces'     => array_keys( $this->namespaces ),
976
			'authentication' => array(),
977
			'routes'         => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
978
		);
979
980
		$response = new WP_REST_Response( $available );
981
982
		$response->add_link( 'help', 'http://v2.wp-api.org/' );
983
984
		/**
985
		 * Filters the API root index data.
986
		 *
987
		 * This contains the data describing the API. This includes information
988
		 * about supported authentication schemes, supported namespaces, routes
989
		 * available on the API, and a small amount of data about the site.
990
		 *
991
		 * @since 4.4.0
992
		 *
993
		 * @param WP_REST_Response $response Response data.
994
		 */
995
		return apply_filters( 'rest_index', $response );
996
	}
997
998
	/**
999
	 * Retrieves the index for a namespace.
1000
	 *
1001
	 * @since 4.4.0
1002
	 * @access public
1003
	 *
1004
	 * @param WP_REST_Request $request REST request instance.
1005
	 * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
1006
	 *                                   WP_Error if the namespace isn't set.
1007
	 */
1008
	public function get_namespace_index( $request ) {
1009
		$namespace = $request['namespace'];
1010
1011
		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
1012
			return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
1013
		}
1014
1015
		$routes = $this->namespaces[ $namespace ];
1016
		$endpoints = array_intersect_key( $this->get_routes(), $routes );
1017
1018
		$data = array(
1019
			'namespace' => $namespace,
1020
			'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
1021
		);
1022
		$response = rest_ensure_response( $data );
1023
1024
		// Link to the root index.
1025
		$response->add_link( 'up', rest_url( '/' ) );
1026
1027
		/**
1028
		 * Filters the namespace index data.
1029
		 *
1030
		 * This typically is just the route data for the namespace, but you can
1031
		 * add any data you'd like here.
1032
		 *
1033
		 * @since 4.4.0
1034
		 *
1035
		 * @param WP_REST_Response $response Response data.
1036
		 * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
1037
		 */
1038
		return apply_filters( 'rest_namespace_index', $response, $request );
1039
	}
1040
1041
	/**
1042
	 * Retrieves the publicly-visible data for routes.
1043
	 *
1044
	 * @since 4.4.0
1045
	 * @access public
1046
	 *
1047
	 * @param array  $routes  Routes to get data for.
1048
	 * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1049
	 * @return array Route data to expose in indexes.
1050
	 */
1051
	public function get_data_for_routes( $routes, $context = 'view' ) {
1052
		$available = array();
1053
1054
		// Find the available routes.
1055
		foreach ( $routes as $route => $callbacks ) {
1056
			$data = $this->get_data_for_route( $route, $callbacks, $context );
1057
			if ( empty( $data ) ) {
1058
				continue;
1059
			}
1060
1061
			/**
1062
			 * Filters the REST endpoint data.
1063
			 *
1064
			 * @since 4.4.0
1065
			 *
1066
			 * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1067
			 */
1068
			$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1069
		}
1070
1071
		/**
1072
		 * Filters the publicly-visible data for routes.
1073
		 *
1074
		 * This data is exposed on indexes and can be used by clients or
1075
		 * developers to investigate the site and find out how to use it. It
1076
		 * acts as a form of self-documentation.
1077
		 *
1078
		 * @since 4.4.0
1079
		 *
1080
		 * @param array $available Map of route to route data.
1081
		 * @param array $routes    Internal route data as an associative array.
1082
		 */
1083
		return apply_filters( 'rest_route_data', $available, $routes );
1084
	}
1085
1086
	/**
1087
	 * Retrieves publicly-visible data for the route.
1088
	 *
1089
	 * @since 4.4.0
1090
	 * @access public
1091
	 *
1092
	 * @param string $route     Route to get data for.
1093
	 * @param array  $callbacks Callbacks to convert to data.
1094
	 * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1095
	 * @return array|null Data for the route, or null if no publicly-visible data.
1096
	 */
1097
	public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1098
		$data = array(
1099
			'namespace' => '',
1100
			'methods' => array(),
1101
			'endpoints' => array(),
1102
		);
1103
1104
		if ( isset( $this->route_options[ $route ] ) ) {
1105
			$options = $this->route_options[ $route ];
1106
1107
			if ( isset( $options['namespace'] ) ) {
1108
				$data['namespace'] = $options['namespace'];
1109
			}
1110
1111
			if ( isset( $options['schema'] ) && 'help' === $context ) {
1112
				$data['schema'] = call_user_func( $options['schema'] );
1113
			}
1114
		}
1115
1116
		$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1117
1118
		foreach ( $callbacks as $callback ) {
1119
			// Skip to the next route if any callback is hidden.
1120
			if ( empty( $callback['show_in_index'] ) ) {
1121
				continue;
1122
			}
1123
1124
			$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1125
			$endpoint_data = array(
1126
				'methods' => array_keys( $callback['methods'] ),
1127
			);
1128
1129
			if ( isset( $callback['args'] ) ) {
1130
				$endpoint_data['args'] = array();
1131
				foreach ( $callback['args'] as $key => $opts ) {
1132
					$arg_data = array(
1133
						'required' => ! empty( $opts['required'] ),
1134
					);
1135
					if ( isset( $opts['default'] ) ) {
1136
						$arg_data['default'] = $opts['default'];
1137
					}
1138
					if ( isset( $opts['enum'] ) ) {
1139
						$arg_data['enum'] = $opts['enum'];
1140
					}
1141
					if ( isset( $opts['description'] ) ) {
1142
						$arg_data['description'] = $opts['description'];
1143
					}
1144
					$endpoint_data['args'][ $key ] = $arg_data;
1145
				}
1146
			}
1147
1148
			$data['endpoints'][] = $endpoint_data;
1149
1150
			// For non-variable routes, generate links.
1151
			if ( strpos( $route, '{' ) === false ) {
1152
				$data['_links'] = array(
1153
					'self' => rest_url( $route ),
1154
				);
1155
			}
1156
		}
1157
1158
		if ( empty( $data['methods'] ) ) {
1159
			// No methods supported, hide the route.
1160
			return null;
1161
		}
1162
1163
		return $data;
1164
	}
1165
1166
	/**
1167
	 * Sends an HTTP status code.
1168
	 *
1169
	 * @since 4.4.0
1170
	 * @access protected
1171
	 *
1172
	 * @param int $code HTTP status.
1173
	 */
1174
	protected function set_status( $code ) {
1175
		status_header( $code );
1176
	}
1177
1178
	/**
1179
	 * Sends an HTTP header.
1180
	 *
1181
	 * @since 4.4.0
1182
	 * @access public
1183
	 *
1184
	 * @param string $key Header key.
1185
	 * @param string $value Header value.
1186
	 */
1187
	public function send_header( $key, $value ) {
1188
		/*
1189
		 * Sanitize as per RFC2616 (Section 4.2):
1190
		 *
1191
		 * Any LWS that occurs between field-content MAY be replaced with a
1192
		 * single SP before interpreting the field value or forwarding the
1193
		 * message downstream.
1194
		 */
1195
		$value = preg_replace( '/\s+/', ' ', $value );
1196
		header( sprintf( '%s: %s', $key, $value ) );
1197
	}
1198
1199
	/**
1200
	 * Sends multiple HTTP headers.
1201
	 *
1202
	 * @since 4.4.0
1203
	 * @access public
1204
	 *
1205
	 * @param array $headers Map of header name to header value.
1206
	 */
1207
	public function send_headers( $headers ) {
1208
		foreach ( $headers as $key => $value ) {
1209
			$this->send_header( $key, $value );
1210
		}
1211
	}
1212
1213
	/**
1214
	 * Retrieves the raw request entity (body).
1215
	 *
1216
	 * @since 4.4.0
1217
	 * @access public
1218
	 *
1219
	 * @global string $HTTP_RAW_POST_DATA Raw post data.
1220
	 *
1221
	 * @return string Raw request data.
1222
	 */
1223
	public static function get_raw_data() {
1224
		global $HTTP_RAW_POST_DATA;
1225
1226
		/*
1227
		 * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
1228
		 * but we can do it ourself.
1229
		 */
1230
		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1231
			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1232
		}
1233
1234
		return $HTTP_RAW_POST_DATA;
1235
	}
1236
1237
	/**
1238
	 * Extracts headers from a PHP-style $_SERVER array.
1239
	 *
1240
	 * @since 4.4.0
1241
	 * @access public
1242
	 *
1243
	 * @param array $server Associative array similar to `$_SERVER`.
1244
	 * @return array Headers extracted from the input.
1245
	 */
1246
	public function get_headers( $server ) {
1247
		$headers = array();
1248
1249
		// CONTENT_* headers are not prefixed with HTTP_.
1250
		$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
1251
1252
		foreach ( $server as $key => $value ) {
1253
			if ( strpos( $key, 'HTTP_' ) === 0 ) {
1254
				$headers[ substr( $key, 5 ) ] = $value;
1255
			} elseif ( isset( $additional[ $key ] ) ) {
1256
				$headers[ $key ] = $value;
1257
			}
1258
		}
1259
1260
		return $headers;
1261
	}
1262
}
1263