Completed
Pull Request — feature/sync-json-endpoints (#7037)
by
unknown
23:10 queued 11:24
created

class.json-api.php (11 issues)

Upgrade to new PHP Analysis Engine

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

1
<?php
2
3
defined( 'WPCOM_JSON_API__DEBUG' ) or define( 'WPCOM_JSON_API__DEBUG', false );
4
5
require_once dirname( __FILE__ ) . '/sal/class.json-api-platform.php';
6
7
class WPCOM_JSON_API {
8
	static $self = null;
9
10
	public $endpoints = array();
11
12
	public $token_details = array();
13
14
	public $method = '';
15
	public $url = '';
16
	public $path = '';
17
	public $version = null;
18
	public $query = array();
19
	public $post_body = null;
20
	public $files = null;
21
	public $content_type = null;
22
	public $accept = '';
23
24
	public $_server_https;
25
	public $exit = true;
26
	public $public_api_scheme = 'https';
27
28
	public $output_status_code = 200;
29
30
	public $trapped_error = null;
31
	public $did_output = false;
32
33
	/**
34
	 * @return WPCOM_JSON_API instance
35
	 */
36
	static function init( $method = null, $url = null, $post_body = null ) {
37
		if ( !self::$self ) {
38
			$class = function_exists( 'get_called_class' ) ? get_called_class() : __CLASS__;
39
			self::$self = new $class( $method, $url, $post_body );
40
		}
41
		return self::$self;
42
	}
43
44
	function add( WPCOM_JSON_API_Endpoint $endpoint ) {
45
		$path_versions = serialize( array (
46
			$endpoint->path,
47
			$endpoint->min_version,
48
			$endpoint->max_version,
49
		) );
50
		if ( !isset( $this->endpoints[$path_versions] ) ) {
51
			$this->endpoints[$path_versions] = array();
52
		}
53
		$this->endpoints[$path_versions][$endpoint->method] = $endpoint;
54
	}
55
56
	static function is_truthy( $value ) {
57
		switch ( strtolower( (string) $value ) ) {
58
		case '1' :
59
		case 't' :
60
		case 'true' :
61
			return true;
62
		}
63
64
		return false;
65
	}
66
67
	static function is_falsy( $value ) {
68
		switch ( strtolower( (string) $value ) ) {
69
			case '0' :
70
			case 'f' :
71
			case 'false' :
72
				return true;
73
		}
74
75
		return false;
76
	}
77
78
	function __construct() {
79
		$args = func_get_args();
80
		call_user_func_array( array( $this, 'setup_inputs' ), $args );
81
	}
82
83
	function setup_inputs( $method = null, $url = null, $post_body = null ) {
84
		if ( is_null( $method ) ) {
85
			$this->method = strtoupper( $_SERVER['REQUEST_METHOD'] );
86
		} else {
87
			$this->method = strtoupper( $method );
88
		}
89
		if ( is_null( $url ) ) {
90
			$this->url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
91
		} else {
92
			$this->url = $url;
93
		}
94
95
		$parsed     = parse_url( $this->url );
96
		$this->path = $parsed['path'];
97
98
		if ( !empty( $parsed['query'] ) ) {
99
			wp_parse_str( $parsed['query'], $this->query );
100
		}
101
102
		if ( isset( $_SERVER['HTTP_ACCEPT'] ) && $_SERVER['HTTP_ACCEPT'] ) {
103
			$this->accept = $_SERVER['HTTP_ACCEPT'];
104
		}
105
106
		if ( 'POST' === $this->method ) {
107
			if ( is_null( $post_body ) ) {
108
				$this->post_body = file_get_contents( 'php://input' );
109
110
				if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) && $_SERVER['HTTP_CONTENT_TYPE'] ) {
111
					$this->content_type = $_SERVER['HTTP_CONTENT_TYPE'];
112
				} elseif ( isset( $_SERVER['CONTENT_TYPE'] ) && $_SERVER['CONTENT_TYPE'] ) {
113
					$this->content_type = $_SERVER['CONTENT_TYPE'] ;
114
				} elseif ( '{' === $this->post_body[0] ) {
115
					$this->content_type = 'application/json';
116
				} else {
117
					$this->content_type = 'application/x-www-form-urlencoded';
118
				}
119
120
				if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
121
					$this->post_body = http_build_query( stripslashes_deep( $_POST ) );
122
					$this->files = $_FILES;
123
					$this->content_type = 'multipart/form-data';
124
				}
125
			} else {
126
				$this->post_body = $post_body;
127
				$this->content_type = '{' === isset( $this->post_body[0] ) && $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
128
			}
129
		} else {
130
			$this->post_body = null;
131
			$this->content_type = null;
132
		}
133
134
		$this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? $_SERVER['HTTPS'] : '--UNset--';
135
	}
136
137
	function initialize() {
138
		$this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
139
	}
140
141
	function serve( $exit = true ) {
142
		ini_set( 'display_errors', false );
143
144
		$this->exit = (bool) $exit;
145
146
		// This was causing problems with Jetpack, but is necessary for wpcom
147
		// @see https://github.com/Automattic/jetpack/pull/2603
148
		// @see r124548-wpcom
149
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
150
			add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
151
		}
152
153
		add_filter( 'user_can_richedit', '__return_true' );
154
155
		add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
156
157
		$initialization = $this->initialize();
158
		if ( 'OPTIONS' == $this->method ) {
159
			/**
160
			 * Fires before the page output.
161
			 * Can be used to specify custom header options.
162
			 *
163
			 * @module json-api
164
			 *
165
			 * @since 3.1.0
166
			 */
167
			do_action( 'wpcom_json_api_options' );
168
			return $this->output( 200, '', 'plain/text' );
169
		}
170
171
		if ( is_wp_error( $initialization ) ) {
172
			$this->output_error( $initialization );
173
			return;
174
		}
175
176
		// Normalize path and extract API version
177
		$this->path = untrailingslashit( $this->path );
178
		preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
179
		$this->path = substr( $this->path, strlen( $matches[0] ) );
180
		$this->version = $matches[1];
181
182
		$allowed_methods = array( 'GET', 'POST' );
183
		$four_oh_five = false;
184
185
		$is_help = preg_match( '#/help/?$#i', $this->path );
186
		$matching_endpoints = array();
187
188
		if ( $is_help ) {
189
			$origin = get_http_origin();
190
191
			if ( !empty( $origin ) && 'GET' == $this->method ) {
192
				header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
193
			}
194
195
			$this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
196
			// Show help for all matching endpoints regardless of method
197
			$methods = $allowed_methods;
198
			$find_all_matching_endpoints = true;
199
			// How deep to truncate each endpoint's path to see if it matches this help request
200
			$depth = substr_count( $this->path, '/' ) + 1;
201
			if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
202
				$help_content_type = 'json';
203
			} else {
204
				$help_content_type = 'html';
205
			}
206
		} else {
207
			if ( in_array( $this->method, $allowed_methods ) ) {
208
				// Only serve requested method
209
				$methods = array( $this->method );
210
				$find_all_matching_endpoints = false;
211
			} else {
212
				// We don't allow this requested method - find matching endpoints and send 405
213
				$methods = $allowed_methods;
214
				$find_all_matching_endpoints = true;
215
				$four_oh_five = true;
216
			}
217
		}
218
219
		// Find which endpoint to serve
220
		$found = false;
221
		foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
222
			$endpoint_path_versions = unserialize( $endpoint_path_versions );
223
			$endpoint_path        = $endpoint_path_versions[0];
224
			$endpoint_min_version = $endpoint_path_versions[1];
225
			$endpoint_max_version = $endpoint_path_versions[2];
226
227
			// Make sure max_version is not less than min_version
228
			if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
229
				$endpoint_max_version = $endpoint_min_version;
230
			}
231
232
			foreach ( $methods as $method ) {
233
				if ( !isset( $endpoints_by_method[$method] ) ) {
234
					continue;
235
				}
236
237
				// Normalize
238
				$endpoint_path = untrailingslashit( $endpoint_path );
239
				if ( $is_help ) {
240
					// Truncate path at help depth
241
					$endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
242
				}
243
244
				// Generate regular expression from sprintf()
245
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
246
247
				if ( !preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
248
					// This endpoint does not match the requested path.
249
					continue;
250
				}
251
252
				if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
253
					// This endpoint does not match the requested version.
254
					continue;
255
				}
256
257
				$found = true;
258
259
				if ( $find_all_matching_endpoints ) {
260
					$matching_endpoints[] = array( $endpoints_by_method[$method], $path_pieces );
261
				} else {
262
					// The method parameters are now in $path_pieces
263
					$endpoint = $endpoints_by_method[$method];
264
					break 2;
265
				}
266
			}
267
		}
268
269
		if ( !$found ) {
270
			return $this->output( 404, '', 'text/plain' );
271
		}
272
273
		if ( $four_oh_five ) {
274
			$allowed_methods = array();
275
			foreach ( $matching_endpoints as $matching_endpoint ) {
276
				$allowed_methods[] = $matching_endpoint[0]->method;
277
			}
278
279
			header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
280
			return $this->output( 405, array( 'error' => 'not_allowed', 'error_message' => 'Method not allowed' ) );
281
		}
282
283
		if ( $is_help ) {
284
			/**
285
			 * Fires before the API output.
286
			 *
287
			 * @since 1.9.0
288
			 *
289
			 * @param string help.
290
			 */
291
			do_action( 'wpcom_json_api_output', 'help' );
292
			if ( 'json' === $help_content_type ) {
293
				$docs = array();
294 View Code Duplication
				foreach ( $matching_endpoints as $matching_endpoint ) {
295
					if ( $matching_endpoint[0]->is_publicly_documentable() || WPCOM_JSON_API__DEBUG )
296
						$docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
297
				}
298
				return $this->output( 200, $docs );
299
			} else {
300
				status_header( 200 );
301 View Code Duplication
				foreach ( $matching_endpoints as $matching_endpoint ) {
302
					if ( $matching_endpoint[0]->is_publicly_documentable() || WPCOM_JSON_API__DEBUG )
303
						call_user_func( array( $matching_endpoint[0], 'document' ) );
304
				}
305
			}
306
			exit;
307
		}
308
309
		if ( $endpoint->in_testing && !WPCOM_JSON_API__DEBUG ) {
310
			return $this->output( 404, '', 'text/plain' );
311
		}
312
313
		/** This action is documented in class.json-api.php */
314
		do_action( 'wpcom_json_api_output', $endpoint->stat );
315
316
		$response = $this->process_request( $endpoint, $path_pieces );
317
318
		if ( !$response && !is_array( $response ) ) {
319
			return $this->output( 500, '', 'text/plain' );
320
		} elseif ( is_wp_error( $response ) ) {
321
			return $this->output_error( $response );
322
		}
323
324
		$output_status_code = $this->output_status_code;
325
		$this->set_output_status_code();
326
327
		return $this->output( $output_status_code, $response );
328
	}
329
330
	function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
331
		$this->endpoint = $endpoint;
332
		return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
333
	}
334
335
	function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
336
		$exit = $this->exit;
337
		$this->exit = false;
338
		if ( is_wp_error( $response ) )
339
			$this->output_error( $response );
340
		else
341
			$this->output( $status_code, $response, $content_type );
342
		$this->exit = $exit;
343
		if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
344
			$this->finish_request();
345
		}
346
	}
347
348
	function set_output_status_code( $code = 200 ) {
349
		$this->output_status_code = $code;
350
	}
351
352
	function output( $status_code, $response = null, $content_type = 'application/json' ) {
353
		// In case output() was called before the callback returned
354
		if ( $this->did_output ) {
355
			if ( $this->exit )
356
				exit;
357
			return $content_type;
358
		}
359
		$this->did_output = true;
360
361
		// 400s and 404s are allowed for all origins
362
		if ( 404 == $status_code || 400 == $status_code )
363
			header( 'Access-Control-Allow-Origin: *' );
364
365
		if ( is_null( $response ) ) {
366
			$response = new stdClass;
367
		}
368
369
		if ( 'text/plain' === $content_type ) {
370
			status_header( (int) $status_code );
371
			header( 'Content-Type: text/plain' );
372
			echo $response;
373
			if ( $this->exit ) {
374
				exit;
375
			}
376
377
			return $content_type;
378
		}
379
380
		$response = $this->filter_fields( $response );
381
382
		if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
383
			$response = array(
384
				'code' => (int) $status_code,
385
				'headers' => array(
386
					array(
387
						'name' => 'Content-Type',
388
						'value' => $content_type,
389
					),
390
				),
391
				'body' => $response,
392
			);
393
			$status_code = 200;
394
			$content_type = 'application/json';
395
		}
396
397
		status_header( (int) $status_code );
398
		header( "Content-Type: $content_type" );
399
		if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
400
			$callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
401
		} else {
402
			$callback = false;
403
		}
404
405
		if ( $callback ) {
406
			// Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
407
			// and by prepending the JSONP response with a JS comment.
408
			// [1] http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
409
			echo "/**/$callback(";
410
411
		}
412
		echo $this->json_encode( $response );
413
		if ( $callback ) {
414
			echo ");";
415
		}
416
417
		if ( $this->exit ) {
418
			exit;
419
		}
420
421
		return $content_type;
422
	}
423
424
	public static function serializable_error ( $error ) {
425
426
		$status_code = $error->get_error_data();
427
428
		if ( is_array( $status_code ) )
429
			$status_code = $status_code['status_code'];
430
431
		if ( !$status_code ) {
432
			$status_code = 400;
433
		}
434
		$response = array(
435
			'error'   => $error->get_error_code(),
436
			'message' => $error->get_error_message(),
437
		);
438
		return array(
439
			'status_code' => $status_code,
440
			'errors' => $response
441
		);
442
	}
443
444
	function output_error( $error ) {
445
		if ( function_exists( 'bump_stats_extra' ) ) {
446
			$client_id = ! empty( $this->token_details['client_id'] ) ? $this->token_details['client_id'] : 0;
447
			bump_stats_extra( 'rest-api-errors', $client_id );
448
		}
449
450
		$error_response = $this->serializable_error( $error );
451
452
		return $this->output( $error_response[ 'status_code'], $error_response['errors'] );
453
	}
454
455
	function filter_fields( $response ) {
456
		if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) )
457
			return $response;
458
459
		$fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
460
461
		if ( is_object( $response ) ) {
462
			$response = (array) $response;
463
		}
464
465
		$has_filtered = false;
466
		if ( is_array( $response ) && empty( $response['ID'] ) ) {
467
			$keys_to_filter = array(
468
				'categories',
469
				'comments',
470
				'connections',
471
				'domains',
472
				'groups',
473
				'likes',
474
				'media',
475
				'notes',
476
				'posts',
477
				'services',
478
				'sites',
479
				'suggestions',
480
				'tags',
481
				'themes',
482
				'topics',
483
				'users',
484
			);
485
486
			foreach ( $keys_to_filter as $key_to_filter ) {
487
				if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered )
488
					continue;
489
490
				foreach ( $response[ $key_to_filter ] as $key => $values ) {
491
					if ( is_object( $values ) ) {
492
						$response[ $key_to_filter ][ $key ] = (object) array_intersect_key( (array) $values, array_flip( $fields ) );
493
					} elseif ( is_array( $values ) ) {
494
						$response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
495
					}
496
				}
497
498
				$has_filtered = true;
499
			}
500
		}
501
502
		if ( ! $has_filtered ) {
503
			if ( is_object( $response ) ) {
504
				$response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
505
			} else if ( is_array( $response ) ) {
506
				$response = array_intersect_key( $response, array_flip( $fields ) );
507
			}
508
		}
509
510
		return $response;
511
	}
512
513
	function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
514
		if ( $original_scheme ) {
515
			return $url;
516
		}
517
518
		return preg_replace( '#^https:#', 'http:', $url );
519
	}
520
521
	function comment_edit_pre( $comment_content ) {
522
		return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
523
	}
524
525
	function json_encode( $data ) {
526
		return json_encode( $data );
527
	}
528
529
	function ends_with( $haystack, $needle ) {
530
		return $needle === substr( $haystack, -strlen( $needle ) );
531
	}
532
533
	// Returns the site's blog_id in the WP.com ecosystem
534
	function get_blog_id_for_output() {
535
		return $this->token_details['blog_id'];
536
	}
537
538
	// Returns the site's local blog_id
539
	function get_blog_id( $blog_id ) {
0 ignored issues
show
The parameter $blog_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
540
		return $GLOBALS['blog_id'];
541
	}
542
543
	function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
0 ignored issues
show
The parameter $verify_token_for_blog is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
544
		if ( $this->is_restricted_blog( $blog_id ) ) {
545
			return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
546
		}
547
548
		if ( -1 == get_option( 'blog_public' ) && !current_user_can( 'read' ) ) {
549
			return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
550
		}
551
552
		return $blog_id;
553
	}
554
555
	// Returns true if the specified blog ID is a restricted blog
556
	function is_restricted_blog( $blog_id ) {
557
		/**
558
		 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
559
		 *
560
		 * @module json-api
561
		 *
562
		 * @since 3.4.0
563
		 *
564
		 * @param array $array Array of Blog IDs.
565
		 */
566
		$restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
567
		return true === in_array( $blog_id, $restricted_blog_ids );
568
	}
569
570
	function post_like_count( $blog_id, $post_id ) {
0 ignored issues
show
The parameter $blog_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $post_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
571
		return 0;
572
	}
573
574
	function is_liked( $blog_id, $post_id ) {
0 ignored issues
show
The parameter $blog_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $post_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
575
		return false;
576
	}
577
578
	function is_reblogged( $blog_id, $post_id ) {
0 ignored issues
show
The parameter $blog_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $post_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
579
		return false;
580
	}
581
582
	function is_following( $blog_id ) {
0 ignored issues
show
The parameter $blog_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
583
		return false;
584
	}
585
586
	function add_global_ID( $blog_id, $post_id ) {
0 ignored issues
show
The parameter $blog_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
The parameter $post_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
587
		return '';
588
	}
589
590
	/**
591
	 * Traps `wp_die()` calls and outputs a JSON response instead.
592
	 * The result is always output, never returned.
593
	 *
594
	 * @param string|null $error_code  Call with string to start the trapping.  Call with null to stop.
595
	 */
596
	function trap_wp_die( $error_code = null ) {
597
		// Stop trapping
598
		if ( is_null( $error_code ) ) {
599
			$this->trapped_error = null;
600
			remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
601
			return;
602
		}
603
604
		// If API called via PHP, bail: don't do our custom wp_die().  Do the normal wp_die().
605
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
606
			if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
607
				return;
608
			}
609
		} else {
610
			if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
611
				return;
612
			}
613
		}
614
615
		// Start trapping
616
		$this->trapped_error = array(
617
			'status'  => 500,
618
			'code'    => $error_code,
619
			'message' => '',
620
		);
621
622
		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
623
	}
624
625
	function wp_die_handler_callback() {
626
		return array( $this, 'wp_die_handler' );
627
	}
628
629
	function wp_die_handler( $message, $title = '', $args = array() ) {
630
		$args = wp_parse_args( $args, array(
631
			'response' => 500,
632
		) );
633
634
		if ( $title ) {
635
			$message = "$title: $message";
636
		}
637
638
		switch ( $this->trapped_error['code'] ) {
639
		case 'comment_failure' :
640
			if ( did_action( 'comment_duplicate_trigger' ) ) {
641
				$this->trapped_error['code'] = 'comment_duplicate';
642
			} else if ( did_action( 'comment_flood_trigger' ) ) {
643
				$this->trapped_error['code'] = 'comment_flood';
644
			}
645
			break;
646
		}
647
648
		$this->trapped_error['status']  = $args['response'];
649
		$this->trapped_error['message'] = wp_kses( $message, array() );
650
651
		// We still want to exit so that code execution stops where it should.
652
		// Attach the JSON output to WordPress' shutdown handler
653
		add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
654
		exit;
655
	}
656
657
	function output_trapped_error() {
658
		$this->exit = false; // We're already exiting once.  Don't do it twice.
659
		$this->output( $this->trapped_error['status'], (object) array(
660
			'error'   => $this->trapped_error['code'],
661
			'message' => $this->trapped_error['message'],
662
		) );
663
	}
664
665
	function finish_request() {
666
		if ( function_exists( 'fastcgi_finish_request' ) )
667
			return fastcgi_finish_request();
668
	}
669
}
670