Completed
Push — master ( a7cd2a...eabd6c )
by Stephen
38:42
created

WP_REST_Server::serve_request()   F

Complexity

Conditions 21
Paths 6932

Size

Total Lines 180
Code Lines 71

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 180
rs 2
cc 21
eloc 71
nc 6932
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
230
		/*
231
		 * Mitigate possible JSONP Flash attacks.
232
		 *
233
		 * http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
234
		 */
235
		$this->send_header( 'X-Content-Type-Options', 'nosniff' );
236
		$this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
237
		$this->send_header( 'Access-Control-Allow-Headers', 'Authorization' );
238
239
		/**
240
		 * Send nocache headers on authenticated requests.
241
		 *
242
		 * @since 4.4.0
243
		 *
244
		 * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
245
		 */
246
		$send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
247
		if ( $send_no_cache_headers ) {
248
			foreach ( wp_get_nocache_headers() as $header => $header_value ) {
249
				$this->send_header( $header, $header_value );
0 ignored issues
show
Documentation introduced by
$header_value is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
250
			}
251
		}
252
253
		/**
254
		 * Filter whether the REST API is enabled.
255
		 *
256
		 * @since 4.4.0
257
		 *
258
		 * @param bool $rest_enabled Whether the REST API is enabled. Default true.
259
		 */
260
		$enabled = apply_filters( 'rest_enabled', true );
261
262
		/**
263
		 * Filter whether jsonp is enabled.
264
		 *
265
		 * @since 4.4.0
266
		 *
267
		 * @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
268
		 */
269
		$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
270
271
		$jsonp_callback = null;
272
273
		if ( ! $enabled ) {
274
			echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 );
275
			return false;
276
		}
277
		if ( isset( $_GET['_jsonp'] ) ) {
278
			if ( ! $jsonp_enabled ) {
279
				echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
280
				return false;
281
			}
282
283
			// Check for invalid characters (only alphanumeric allowed).
284
			if ( is_string( $_GET['_jsonp'] ) ) {
285
				$jsonp_callback = preg_replace( '/[^\w\.]/', '', wp_unslash( $_GET['_jsonp'] ), -1, $illegal_char_count );
286
				if ( 0 !== $illegal_char_count ) {
287
					$jsonp_callback = null;
288
				}
289
			}
290
			if ( null === $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( $_GET );
307
		$request->set_body_params( $_POST );
308
		$request->set_file_params( $_FILES );
309
		$request->set_headers( $this->get_headers( $_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
		 * Filter 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
		 * Filter 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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $json_error_message of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

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

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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
				// http://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_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
458
		if ( empty( $links ) ) {
459
			return array();
460
		}
461
462
		// Convert links to part of the data.
463
		$data = array();
464
		$curies = $response->get_curies();
465
		$used_curies = array();
466
467
		foreach ( $links as $rel => $items ) {
468
469
			// Convert $rel URIs to their compact versions if they exist.
470
			foreach ( $curies as $curie ) {
471
				$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
472
				if ( strpos( $rel, $href_prefix ) !== 0 ) {
473
					continue;
474
				}
475
				$used_curies[ $curie['name'] ] = $curie;
476
477
				// Relation now changes from '$uri' to '$curie:$relation'
478
				$rel_regex = str_replace( '\{rel\}', '([\w]+)', preg_quote( $curie['href'], '!' ) );
479
				preg_match( '!' . $rel_regex . '!', $rel, $matches );
480
				if ( $matches ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $matches of type string[] 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...
481
					$rel = $curie['name'] . ':' . $matches[1];
482
				}
483
				break;
484
			}
485
486
			$data[ $rel ] = array();
487
488
			foreach ( $items as $item ) {
489
				$attributes = $item['attributes'];
490
				$attributes['href'] = $item['href'];
491
				$data[ $rel ][] = $attributes;
492
			}
493
		}
494
495
		// Push the curies onto the start of the links array.
496
		if ( $used_curies ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $used_curies 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...
497
			$data = array_merge( array( 'curies' => array_values( $used_curies ) ), $data );
498
		}
499
500
		return $data;
501
	}
502
503
	/**
504
	 * Embeds the links from the data into the request.
505
	 *
506
	 * @since 4.4.0
507
	 * @access protected
508
	 *
509
	 * @param array $data Data from the request.
510
	 * @return array {
511
	 *     Data with sub-requests embedded.
512
	 *
513
	 *     @type array [$_links]    Links.
514
	 *     @type array [$_embedded] Embeddeds.
515
	 * }
516
	 */
517
	protected function embed_links( $data ) {
518
		if ( empty( $data['_links'] ) ) {
519
			return $data;
520
		}
521
522
		$embedded = array();
523
524
		foreach ( $data['_links'] as $rel => $links ) {
525
			// Ignore links to self, for obvious reasons.
526
			if ( 'self' === $rel ) {
527
				continue;
528
			}
529
530
			$embeds = array();
531
532
			foreach ( $links as $item ) {
533
				// Determine if the link is embeddable.
534
				if ( empty( $item['embeddable'] ) ) {
535
					// Ensure we keep the same order.
536
					$embeds[] = array();
537
					continue;
538
				}
539
540
				// Run through our internal routing and serve.
541
				$request = WP_REST_Request::from_url( $item['href'] );
542
				if ( ! $request ) {
543
					$embeds[] = array();
544
					continue;
545
				}
546
547
				// Embedded resources get passed context=embed.
548
				if ( empty( $request['context'] ) ) {
549
					$request['context'] = 'embed';
550
				}
551
552
				$response = $this->dispatch( $request );
553
554
				/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
555
				$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
556
557
				$embeds[] = $this->response_to_data( $response, false );
558
			}
559
560
			// Determine if any real links were found.
561
			$has_links = count( array_filter( $embeds ) );
562
			if ( $has_links ) {
563
				$embedded[ $rel ] = $embeds;
564
			}
565
		}
566
567
		if ( ! empty( $embedded ) ) {
568
			$data['_embedded'] = $embedded;
569
		}
570
571
		return $data;
572
	}
573
574
	/**
575
	 * Wraps the response in an envelope.
576
	 *
577
	 * The enveloping technique is used to work around browser/client
578
	 * compatibility issues. Essentially, it converts the full HTTP response to
579
	 * data instead.
580
	 *
581
	 * @since 4.4.0
582
	 * @access public
583
	 *
584
	 * @param WP_REST_Response $response Response object.
585
	 * @param bool             $embed    Whether links should be embedded.
586
	 * @return WP_REST_Response New response with wrapped data
587
	 */
588
	public function envelope_response( $response, $embed ) {
589
		$envelope = array(
590
			'body'    => $this->response_to_data( $response, $embed ),
591
			'status'  => $response->get_status(),
592
			'headers' => $response->get_headers(),
593
		);
594
595
		/**
596
		 * Filter the enveloped form of a response.
597
		 *
598
		 * @since 4.4.0
599
		 *
600
		 * @param array            $envelope Envelope data.
601
		 * @param WP_REST_Response $response Original response data.
602
		 */
603
		$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
604
605
		// Ensure it's still a response and return.
606
		return rest_ensure_response( $envelope );
607
	}
608
609
	/**
610
	 * Registers a route to the server.
611
	 *
612
	 * @since 4.4.0
613
	 * @access public
614
	 *
615
	 * @param string $namespace  Namespace.
616
	 * @param string $route      The REST route.
617
	 * @param array  $route_args Route arguments.
618
	 * @param bool   $override   Optional. Whether the route should be overriden if it already exists.
619
	 *                           Default false.
620
	 */
621
	public function register_route( $namespace, $route, $route_args, $override = false ) {
622
		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
623
			$this->namespaces[ $namespace ] = array();
624
625
			$this->register_route( $namespace, '/' . $namespace, array(
626
				array(
627
					'methods' => self::READABLE,
628
					'callback' => array( $this, 'get_namespace_index' ),
629
					'args' => array(
630
						'namespace' => array(
631
							'default' => $namespace,
632
						),
633
						'context' => array(
634
							'default' => 'view',
635
						),
636
					),
637
				),
638
			) );
639
		}
640
641
		// Associative to avoid double-registration.
642
		$this->namespaces[ $namespace ][ $route ] = true;
643
		$route_args['namespace'] = $namespace;
644
645
		if ( $override || empty( $this->endpoints[ $route ] ) ) {
646
			$this->endpoints[ $route ] = $route_args;
647
		} else {
648
			$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
649
		}
650
	}
651
652
	/**
653
	 * Retrieves the route map.
654
	 *
655
	 * The route map is an associative array with path regexes as the keys. The
656
	 * value is an indexed array with the callback function/method as the first
657
	 * item, and a bitmask of HTTP methods as the second item (see the class
658
	 * constants).
659
	 *
660
	 * Each route can be mapped to more than one callback by using an array of
661
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
662
	 * and POST requests to another.
663
	 *
664
	 * Note that the path regexes (array keys) must have @ escaped, as this is
665
	 * used as the delimiter with preg_match()
666
	 *
667
	 * @since 4.4.0
668
	 * @access public
669
	 *
670
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or
671
	 *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
672
	 */
673
	public function get_routes() {
674
675
		/**
676
		 * Filter the array of available endpoints.
677
		 *
678
		 * @since 4.4.0
679
		 *
680
		 * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
681
		 *                         to an array of callbacks for the endpoint. These take the format
682
		 *                         `'/path/regex' => array( $callback, $bitmask )` or
683
		 *                         `'/path/regex' => array( array( $callback, $bitmask ).
684
		 */
685
		$endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
686
687
		// Normalise the endpoints.
688
		$defaults = array(
689
			'methods'       => '',
690
			'accept_json'   => false,
691
			'accept_raw'    => false,
692
			'show_in_index' => true,
693
			'args'          => array(),
694
		);
695
696
		foreach ( $endpoints as $route => &$handlers ) {
697
698
			if ( isset( $handlers['callback'] ) ) {
699
				// Single endpoint, add one deeper.
700
				$handlers = array( $handlers );
701
			}
702
703
			if ( ! isset( $this->route_options[ $route ] ) ) {
704
				$this->route_options[ $route ] = array();
705
			}
706
707
			foreach ( $handlers as $key => &$handler ) {
708
709
				if ( ! is_numeric( $key ) ) {
710
					// Route option, move it to the options.
711
					$this->route_options[ $route ][ $key ] = $handler;
712
					unset( $handlers[ $key ] );
713
					continue;
714
				}
715
716
				$handler = wp_parse_args( $handler, $defaults );
717
718
				// Allow comma-separated HTTP methods.
719
				if ( is_string( $handler['methods'] ) ) {
720
					$methods = explode( ',', $handler['methods'] );
721
				} else if ( is_array( $handler['methods'] ) ) {
722
					$methods = $handler['methods'];
723
				} else {
724
					$methods = array();
725
				}
726
727
				$handler['methods'] = array();
728
729
				foreach ( $methods as $method ) {
730
					$method = strtoupper( trim( $method ) );
731
					$handler['methods'][ $method ] = true;
732
				}
733
			}
734
		}
735
		return $endpoints;
736
	}
737
738
	/**
739
	 * Retrieves namespaces registered on the server.
740
	 *
741
	 * @since 4.4.0
742
	 * @access public
743
	 *
744
	 * @return array List of registered namespaces.
745
	 */
746
	public function get_namespaces() {
747
		return array_keys( $this->namespaces );
748
	}
749
750
	/**
751
	 * Retrieves specified options for a route.
752
	 *
753
	 * @since 4.4.0
754
	 * @access public
755
	 *
756
	 * @param string $route Route pattern to fetch options for.
757
	 * @return array|null Data as an associative array if found, or null if not found.
758
	 */
759
	public function get_route_options( $route ) {
760
		if ( ! isset( $this->route_options[ $route ] ) ) {
761
			return null;
762
		}
763
764
		return $this->route_options[ $route ];
765
	}
766
767
	/**
768
	 * Matches the request to a callback and call it.
769
	 *
770
	 * @since 4.4.0
771
	 * @access public
772
	 *
773
	 * @param WP_REST_Request $request Request to attempt dispatching.
774
	 * @return WP_REST_Response Response returned by the callback.
775
	 */
776
	public function dispatch( $request ) {
777
		/**
778
		 * Filter the pre-calculated result of a REST dispatch request.
779
		 *
780
		 * Allow hijacking the request before dispatching by returning a non-empty. The returned value
781
		 * will be used to serve the request instead.
782
		 *
783
		 * @since 4.4.0
784
		 *
785
		 * @param mixed           $result  Response to replace the requested version with. Can be anything
786
		 *                                 a normal endpoint can return, or null to not hijack the request.
787
		 * @param WP_REST_Server  $this    Server instance.
788
		 * @param WP_REST_Request $request Request used to generate the response.
789
		 */
790
		$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
791
792
		if ( ! empty( $result ) ) {
793
			return $result;
794
		}
795
796
		$method = $request->get_method();
797
		$path   = $request->get_route();
798
799
		foreach ( $this->get_routes() as $route => $handlers ) {
800
			$match = preg_match( '@^' . $route . '$@i', $path, $args );
801
802
			if ( ! $match ) {
803
				continue;
804
			}
805
806
			foreach ( $handlers as $handler ) {
807
				$callback  = $handler['callback'];
808
				$response = null;
809
810
				// Fallback to GET method if no HEAD method is registered.
811
				$checked_method = $method;
812
				if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
813
					$checked_method = 'GET';
814
				}
815
				if ( empty( $handler['methods'][ $checked_method ] ) ) {
816
					continue;
817
				}
818
819
				if ( ! is_callable( $callback ) ) {
820
					$response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
821
				}
822
823
				if ( ! is_wp_error( $response ) ) {
824
					// Remove the redundant preg_match argument.
825
					unset( $args[0] );
826
827
					$request->set_url_params( $args );
828
					$request->set_attributes( $handler );
829
830
					$request->sanitize_params();
831
832
					$defaults = array();
833
834
					foreach ( $handler['args'] as $arg => $options ) {
835
						if ( isset( $options['default'] ) ) {
836
							$defaults[ $arg ] = $options['default'];
837
						}
838
					}
839
840
					$request->set_default_params( $defaults );
841
842
					$check_required = $request->has_valid_params();
843
					if ( is_wp_error( $check_required ) ) {
844
						$response = $check_required;
845
					}
846
				}
847
848
				if ( ! is_wp_error( $response ) ) {
849
					// Check permission specified on the route.
850
					if ( ! empty( $handler['permission_callback'] ) ) {
851
						$permission = call_user_func( $handler['permission_callback'], $request );
852
853
						if ( is_wp_error( $permission ) ) {
854
							$response = $permission;
855
						} else if ( false === $permission || null === $permission ) {
856
							$response = new WP_Error( 'rest_forbidden', __( "You don't have permission to do this." ), array( 'status' => 403 ) );
857
						}
858
					}
859
				}
860
861
				if ( ! is_wp_error( $response ) ) {
862
					/**
863
					 * Filter the REST dispatch request result.
864
					 *
865
					 * Allow plugins to override dispatching the request.
866
					 *
867
					 * @since 4.4.0
868
					 * @since 4.5.0 Added `$route` and `$handler` parameters.
869
					 *
870
					 * @param bool            $dispatch_result Dispatch result, will be used if not empty.
871
					 * @param WP_REST_Request $request         Request used to generate the response.
872
					 * @param string          $route           Route matched for the request.
873
					 * @param array           $handler         Route handler used for the request.
874
					 */
875
					$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
876
877
					// Allow plugins to halt the request via this filter.
878
					if ( null !== $dispatch_result ) {
879
						$response = $dispatch_result;
880
					} else {
881
						$response = call_user_func( $callback, $request );
882
					}
883
				}
884
885
				if ( is_wp_error( $response ) ) {
886
					$response = $this->error_to_response( $response );
887
				} else {
888
					$response = rest_ensure_response( $response );
889
				}
890
891
				$response->set_matched_route( $route );
892
				$response->set_matched_handler( $handler );
893
894
				return $response;
895
			}
896
		}
897
898
		return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
899
	}
900
901
	/**
902
	 * Returns if an error occurred during most recent JSON encode/decode.
903
	 *
904
	 * Strings to be translated will be in format like
905
	 * "Encoding error: Maximum stack depth exceeded".
906
	 *
907
	 * @since 4.4.0
908
	 * @access protected
909
	 *
910
	 * @return bool|string Boolean false or string error message.
911
	 */
912
	protected function get_json_last_error() {
913
		// See https://core.trac.wordpress.org/ticket/27799.
914
		if ( ! function_exists( 'json_last_error' ) ) {
915
			return false;
916
		}
917
918
		$last_error_code = json_last_error();
919
920
		if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
921
			return false;
922
		}
923
924
		return json_last_error_msg();
925
	}
926
927
	/**
928
	 * Retrieves the site index.
929
	 *
930
	 * This endpoint describes the capabilities of the site.
931
	 *
932
	 * @since 4.4.0
933
	 * @access public
934
	 *
935
	 * @param array $request {
936
	 *     Request.
937
	 *
938
	 *     @type string $context Context.
939
	 * }
940
	 * @return array Index entity
941
	 */
942
	public function get_index( $request ) {
943
		// General site data.
944
		$available = array(
945
			'name'           => get_option( 'blogname' ),
946
			'description'    => get_option( 'blogdescription' ),
947
			'url'            => get_option( 'siteurl' ),
948
			'namespaces'     => array_keys( $this->namespaces ),
949
			'authentication' => array(),
950
			'routes'         => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
951
		);
952
953
		$response = new WP_REST_Response( $available );
954
955
		$response->add_link( 'help', 'http://v2.wp-api.org/' );
956
957
		/**
958
		 * Filter the API root index data.
959
		 *
960
		 * This contains the data describing the API. This includes information
961
		 * about supported authentication schemes, supported namespaces, routes
962
		 * available on the API, and a small amount of data about the site.
963
		 *
964
		 * @since 4.4.0
965
		 *
966
		 * @param WP_REST_Response $response Response data.
967
		 */
968
		return apply_filters( 'rest_index', $response );
969
	}
970
971
	/**
972
	 * Retrieves the index for a namespace.
973
	 *
974
	 * @since 4.4.0
975
	 * @access public
976
	 *
977
	 * @param WP_REST_Request $request REST request instance.
978
	 * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
979
	 *                                   WP_Error if the namespace isn't set.
980
	 */
981
	public function get_namespace_index( $request ) {
982
		$namespace = $request['namespace'];
983
984
		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
985
			return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
986
		}
987
988
		$routes = $this->namespaces[ $namespace ];
989
		$endpoints = array_intersect_key( $this->get_routes(), $routes );
990
991
		$data = array(
992
			'namespace' => $namespace,
993
			'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
994
		);
995
		$response = rest_ensure_response( $data );
996
997
		// Link to the root index.
998
		$response->add_link( 'up', rest_url( '/' ) );
999
1000
		/**
1001
		 * Filter the namespace index data.
1002
		 *
1003
		 * This typically is just the route data for the namespace, but you can
1004
		 * add any data you'd like here.
1005
		 *
1006
		 * @since 4.4.0
1007
		 *
1008
		 * @param WP_REST_Response $response Response data.
1009
		 * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
1010
		 */
1011
		return apply_filters( 'rest_namespace_index', $response, $request );
1012
	}
1013
1014
	/**
1015
	 * Retrieves the publicly-visible data for routes.
1016
	 *
1017
	 * @since 4.4.0
1018
	 * @access public
1019
	 *
1020
	 * @param array  $routes  Routes to get data for.
1021
	 * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1022
	 * @return array Route data to expose in indexes.
1023
	 */
1024
	public function get_data_for_routes( $routes, $context = 'view' ) {
1025
		$available = array();
1026
1027
		// Find the available routes.
1028
		foreach ( $routes as $route => $callbacks ) {
1029
			$data = $this->get_data_for_route( $route, $callbacks, $context );
1030
			if ( empty( $data ) ) {
1031
				continue;
1032
			}
1033
1034
			/**
1035
			 * Filter the REST endpoint data.
1036
			 *
1037
			 * @since 4.4.0
1038
			 *
1039
			 * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1040
			 */
1041
			$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1042
		}
1043
1044
		/**
1045
		 * Filter the publicly-visible data for routes.
1046
		 *
1047
		 * This data is exposed on indexes and can be used by clients or
1048
		 * developers to investigate the site and find out how to use it. It
1049
		 * acts as a form of self-documentation.
1050
		 *
1051
		 * @since 4.4.0
1052
		 *
1053
		 * @param array $available Map of route to route data.
1054
		 * @param array $routes    Internal route data as an associative array.
1055
		 */
1056
		return apply_filters( 'rest_route_data', $available, $routes );
1057
	}
1058
1059
	/**
1060
	 * Retrieves publicly-visible data for the route.
1061
	 *
1062
	 * @since 4.4.0
1063
	 * @access public
1064
	 *
1065
	 * @param string $route     Route to get data for.
1066
	 * @param array  $callbacks Callbacks to convert to data.
1067
	 * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1068
	 * @return array|null Data for the route, or null if no publicly-visible data.
1069
	 */
1070
	public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1071
		$data = array(
1072
			'namespace' => '',
1073
			'methods' => array(),
1074
			'endpoints' => array(),
1075
		);
1076
1077
		if ( isset( $this->route_options[ $route ] ) ) {
1078
			$options = $this->route_options[ $route ];
1079
1080
			if ( isset( $options['namespace'] ) ) {
1081
				$data['namespace'] = $options['namespace'];
1082
			}
1083
1084
			if ( isset( $options['schema'] ) && 'help' === $context ) {
1085
				$data['schema'] = call_user_func( $options['schema'] );
1086
			}
1087
		}
1088
1089
		$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1090
1091
		foreach ( $callbacks as $callback ) {
1092
			// Skip to the next route if any callback is hidden.
1093
			if ( empty( $callback['show_in_index'] ) ) {
1094
				continue;
1095
			}
1096
1097
			$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1098
			$endpoint_data = array(
1099
				'methods' => array_keys( $callback['methods'] ),
1100
			);
1101
1102
			if ( isset( $callback['args'] ) ) {
1103
				$endpoint_data['args'] = array();
1104
				foreach ( $callback['args'] as $key => $opts ) {
1105
					$arg_data = array(
1106
						'required' => ! empty( $opts['required'] ),
1107
					);
1108
					if ( isset( $opts['default'] ) ) {
1109
						$arg_data['default'] = $opts['default'];
1110
					}
1111
					if ( isset( $opts['enum'] ) ) {
1112
						$arg_data['enum'] = $opts['enum'];
1113
					}
1114
					if ( isset( $opts['description'] ) ) {
1115
						$arg_data['description'] = $opts['description'];
1116
					}
1117
					$endpoint_data['args'][ $key ] = $arg_data;
1118
				}
1119
			}
1120
1121
			$data['endpoints'][] = $endpoint_data;
1122
1123
			// For non-variable routes, generate links.
1124
			if ( strpos( $route, '{' ) === false ) {
1125
				$data['_links'] = array(
1126
					'self' => rest_url( $route ),
1127
				);
1128
			}
1129
		}
1130
1131
		if ( empty( $data['methods'] ) ) {
1132
			// No methods supported, hide the route.
1133
			return null;
1134
		}
1135
1136
		return $data;
1137
	}
1138
1139
	/**
1140
	 * Sends an HTTP status code.
1141
	 *
1142
	 * @since 4.4.0
1143
	 * @access protected
1144
	 *
1145
	 * @param int $code HTTP status.
1146
	 */
1147
	protected function set_status( $code ) {
1148
		status_header( $code );
1149
	}
1150
1151
	/**
1152
	 * Sends an HTTP header.
1153
	 *
1154
	 * @since 4.4.0
1155
	 * @access public
1156
	 *
1157
	 * @param string $key Header key.
1158
	 * @param string $value Header value.
1159
	 */
1160
	public function send_header( $key, $value ) {
1161
		/*
1162
		 * Sanitize as per RFC2616 (Section 4.2):
1163
		 *
1164
		 * Any LWS that occurs between field-content MAY be replaced with a
1165
		 * single SP before interpreting the field value or forwarding the
1166
		 * message downstream.
1167
		 */
1168
		$value = preg_replace( '/\s+/', ' ', $value );
1169
		header( sprintf( '%s: %s', $key, $value ) );
1170
	}
1171
1172
	/**
1173
	 * Sends multiple HTTP headers.
1174
	 *
1175
	 * @since 4.4.0
1176
	 * @access public
1177
	 *
1178
	 * @param array $headers Map of header name to header value.
1179
	 */
1180
	public function send_headers( $headers ) {
1181
		foreach ( $headers as $key => $value ) {
1182
			$this->send_header( $key, $value );
1183
		}
1184
	}
1185
1186
	/**
1187
	 * Retrieves the raw request entity (body).
1188
	 *
1189
	 * @since 4.4.0
1190
	 * @access public
1191
	 *
1192
	 * @global string $HTTP_RAW_POST_DATA Raw post data.
1193
	 *
1194
	 * @return string Raw request data.
1195
	 */
1196
	public static function get_raw_data() {
1197
		global $HTTP_RAW_POST_DATA;
1198
1199
		/*
1200
		 * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
1201
		 * but we can do it ourself.
1202
		 */
1203
		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1204
			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1205
		}
1206
1207
		return $HTTP_RAW_POST_DATA;
1208
	}
1209
1210
	/**
1211
	 * Extracts headers from a PHP-style $_SERVER array.
1212
	 *
1213
	 * @since 4.4.0
1214
	 * @access public
1215
	 *
1216
	 * @param array $server Associative array similar to `$_SERVER`.
1217
	 * @return array Headers extracted from the input.
1218
	 */
1219
	public function get_headers( $server ) {
1220
		$headers = array();
1221
1222
		// CONTENT_* headers are not prefixed with HTTP_.
1223
		$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
1224
1225
		foreach ( $server as $key => $value ) {
1226
			if ( strpos( $key, 'HTTP_' ) === 0 ) {
1227
				$headers[ substr( $key, 5 ) ] = $value;
1228
			} elseif ( isset( $additional[ $key ] ) ) {
1229
				$headers[ $key ] = $value;
1230
			}
1231
		}
1232
1233
		return $headers;
1234
	}
1235
}
1236