Completed
Push — master ( 8ff7a8...c0c536 )
by Stephen
77:04 queued 40:03
created

WP_REST_Server::get_compact_response_links()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 39
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 39
rs 6.7272
c 1
b 0
f 0
cc 7
eloc 22
nc 11
nop 1
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_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 ) {
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...
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 ) {
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...
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
		 * Filter 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
		 * Filter 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
		 * Filter 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
					$request->sanitize_params();
857
858
					$defaults = array();
859
860
					foreach ( $handler['args'] as $arg => $options ) {
861
						if ( isset( $options['default'] ) ) {
862
							$defaults[ $arg ] = $options['default'];
863
						}
864
					}
865
866
					$request->set_default_params( $defaults );
867
868
					$check_required = $request->has_valid_params();
869
					if ( is_wp_error( $check_required ) ) {
870
						$response = $check_required;
871
					}
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', __( "You don't have permission to do this." ), array( 'status' => 403 ) );
883
						}
884
					}
885
				}
886
887
				if ( ! is_wp_error( $response ) ) {
888
					/**
889
					 * Filter 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
		 * Filter 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
		 * Filter 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
			 * Filter 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
		 * Filter 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