WP_REST_Server::register_route()   B
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 4
nop 4
dl 0
loc 30
rs 8.5806
c 0
b 0
f 0
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
		 * Filters REST authentication errors.
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
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

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

Loading history...
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, Content-Type' );
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
				if ( empty( $header_value ) ) {
256
					$this->remove_header( $header );
257
				} else {
258
					$this->send_header( $header, $header_value );
259
				}
260
			}
261
		}
262
263
		/**
264
		 * Filters whether the REST API is enabled.
265
		 *
266
		 * @since 4.4.0
267
		 * @deprecated 4.7.0 Use the rest_authentication_errors filter to restrict access to the API
268
		 *
269
		 * @param bool $rest_enabled Whether the REST API is enabled. Default true.
270
		 */
271
		apply_filters_deprecated( 'rest_enabled', array( true ), '4.7.0', 'rest_authentication_errors',
272
			__( 'The REST API can no longer be completely disabled, the rest_authentication_errors filter can be used to restrict access to the API, instead.' )
273
		);
274
275
		/**
276
		 * Filters whether jsonp is enabled.
277
		 *
278
		 * @since 4.4.0
279
		 *
280
		 * @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
281
		 */
282
		$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
283
284
		$jsonp_callback = null;
285
286
		if ( isset( $_GET['_jsonp'] ) ) {
287
			if ( ! $jsonp_enabled ) {
288
				echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
289
				return false;
290
			}
291
292
			$jsonp_callback = $_GET['_jsonp'];
293
			if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
294
				echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
295
				return false;
296
			}
297
		}
298
299
		if ( empty( $path ) ) {
300
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
301
				$path = $_SERVER['PATH_INFO'];
302
			} else {
303
				$path = '/';
304
			}
305
		}
306
307
		$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
308
309
		$request->set_query_params( wp_unslash( $_GET ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_GET) targeting wp_unslash() can also be of type string; however, WP_REST_Request::set_query_params() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
310
		$request->set_body_params( wp_unslash( $_POST ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_POST) targeting wp_unslash() can also be of type string; however, WP_REST_Request::set_body_params() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
311
		$request->set_file_params( $_FILES );
312
		$request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_SERVER) targeting wp_unslash() can also be of type string; however, WP_REST_Server::get_headers() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
313
		$request->set_body( $this->get_raw_data() );
314
315
		/*
316
		 * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
317
		 * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
318
		 * header.
319
		 */
320
		if ( isset( $_GET['_method'] ) ) {
321
			$request->set_method( $_GET['_method'] );
322
		} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
323
			$request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
324
		}
325
326
		$result = $this->check_authentication();
327
328
		if ( ! is_wp_error( $result ) ) {
329
			$result = $this->dispatch( $request );
330
		}
331
332
		// Normalize to either WP_Error or WP_REST_Response...
333
		$result = rest_ensure_response( $result );
334
335
		// ...then convert WP_Error across.
336
		if ( is_wp_error( $result ) ) {
337
			$result = $this->error_to_response( $result );
338
		}
339
340
		/**
341
		 * Filters the API response.
342
		 *
343
		 * Allows modification of the response before returning.
344
		 *
345
		 * @since 4.4.0
346
		 * @since 4.5.0 Applied to embedded responses.
347
		 *
348
		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
349
		 * @param WP_REST_Server   $this    Server instance.
350
		 * @param WP_REST_Request  $request Request used to generate the response.
351
		 */
352
		$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
353
354
		// Wrap the response in an envelope if asked for.
355
		if ( isset( $_GET['_envelope'] ) ) {
356
			$result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
357
		}
358
359
		// Send extra data from response objects.
360
		$headers = $result->get_headers();
361
		$this->send_headers( $headers );
362
363
		$code = $result->get_status();
364
		$this->set_status( $code );
365
366
		/**
367
		 * Filters whether the request has already been served.
368
		 *
369
		 * Allow sending the request manually - by returning true, the API result
370
		 * will not be sent to the client.
371
		 *
372
		 * @since 4.4.0
373
		 *
374
		 * @param bool             $served  Whether the request has already been served.
375
		 *                                           Default false.
376
		 * @param WP_HTTP_Response $result  Result to send to the client. Usually a WP_REST_Response.
377
		 * @param WP_REST_Request  $request Request used to generate the response.
378
		 * @param WP_REST_Server   $this    Server instance.
379
		 */
380
		$served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
381
382
		if ( ! $served ) {
383
			if ( 'HEAD' === $request->get_method() ) {
384
				return null;
385
			}
386
387
			// Embed links inside the request.
388
			$result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
389
390
			$result = wp_json_encode( $result );
391
392
			$json_error_message = $this->get_json_last_error();
393
			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...
394
				$json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
395
				$result = $this->error_to_response( $json_error_obj );
396
				$result = wp_json_encode( $result->data[0] );
397
			}
398
399
			if ( $jsonp_callback ) {
400
				// Prepend '/**/' to mitigate possible JSONP Flash attacks.
401
				// https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
402
				echo '/**/' . $jsonp_callback . '(' . $result . ')';
403
			} else {
404
				echo $result;
405
			}
406
		}
407
		return null;
408
	}
409
410
	/**
411
	 * Converts a response to data to send.
412
	 *
413
	 * @since 4.4.0
414
	 * @access public
415
	 *
416
	 * @param WP_REST_Response $response Response object.
417
	 * @param bool             $embed    Whether links should be embedded.
418
	 * @return array {
419
	 *     Data with sub-requests embedded.
420
	 *
421
	 *     @type array [$_links]    Links.
422
	 *     @type array [$_embedded] Embeddeds.
423
	 * }
424
	 */
425
	public function response_to_data( $response, $embed ) {
426
		$data  = $response->get_data();
427
		$links = $this->get_compact_response_links( $response );
428
429
		if ( ! empty( $links ) ) {
430
			// Convert links to part of the data.
431
			$data['_links'] = $links;
432
		}
433
		if ( $embed ) {
434
			// Determine if this is a numeric array.
435
			if ( wp_is_numeric_array( $data ) ) {
436
				$data = array_map( array( $this, 'embed_links' ), $data );
437
			} else {
438
				$data = $this->embed_links( $data );
439
			}
440
		}
441
442
		return $data;
443
	}
444
445
	/**
446
	 * Retrieves links from a response.
447
	 *
448
	 * Extracts the links from a response into a structured hash, suitable for
449
	 * direct output.
450
	 *
451
	 * @since 4.4.0
452
	 * @access public
453
	 * @static
454
	 *
455
	 * @param WP_REST_Response $response Response to extract links from.
456
	 * @return array Map of link relation to list of link hashes.
457
	 */
458
	public static function get_response_links( $response ) {
459
		$links = $response->get_links();
460
		if ( empty( $links ) ) {
461
			return array();
462
		}
463
464
		// Convert links to part of the data.
465
		$data = array();
466
		foreach ( $links as $rel => $items ) {
467
			$data[ $rel ] = array();
468
469
			foreach ( $items as $item ) {
470
				$attributes = $item['attributes'];
471
				$attributes['href'] = $item['href'];
472
				$data[ $rel ][] = $attributes;
473
			}
474
		}
475
476
		return $data;
477
	}
478
479
	/**
480
	 * Retrieves the CURIEs (compact URIs) used for relations.
481
	 *
482
	 * Extracts the links from a response into a structured hash, suitable for
483
	 * direct output.
484
	 *
485
	 * @since 4.5.0
486
	 * @access public
487
	 * @static
488
	 *
489
	 * @param WP_REST_Response $response Response to extract links from.
490
	 * @return array Map of link relation to list of link hashes.
491
	 */
492
	public static function get_compact_response_links( $response ) {
493
		$links = self::get_response_links( $response );
494
495
		if ( empty( $links ) ) {
496
			return array();
497
		}
498
499
		$curies = $response->get_curies();
500
		$used_curies = array();
501
502
		foreach ( $links as $rel => $items ) {
503
504
			// Convert $rel URIs to their compact versions if they exist.
505
			foreach ( $curies as $curie ) {
506
				$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
507
				if ( strpos( $rel, $href_prefix ) !== 0 ) {
508
					continue;
509
				}
510
511
				// Relation now changes from '$uri' to '$curie:$relation'.
512
				$rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
513
				preg_match( '!' . $rel_regex . '!', $rel, $matches );
514
				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...
515
					$new_rel = $curie['name'] . ':' . $matches[1];
516
					$used_curies[ $curie['name'] ] = $curie;
517
					$links[ $new_rel ] = $items;
518
					unset( $links[ $rel ] );
519
					break;
520
				}
521
			}
522
		}
523
524
		// Push the curies onto the start of the links array.
525
		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...
526
			$links['curies'] = array_values( $used_curies );
527
		}
528
529
		return $links;
530
	}
531
532
	/**
533
	 * Embeds the links from the data into the request.
534
	 *
535
	 * @since 4.4.0
536
	 * @access protected
537
	 *
538
	 * @param array $data Data from the request.
539
	 * @return array {
540
	 *     Data with sub-requests embedded.
541
	 *
542
	 *     @type array [$_links]    Links.
543
	 *     @type array [$_embedded] Embeddeds.
544
	 * }
545
	 */
546
	protected function embed_links( $data ) {
547
		if ( empty( $data['_links'] ) ) {
548
			return $data;
549
		}
550
551
		$embedded = array();
552
553
		foreach ( $data['_links'] as $rel => $links ) {
554
			// Ignore links to self, for obvious reasons.
555
			if ( 'self' === $rel ) {
556
				continue;
557
			}
558
559
			$embeds = array();
560
561
			foreach ( $links as $item ) {
562
				// Determine if the link is embeddable.
563
				if ( empty( $item['embeddable'] ) ) {
564
					// Ensure we keep the same order.
565
					$embeds[] = array();
566
					continue;
567
				}
568
569
				// Run through our internal routing and serve.
570
				$request = WP_REST_Request::from_url( $item['href'] );
571
				if ( ! $request ) {
572
					$embeds[] = array();
573
					continue;
574
				}
575
576
				// Embedded resources get passed context=embed.
577
				if ( empty( $request['context'] ) ) {
578
					$request['context'] = 'embed';
579
				}
580
581
				$response = $this->dispatch( $request );
582
583
				/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
584
				$response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
585
586
				$embeds[] = $this->response_to_data( $response, false );
587
			}
588
589
			// Determine if any real links were found.
590
			$has_links = count( array_filter( $embeds ) );
591
592
			if ( $has_links ) {
593
				$embedded[ $rel ] = $embeds;
594
			}
595
		}
596
597
		if ( ! empty( $embedded ) ) {
598
			$data['_embedded'] = $embedded;
599
		}
600
601
		return $data;
602
	}
603
604
	/**
605
	 * Wraps the response in an envelope.
606
	 *
607
	 * The enveloping technique is used to work around browser/client
608
	 * compatibility issues. Essentially, it converts the full HTTP response to
609
	 * data instead.
610
	 *
611
	 * @since 4.4.0
612
	 * @access public
613
	 *
614
	 * @param WP_REST_Response $response Response object.
615
	 * @param bool             $embed    Whether links should be embedded.
616
	 * @return WP_REST_Response New response with wrapped data
617
	 */
618
	public function envelope_response( $response, $embed ) {
619
		$envelope = array(
620
			'body'    => $this->response_to_data( $response, $embed ),
621
			'status'  => $response->get_status(),
622
			'headers' => $response->get_headers(),
623
		);
624
625
		/**
626
		 * Filters the enveloped form of a response.
627
		 *
628
		 * @since 4.4.0
629
		 *
630
		 * @param array            $envelope Envelope data.
631
		 * @param WP_REST_Response $response Original response data.
632
		 */
633
		$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
634
635
		// Ensure it's still a response and return.
636
		return rest_ensure_response( $envelope );
637
	}
638
639
	/**
640
	 * Registers a route to the server.
641
	 *
642
	 * @since 4.4.0
643
	 * @access public
644
	 *
645
	 * @param string $namespace  Namespace.
646
	 * @param string $route      The REST route.
647
	 * @param array  $route_args Route arguments.
648
	 * @param bool   $override   Optional. Whether the route should be overridden if it already exists.
649
	 *                           Default false.
650
	 */
651
	public function register_route( $namespace, $route, $route_args, $override = false ) {
652
		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
653
			$this->namespaces[ $namespace ] = array();
654
655
			$this->register_route( $namespace, '/' . $namespace, array(
656
				array(
657
					'methods' => self::READABLE,
658
					'callback' => array( $this, 'get_namespace_index' ),
659
					'args' => array(
660
						'namespace' => array(
661
							'default' => $namespace,
662
						),
663
						'context' => array(
664
							'default' => 'view',
665
						),
666
					),
667
				),
668
			) );
669
		}
670
671
		// Associative to avoid double-registration.
672
		$this->namespaces[ $namespace ][ $route ] = true;
673
		$route_args['namespace'] = $namespace;
674
675
		if ( $override || empty( $this->endpoints[ $route ] ) ) {
676
			$this->endpoints[ $route ] = $route_args;
677
		} else {
678
			$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
679
		}
680
	}
681
682
	/**
683
	 * Retrieves the route map.
684
	 *
685
	 * The route map is an associative array with path regexes as the keys. The
686
	 * value is an indexed array with the callback function/method as the first
687
	 * item, and a bitmask of HTTP methods as the second item (see the class
688
	 * constants).
689
	 *
690
	 * Each route can be mapped to more than one callback by using an array of
691
	 * the indexed arrays. This allows mapping e.g. GET requests to one callback
692
	 * and POST requests to another.
693
	 *
694
	 * Note that the path regexes (array keys) must have @ escaped, as this is
695
	 * used as the delimiter with preg_match()
696
	 *
697
	 * @since 4.4.0
698
	 * @access public
699
	 *
700
	 * @return array `'/path/regex' => array( $callback, $bitmask )` or
701
	 *               `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
702
	 */
703
	public function get_routes() {
704
705
		/**
706
		 * Filters the array of available endpoints.
707
		 *
708
		 * @since 4.4.0
709
		 *
710
		 * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
711
		 *                         to an array of callbacks for the endpoint. These take the format
712
		 *                         `'/path/regex' => array( $callback, $bitmask )` or
713
		 *                         `'/path/regex' => array( array( $callback, $bitmask ).
714
		 */
715
		$endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
716
717
		// Normalise the endpoints.
718
		$defaults = array(
719
			'methods'       => '',
720
			'accept_json'   => false,
721
			'accept_raw'    => false,
722
			'show_in_index' => true,
723
			'args'          => array(),
724
		);
725
726
		foreach ( $endpoints as $route => &$handlers ) {
727
728
			if ( isset( $handlers['callback'] ) ) {
729
				// Single endpoint, add one deeper.
730
				$handlers = array( $handlers );
731
			}
732
733
			if ( ! isset( $this->route_options[ $route ] ) ) {
734
				$this->route_options[ $route ] = array();
735
			}
736
737
			foreach ( $handlers as $key => &$handler ) {
738
739
				if ( ! is_numeric( $key ) ) {
740
					// Route option, move it to the options.
741
					$this->route_options[ $route ][ $key ] = $handler;
742
					unset( $handlers[ $key ] );
743
					continue;
744
				}
745
746
				$handler = wp_parse_args( $handler, $defaults );
747
748
				// Allow comma-separated HTTP methods.
749
				if ( is_string( $handler['methods'] ) ) {
750
					$methods = explode( ',', $handler['methods'] );
751
				} elseif ( is_array( $handler['methods'] ) ) {
752
					$methods = $handler['methods'];
753
				} else {
754
					$methods = array();
755
				}
756
757
				$handler['methods'] = array();
758
759
				foreach ( $methods as $method ) {
760
					$method = strtoupper( trim( $method ) );
761
					$handler['methods'][ $method ] = true;
762
				}
763
			}
764
		}
765
766
		return $endpoints;
767
	}
768
769
	/**
770
	 * Retrieves namespaces registered on the server.
771
	 *
772
	 * @since 4.4.0
773
	 * @access public
774
	 *
775
	 * @return array List of registered namespaces.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<integer|string>.

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

Loading history...
776
	 */
777
	public function get_namespaces() {
778
		return array_keys( $this->namespaces );
779
	}
780
781
	/**
782
	 * Retrieves specified options for a route.
783
	 *
784
	 * @since 4.4.0
785
	 * @access public
786
	 *
787
	 * @param string $route Route pattern to fetch options for.
788
	 * @return array|null Data as an associative array if found, or null if not found.
789
	 */
790
	public function get_route_options( $route ) {
791
		if ( ! isset( $this->route_options[ $route ] ) ) {
792
			return null;
793
		}
794
795
		return $this->route_options[ $route ];
796
	}
797
798
	/**
799
	 * Matches the request to a callback and call it.
800
	 *
801
	 * @since 4.4.0
802
	 * @access public
803
	 *
804
	 * @param WP_REST_Request $request Request to attempt dispatching.
805
	 * @return WP_REST_Response Response returned by the callback.
806
	 */
807
	public function dispatch( $request ) {
808
		/**
809
		 * Filters the pre-calculated result of a REST dispatch request.
810
		 *
811
		 * Allow hijacking the request before dispatching by returning a non-empty. The returned value
812
		 * will be used to serve the request instead.
813
		 *
814
		 * @since 4.4.0
815
		 *
816
		 * @param mixed           $result  Response to replace the requested version with. Can be anything
817
		 *                                 a normal endpoint can return, or null to not hijack the request.
818
		 * @param WP_REST_Server  $this    Server instance.
819
		 * @param WP_REST_Request $request Request used to generate the response.
820
		 */
821
		$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
822
823
		if ( ! empty( $result ) ) {
824
			return $result;
825
		}
826
827
		$method = $request->get_method();
828
		$path   = $request->get_route();
829
830
		foreach ( $this->get_routes() as $route => $handlers ) {
831
			$match = preg_match( '@^' . $route . '$@i', $path, $args );
832
833
			if ( ! $match ) {
834
				continue;
835
			}
836
837
			foreach ( $handlers as $handler ) {
838
				$callback  = $handler['callback'];
839
				$response = null;
840
841
				// Fallback to GET method if no HEAD method is registered.
842
				$checked_method = $method;
843
				if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
844
					$checked_method = 'GET';
845
				}
846
				if ( empty( $handler['methods'][ $checked_method ] ) ) {
847
					continue;
848
				}
849
850
				if ( ! is_callable( $callback ) ) {
851
					$response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
852
				}
853
854
				if ( ! is_wp_error( $response ) ) {
855
					// Remove the redundant preg_match argument.
856
					unset( $args[0] );
857
858
					$request->set_url_params( $args );
859
					$request->set_attributes( $handler );
860
861
					$defaults = array();
862
863
					foreach ( $handler['args'] as $arg => $options ) {
864
						if ( isset( $options['default'] ) ) {
865
							$defaults[ $arg ] = $options['default'];
866
						}
867
					}
868
869
					$request->set_default_params( $defaults );
870
871
					$check_required = $request->has_valid_params();
872
					if ( is_wp_error( $check_required ) ) {
873
						$response = $check_required;
874
					} else {
875
						$check_sanitized = $request->sanitize_params();
876
						if ( is_wp_error( $check_sanitized ) ) {
877
							$response = $check_sanitized;
878
						}
879
					}
880
				}
881
882
				/**
883
				 * Filters the response before executing any REST API callbacks.
884
				 *
885
				 * Allows plugins to perform additional validation after a
886
				 * request is initialized and matched to a registered route,
887
				 * but before it is executed.
888
				 *
889
				 * Note that this filter will not be called for requests that
890
				 * fail to authenticate or match to a registered route.
891
				 *
892
				 * @since 4.7.0
893
				 *
894
				 * @param WP_HTTP_Response $response Result to send to the client. Usually a WP_REST_Response.
895
				 * @param WP_REST_Server   $handler  ResponseHandler instance (usually WP_REST_Server).
896
				 * @param WP_REST_Request  $request  Request used to generate the response.
897
				 */
898
				$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
899
900
				if ( ! is_wp_error( $response ) ) {
901
					// Check permission specified on the route.
902
					if ( ! empty( $handler['permission_callback'] ) ) {
903
						$permission = call_user_func( $handler['permission_callback'], $request );
904
905
						if ( is_wp_error( $permission ) ) {
906
							$response = $permission;
907
						} elseif ( false === $permission || null === $permission ) {
908
							$response = new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.' ), array( 'status' => 403 ) );
909
						}
910
					}
911
				}
912
913
				if ( ! is_wp_error( $response ) ) {
914
					/**
915
					 * Filters the REST dispatch request result.
916
					 *
917
					 * Allow plugins to override dispatching the request.
918
					 *
919
					 * @since 4.4.0
920
					 * @since 4.5.0 Added `$route` and `$handler` parameters.
921
					 *
922
					 * @param bool            $dispatch_result Dispatch result, will be used if not empty.
923
					 * @param WP_REST_Request $request         Request used to generate the response.
924
					 * @param string          $route           Route matched for the request.
925
					 * @param array           $handler         Route handler used for the request.
926
					 */
927
					$dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
928
929
					// Allow plugins to halt the request via this filter.
930
					if ( null !== $dispatch_result ) {
931
						$response = $dispatch_result;
932
					} else {
933
						$response = call_user_func( $callback, $request );
934
					}
935
				}
936
937
				/**
938
				 * Filters the response immediately after executing any REST API
939
				 * callbacks.
940
				 *
941
				 * Allows plugins to perform any needed cleanup, for example,
942
				 * to undo changes made during the {@see 'rest_request_before_callbacks'}
943
				 * filter.
944
				 *
945
				 * Note that this filter will not be called for requests that
946
				 * fail to authenticate or match to a registered route.
947
				 *
948
				 * Note that an endpoint's `permission_callback` can still be
949
				 * called after this filter - see `rest_send_allow_header()`.
950
				 *
951
				 * @since 4.7.0
952
				 *
953
				 * @param WP_HTTP_Response $response Result to send to the client. Usually a WP_REST_Response.
954
				 * @param WP_REST_Server   $handler  ResponseHandler instance (usually WP_REST_Server).
955
				 * @param WP_REST_Request  $request  Request used to generate the response.
956
				 */
957
				$response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
958
959
				if ( is_wp_error( $response ) ) {
960
					$response = $this->error_to_response( $response );
961
				} else {
962
					$response = rest_ensure_response( $response );
963
				}
964
965
				$response->set_matched_route( $route );
966
				$response->set_matched_handler( $handler );
967
968
				return $response;
969
			}
970
		}
971
972
		return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
973
	}
974
975
	/**
976
	 * Returns if an error occurred during most recent JSON encode/decode.
977
	 *
978
	 * Strings to be translated will be in format like
979
	 * "Encoding error: Maximum stack depth exceeded".
980
	 *
981
	 * @since 4.4.0
982
	 * @access protected
983
	 *
984
	 * @return bool|string Boolean false or string error message.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use false|string.

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

Loading history...
985
	 */
986
	protected function get_json_last_error() {
987
		// See https://core.trac.wordpress.org/ticket/27799.
988
		if ( ! function_exists( 'json_last_error' ) ) {
989
			return false;
990
		}
991
992
		$last_error_code = json_last_error();
993
994
		if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
995
			return false;
996
		}
997
998
		return json_last_error_msg();
999
	}
1000
1001
	/**
1002
	 * Retrieves the site index.
1003
	 *
1004
	 * This endpoint describes the capabilities of the site.
1005
	 *
1006
	 * @since 4.4.0
1007
	 * @access public
1008
	 *
1009
	 * @param array $request {
1010
	 *     Request.
1011
	 *
1012
	 *     @type string $context Context.
1013
	 * }
1014
	 * @return array Index entity
1015
	 */
1016
	public function get_index( $request ) {
1017
		// General site data.
1018
		$available = array(
1019
			'name'            => get_option( 'blogname' ),
1020
			'description'     => get_option( 'blogdescription' ),
1021
			'url'             => get_option( 'siteurl' ),
1022
			'home'            => home_url(),
1023
			'gmt_offset'      => get_option( 'gmt_offset' ),
1024
			'timezone_string' => get_option( 'timezone_string' ),
1025
			'namespaces'      => array_keys( $this->namespaces ),
1026
			'authentication'  => array(),
1027
			'routes'          => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
1028
		);
1029
1030
		$response = new WP_REST_Response( $available );
1031
1032
		$response->add_link( 'help', 'http://v2.wp-api.org/' );
1033
1034
		/**
1035
		 * Filters the API root index data.
1036
		 *
1037
		 * This contains the data describing the API. This includes information
1038
		 * about supported authentication schemes, supported namespaces, routes
1039
		 * available on the API, and a small amount of data about the site.
1040
		 *
1041
		 * @since 4.4.0
1042
		 *
1043
		 * @param WP_REST_Response $response Response data.
1044
		 */
1045
		return apply_filters( 'rest_index', $response );
1046
	}
1047
1048
	/**
1049
	 * Retrieves the index for a namespace.
1050
	 *
1051
	 * @since 4.4.0
1052
	 * @access public
1053
	 *
1054
	 * @param WP_REST_Request $request REST request instance.
1055
	 * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
1056
	 *                                   WP_Error if the namespace isn't set.
1057
	 */
1058
	public function get_namespace_index( $request ) {
1059
		$namespace = $request['namespace'];
1060
1061
		if ( ! isset( $this->namespaces[ $namespace ] ) ) {
1062
			return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
1063
		}
1064
1065
		$routes = $this->namespaces[ $namespace ];
1066
		$endpoints = array_intersect_key( $this->get_routes(), $routes );
1067
1068
		$data = array(
1069
			'namespace' => $namespace,
1070
			'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
1071
		);
1072
		$response = rest_ensure_response( $data );
1073
1074
		// Link to the root index.
1075
		$response->add_link( 'up', rest_url( '/' ) );
1076
1077
		/**
1078
		 * Filters the namespace index data.
1079
		 *
1080
		 * This typically is just the route data for the namespace, but you can
1081
		 * add any data you'd like here.
1082
		 *
1083
		 * @since 4.4.0
1084
		 *
1085
		 * @param WP_REST_Response $response Response data.
1086
		 * @param WP_REST_Request  $request  Request data. The namespace is passed as the 'namespace' parameter.
1087
		 */
1088
		return apply_filters( 'rest_namespace_index', $response, $request );
1089
	}
1090
1091
	/**
1092
	 * Retrieves the publicly-visible data for routes.
1093
	 *
1094
	 * @since 4.4.0
1095
	 * @access public
1096
	 *
1097
	 * @param array  $routes  Routes to get data for.
1098
	 * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1099
	 * @return array Route data to expose in indexes.
1100
	 */
1101
	public function get_data_for_routes( $routes, $context = 'view' ) {
1102
		$available = array();
1103
1104
		// Find the available routes.
1105
		foreach ( $routes as $route => $callbacks ) {
1106
			$data = $this->get_data_for_route( $route, $callbacks, $context );
1107
			if ( empty( $data ) ) {
1108
				continue;
1109
			}
1110
1111
			/**
1112
			 * Filters the REST endpoint data.
1113
			 *
1114
			 * @since 4.4.0
1115
			 *
1116
			 * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1117
			 */
1118
			$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1119
		}
1120
1121
		/**
1122
		 * Filters the publicly-visible data for routes.
1123
		 *
1124
		 * This data is exposed on indexes and can be used by clients or
1125
		 * developers to investigate the site and find out how to use it. It
1126
		 * acts as a form of self-documentation.
1127
		 *
1128
		 * @since 4.4.0
1129
		 *
1130
		 * @param array $available Map of route to route data.
1131
		 * @param array $routes    Internal route data as an associative array.
1132
		 */
1133
		return apply_filters( 'rest_route_data', $available, $routes );
1134
	}
1135
1136
	/**
1137
	 * Retrieves publicly-visible data for the route.
1138
	 *
1139
	 * @since 4.4.0
1140
	 * @access public
1141
	 *
1142
	 * @param string $route     Route to get data for.
1143
	 * @param array  $callbacks Callbacks to convert to data.
1144
	 * @param string $context   Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1145
	 * @return array|null Data for the route, or null if no publicly-visible data.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use null|array.

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

Loading history...
1146
	 */
1147
	public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1148
		$data = array(
1149
			'namespace' => '',
1150
			'methods' => array(),
1151
			'endpoints' => array(),
1152
		);
1153
1154
		if ( isset( $this->route_options[ $route ] ) ) {
1155
			$options = $this->route_options[ $route ];
1156
1157
			if ( isset( $options['namespace'] ) ) {
1158
				$data['namespace'] = $options['namespace'];
1159
			}
1160
1161
			if ( isset( $options['schema'] ) && 'help' === $context ) {
1162
				$data['schema'] = call_user_func( $options['schema'] );
1163
			}
1164
		}
1165
1166
		$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1167
1168
		foreach ( $callbacks as $callback ) {
1169
			// Skip to the next route if any callback is hidden.
1170
			if ( empty( $callback['show_in_index'] ) ) {
1171
				continue;
1172
			}
1173
1174
			$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1175
			$endpoint_data = array(
1176
				'methods' => array_keys( $callback['methods'] ),
1177
			);
1178
1179
			if ( isset( $callback['args'] ) ) {
1180
				$endpoint_data['args'] = array();
1181
				foreach ( $callback['args'] as $key => $opts ) {
1182
					$arg_data = array(
1183
						'required' => ! empty( $opts['required'] ),
1184
					);
1185
					if ( isset( $opts['default'] ) ) {
1186
						$arg_data['default'] = $opts['default'];
1187
					}
1188
					if ( isset( $opts['enum'] ) ) {
1189
						$arg_data['enum'] = $opts['enum'];
1190
					}
1191
					if ( isset( $opts['description'] ) ) {
1192
						$arg_data['description'] = $opts['description'];
1193
					}
1194
					if ( isset( $opts['type'] ) ) {
1195
						$arg_data['type'] = $opts['type'];
1196
					}
1197
					if ( isset( $opts['items'] ) ) {
1198
						$arg_data['items'] = $opts['items'];
1199
					}
1200
					$endpoint_data['args'][ $key ] = $arg_data;
1201
				}
1202
			}
1203
1204
			$data['endpoints'][] = $endpoint_data;
1205
1206
			// For non-variable routes, generate links.
1207
			if ( strpos( $route, '{' ) === false ) {
1208
				$data['_links'] = array(
1209
					'self' => rest_url( $route ),
1210
				);
1211
			}
1212
		}
1213
1214
		if ( empty( $data['methods'] ) ) {
1215
			// No methods supported, hide the route.
1216
			return null;
1217
		}
1218
1219
		return $data;
1220
	}
1221
1222
	/**
1223
	 * Sends an HTTP status code.
1224
	 *
1225
	 * @since 4.4.0
1226
	 * @access protected
1227
	 *
1228
	 * @param int $code HTTP status.
1229
	 */
1230
	protected function set_status( $code ) {
1231
		status_header( $code );
1232
	}
1233
1234
	/**
1235
	 * Sends an HTTP header.
1236
	 *
1237
	 * @since 4.4.0
1238
	 * @access public
1239
	 *
1240
	 * @param string $key Header key.
1241
	 * @param string $value Header value.
1242
	 */
1243
	public function send_header( $key, $value ) {
1244
		/*
1245
		 * Sanitize as per RFC2616 (Section 4.2):
1246
		 *
1247
		 * Any LWS that occurs between field-content MAY be replaced with a
1248
		 * single SP before interpreting the field value or forwarding the
1249
		 * message downstream.
1250
		 */
1251
		$value = preg_replace( '/\s+/', ' ', $value );
1252
		header( sprintf( '%s: %s', $key, $value ) );
1253
	}
1254
1255
	/**
1256
	 * Sends multiple HTTP headers.
1257
	 *
1258
	 * @since 4.4.0
1259
	 * @access public
1260
	 *
1261
	 * @param array $headers Map of header name to header value.
1262
	 */
1263
	public function send_headers( $headers ) {
1264
		foreach ( $headers as $key => $value ) {
1265
			$this->send_header( $key, $value );
1266
		}
1267
	}
1268
1269
	/**
1270
	 * Removes an HTTP header from the current response.
1271
	 *
1272
	 * @since 4.8.0
1273
	 * @access public
1274
	 *
1275
	 * @param string $key Header key.
1276
	 */
1277
	public function remove_header( $key ) {
1278
		if ( function_exists( 'header_remove' ) ) {
1279
			// In PHP 5.3+ there is a way to remove an already set header.
1280
			header_remove( $key );
1281
		} else {
1282
			// In PHP 5.2, send an empty header, but only as a last resort to
1283
			// override a header already sent.
1284
			foreach ( headers_list() as $header ) {
1285
				if ( 0 === stripos( $header, "$key:" ) ) {
1286
					$this->send_header( $key, '' );
1287
					break;
1288
				}
1289
			}
1290
		}
1291
	}
1292
1293
	/**
1294
	 * Retrieves the raw request entity (body).
1295
	 *
1296
	 * @since 4.4.0
1297
	 * @access public
1298
	 *
1299
	 * @global string $HTTP_RAW_POST_DATA Raw post data.
1300
	 *
1301
	 * @return string Raw request data.
1302
	 */
1303
	public static function get_raw_data() {
1304
		global $HTTP_RAW_POST_DATA;
1305
1306
		/*
1307
		 * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
1308
		 * but we can do it ourself.
1309
		 */
1310
		if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1311
			$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1312
		}
1313
1314
		return $HTTP_RAW_POST_DATA;
1315
	}
1316
1317
	/**
1318
	 * Extracts headers from a PHP-style $_SERVER array.
1319
	 *
1320
	 * @since 4.4.0
1321
	 * @access public
1322
	 *
1323
	 * @param array $server Associative array similar to `$_SERVER`.
1324
	 * @return array Headers extracted from the input.
1325
	 */
1326
	public function get_headers( $server ) {
1327
		$headers = array();
1328
1329
		// CONTENT_* headers are not prefixed with HTTP_.
1330
		$additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
1331
1332
		foreach ( $server as $key => $value ) {
1333
			if ( strpos( $key, 'HTTP_' ) === 0 ) {
1334
				$headers[ substr( $key, 5 ) ] = $value;
1335
			} elseif ( isset( $additional[ $key ] ) ) {
1336
				$headers[ $key ] = $value;
1337
			}
1338
		}
1339
1340
		return $headers;
1341
	}
1342
}
1343