Completed
Push — update/build-release-branch-sc... ( 8e05aa...6a7f53 )
by
unknown
277:00 queued 265:52
created

class.json-api.php (2 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
	public $extra_headers = array();
34
35
	/**
36
	 * @return WPCOM_JSON_API instance
37
	 */
38
	static function init( $method = null, $url = null, $post_body = null ) {
39
		if ( !self::$self ) {
40
			$class = function_exists( 'get_called_class' ) ? get_called_class() : __CLASS__;
41
			self::$self = new $class( $method, $url, $post_body );
42
		}
43
		return self::$self;
44
	}
45
46
	function add( WPCOM_JSON_API_Endpoint $endpoint ) {
47
		$path_versions = serialize( array (
48
			$endpoint->path,
49
			$endpoint->min_version,
50
			$endpoint->max_version,
51
		) );
52
		if ( !isset( $this->endpoints[$path_versions] ) ) {
53
			$this->endpoints[$path_versions] = array();
54
		}
55
		$this->endpoints[$path_versions][$endpoint->method] = $endpoint;
56
	}
57
58
	static function is_truthy( $value ) {
59
		switch ( strtolower( (string) $value ) ) {
60
		case '1' :
61
		case 't' :
62
		case 'true' :
63
			return true;
64
		}
65
66
		return false;
67
	}
68
69
	static function is_falsy( $value ) {
70
		switch ( strtolower( (string) $value ) ) {
71
			case '0' :
72
			case 'f' :
73
			case 'false' :
74
				return true;
75
		}
76
77
		return false;
78
	}
79
80
	function __construct() {
81
		$args = func_get_args();
82
		call_user_func_array( array( $this, 'setup_inputs' ), $args );
83
	}
84
85
	function setup_inputs( $method = null, $url = null, $post_body = null ) {
86
		if ( is_null( $method ) ) {
87
			$this->method = strtoupper( $_SERVER['REQUEST_METHOD'] );
88
		} else {
89
			$this->method = strtoupper( $method );
90
		}
91
		if ( is_null( $url ) ) {
92
			$this->url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
93
		} else {
94
			$this->url = $url;
95
		}
96
97
		$parsed     = parse_url( $this->url );
98
		$this->path = $parsed['path'];
99
100
		if ( !empty( $parsed['query'] ) ) {
101
			wp_parse_str( $parsed['query'], $this->query );
102
		}
103
104
		if ( isset( $_SERVER['HTTP_ACCEPT'] ) && $_SERVER['HTTP_ACCEPT'] ) {
105
			$this->accept = $_SERVER['HTTP_ACCEPT'];
106
		}
107
108
		if ( 'POST' === $this->method ) {
109
			if ( is_null( $post_body ) ) {
110
				$this->post_body = file_get_contents( 'php://input' );
111
112
				if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) && $_SERVER['HTTP_CONTENT_TYPE'] ) {
113
					$this->content_type = $_SERVER['HTTP_CONTENT_TYPE'];
114
				} elseif ( isset( $_SERVER['CONTENT_TYPE'] ) && $_SERVER['CONTENT_TYPE'] ) {
115
					$this->content_type = $_SERVER['CONTENT_TYPE'] ;
116
				} elseif ( '{' === $this->post_body[0] ) {
117
					$this->content_type = 'application/json';
118
				} else {
119
					$this->content_type = 'application/x-www-form-urlencoded';
120
				}
121
122
				if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
123
					$this->post_body = http_build_query( stripslashes_deep( $_POST ) );
124
					$this->files = $_FILES;
125
					$this->content_type = 'multipart/form-data';
126
				}
127
			} else {
128
				$this->post_body = $post_body;
129
				$this->content_type = '{' === isset( $this->post_body[0] ) && $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
130
			}
131
		} else {
132
			$this->post_body = null;
133
			$this->content_type = null;
134
		}
135
136
		$this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? $_SERVER['HTTPS'] : '--UNset--';
137
	}
138
139
	function initialize() {
140
		$this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
141
	}
142
143
	function serve( $exit = true ) {
144
		ini_set( 'display_errors', false );
145
146
		$this->exit = (bool) $exit;
147
148
		// This was causing problems with Jetpack, but is necessary for wpcom
149
		// @see https://github.com/Automattic/jetpack/pull/2603
150
		// @see r124548-wpcom
151
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
152
			add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
153
		}
154
155
		add_filter( 'user_can_richedit', '__return_true' );
156
157
		add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
158
159
		$initialization = $this->initialize();
160
		if ( 'OPTIONS' == $this->method ) {
161
			/**
162
			 * Fires before the page output.
163
			 * Can be used to specify custom header options.
164
			 *
165
			 * @module json-api
166
			 *
167
			 * @since 3.1.0
168
			 */
169
			do_action( 'wpcom_json_api_options' );
170
			return $this->output( 200, '', 'text/plain' );
171
		}
172
173
		if ( is_wp_error( $initialization ) ) {
174
			$this->output_error( $initialization );
175
			return;
176
		}
177
178
		// Normalize path and extract API version
179
		$this->path = untrailingslashit( $this->path );
180
		preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
181
		$this->path = substr( $this->path, strlen( $matches[0] ) );
182
		$this->version = $matches[1];
183
184
		$allowed_methods = array( 'GET', 'POST' );
185
		$four_oh_five = false;
186
187
		$is_help = preg_match( '#/help/?$#i', $this->path );
188
		$matching_endpoints = array();
189
190
		if ( $is_help ) {
191
			$origin = get_http_origin();
192
193
			if ( !empty( $origin ) && 'GET' == $this->method ) {
194
				header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
195
			}
196
197
			$this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
198
			// Show help for all matching endpoints regardless of method
199
			$methods = $allowed_methods;
200
			$find_all_matching_endpoints = true;
201
			// How deep to truncate each endpoint's path to see if it matches this help request
202
			$depth = substr_count( $this->path, '/' ) + 1;
203
			if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
204
				$help_content_type = 'json';
205
			} else {
206
				$help_content_type = 'html';
207
			}
208
		} else {
209
			if ( in_array( $this->method, $allowed_methods ) ) {
210
				// Only serve requested method
211
				$methods = array( $this->method );
212
				$find_all_matching_endpoints = false;
213
			} else {
214
				// We don't allow this requested method - find matching endpoints and send 405
215
				$methods = $allowed_methods;
216
				$find_all_matching_endpoints = true;
217
				$four_oh_five = true;
218
			}
219
		}
220
221
		// Find which endpoint to serve
222
		$found = false;
223
		foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
224
			$endpoint_path_versions = unserialize( $endpoint_path_versions );
225
			$endpoint_path        = $endpoint_path_versions[0];
226
			$endpoint_min_version = $endpoint_path_versions[1];
227
			$endpoint_max_version = $endpoint_path_versions[2];
228
229
			// Make sure max_version is not less than min_version
230
			if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
231
				$endpoint_max_version = $endpoint_min_version;
232
			}
233
234
			foreach ( $methods as $method ) {
235
				if ( !isset( $endpoints_by_method[$method] ) ) {
236
					continue;
237
				}
238
239
				// Normalize
240
				$endpoint_path = untrailingslashit( $endpoint_path );
241
				if ( $is_help ) {
242
					// Truncate path at help depth
243
					$endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
244
				}
245
246
				// Generate regular expression from sprintf()
247
				$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
248
249
				if ( !preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
250
					// This endpoint does not match the requested path.
251
					continue;
252
				}
253
254
				if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
255
					// This endpoint does not match the requested version.
256
					continue;
257
				}
258
259
				$found = true;
260
261
				if ( $find_all_matching_endpoints ) {
262
					$matching_endpoints[] = array( $endpoints_by_method[$method], $path_pieces );
263
				} else {
264
					// The method parameters are now in $path_pieces
265
					$endpoint = $endpoints_by_method[$method];
266
					break 2;
267
				}
268
			}
269
		}
270
271
		if ( !$found ) {
272
			return $this->output( 404, '', 'text/plain' );
273
		}
274
275
		if ( $four_oh_five ) {
276
			$allowed_methods = array();
277
			foreach ( $matching_endpoints as $matching_endpoint ) {
278
				$allowed_methods[] = $matching_endpoint[0]->method;
279
			}
280
281
			header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
282
			return $this->output( 405, array( 'error' => 'not_allowed', 'error_message' => 'Method not allowed' ) );
283
		}
284
285
		if ( $is_help ) {
286
			/**
287
			 * Fires before the API output.
288
			 *
289
			 * @since 1.9.0
290
			 *
291
			 * @param string help.
292
			 */
293
			do_action( 'wpcom_json_api_output', 'help' );
294
			$proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
295
			if ( 'json' === $help_content_type ) {
296
				$docs = array();
297
				foreach ( $matching_endpoints as $matching_endpoint ) {
298
					if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG )
299
						$docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
300
				}
301
				return $this->output( 200, $docs );
302
			} else {
303
				status_header( 200 );
304
				foreach ( $matching_endpoints as $matching_endpoint ) {
305
					if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG )
306
						call_user_func( array( $matching_endpoint[0], 'document' ) );
307
				}
308
			}
309
			exit;
310
		}
311
312
		if ( $endpoint->in_testing && !WPCOM_JSON_API__DEBUG ) {
313
			return $this->output( 404, '', 'text/plain' );
314
		}
315
316
		/** This action is documented in class.json-api.php */
317
		do_action( 'wpcom_json_api_output', $endpoint->stat );
318
319
		$response = $this->process_request( $endpoint, $path_pieces );
320
321
		if ( !$response && !is_array( $response ) ) {
322
			return $this->output( 500, '', 'text/plain' );
323
		} elseif ( is_wp_error( $response ) ) {
324
			return $this->output_error( $response );
325
		}
326
327
		$output_status_code = $this->output_status_code;
328
		$this->set_output_status_code();
329
330
		return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
331
	}
332
333
	function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
334
		$this->endpoint = $endpoint;
335
		return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
336
	}
337
338
	function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
339
		$exit = $this->exit;
340
		$this->exit = false;
341
		if ( is_wp_error( $response ) )
342
			$this->output_error( $response );
343
		else
344
			$this->output( $status_code, $response, $content_type );
345
		$this->exit = $exit;
346
		if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
347
			$this->finish_request();
348
		}
349
	}
350
351
	function set_output_status_code( $code = 200 ) {
352
		$this->output_status_code = $code;
353
	}
354
355
	function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
356
		// In case output() was called before the callback returned
357
		if ( $this->did_output ) {
358
			if ( $this->exit )
359
				exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
360
			return $content_type;
361
		}
362
		$this->did_output = true;
363
364
		// 400s and 404s are allowed for all origins
365
		if ( 404 == $status_code || 400 == $status_code )
366
			header( 'Access-Control-Allow-Origin: *' );
367
368
		if ( is_null( $response ) ) {
369
			$response = new stdClass;
370
		}
371
372
		if ( 'text/plain' === $content_type ) {
373
			status_header( (int) $status_code );
374
			header( 'Content-Type: text/plain' );
375
			foreach( $extra as $key => $value ) {
376
				header( "$key: $value" );
377
			}
378
			echo $response;
379
			if ( $this->exit ) {
380
				exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method output() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
381
			}
382
383
			return $content_type;
384
		}
385
386
		$response = $this->filter_fields( $response );
387
388
		if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
389
			$headers = array(
390
				array(
391
					'name' => 'Content-Type',
392
					'value' => $content_type,
393
				)
394
			);
395
			
396
			foreach( $extra as $key => $value ) {
397
				$headers[] = array( 'name' => $key, 'value' => $value );
398
			}
399
400
			$response = array(
401
				'code' => (int) $status_code,
402
				'headers' => $headers,
403
				'body' => $response,
404
			);
405
			$status_code = 200;
406
			$content_type = 'application/json';
407
		}
408
409
		status_header( (int) $status_code );
410
		header( "Content-Type: $content_type" );
411
		if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
412
			$callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
413
		} else {
414
			$callback = false;
415
		}
416
417
		if ( $callback ) {
418
			// Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
419
			// and by prepending the JSONP response with a JS comment.
420
			// [1] http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
421
			echo "/**/$callback(";
422
423
		}
424
		echo $this->json_encode( $response );
425
		if ( $callback ) {
426
			echo ");";
427
		}
428
429
		if ( $this->exit ) {
430
			exit;
431
		}
432
433
		return $content_type;
434
	}
435
436
	public static function serializable_error ( $error ) {
437
438
		$status_code = $error->get_error_data();
439
440
		if ( is_array( $status_code ) )
441
			$status_code = $status_code['status_code'];
442
443
		if ( !$status_code ) {
444
			$status_code = 400;
445
		}
446
		$response = array(
447
			'error'   => $error->get_error_code(),
448
			'message' => $error->get_error_message(),
449
		);
450
		return array(
451
			'status_code' => $status_code,
452
			'errors' => $response
453
		);
454
	}
455
456
	function output_error( $error ) {
457
		$error_response = $this->serializable_error( $error );
458
459
		return $this->output( $error_response[ 'status_code'], $error_response['errors'] );
460
	}
461
462
	function filter_fields( $response ) {
463
		if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) )
464
			return $response;
465
466
		$fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
467
468
		if ( is_object( $response ) ) {
469
			$response = (array) $response;
470
		}
471
472
		$has_filtered = false;
473
		if ( is_array( $response ) && empty( $response['ID'] ) ) {
474
			$keys_to_filter = array(
475
				'categories',
476
				'comments',
477
				'connections',
478
				'domains',
479
				'groups',
480
				'likes',
481
				'media',
482
				'notes',
483
				'posts',
484
				'services',
485
				'sites',
486
				'suggestions',
487
				'tags',
488
				'themes',
489
				'topics',
490
				'users',
491
			);
492
493
			foreach ( $keys_to_filter as $key_to_filter ) {
494
				if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered )
495
					continue;
496
497
				foreach ( $response[ $key_to_filter ] as $key => $values ) {
498
					if ( is_object( $values ) ) {
499
						$response[ $key_to_filter ][ $key ] = (object) array_intersect_key( (array) $values, array_flip( $fields ) );
500
					} elseif ( is_array( $values ) ) {
501
						$response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
502
					}
503
				}
504
505
				$has_filtered = true;
506
			}
507
		}
508
509
		if ( ! $has_filtered ) {
510
			if ( is_object( $response ) ) {
511
				$response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
512
			} else if ( is_array( $response ) ) {
513
				$response = array_intersect_key( $response, array_flip( $fields ) );
514
			}
515
		}
516
517
		return $response;
518
	}
519
520
	function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
521
		if ( $original_scheme ) {
522
			return $url;
523
		}
524
525
		return preg_replace( '#^https:#', 'http:', $url );
526
	}
527
528
	function comment_edit_pre( $comment_content ) {
529
		return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
530
	}
531
532
	function json_encode( $data ) {
533
		return json_encode( $data );
534
	}
535
536
	function ends_with( $haystack, $needle ) {
537
		return $needle === substr( $haystack, -strlen( $needle ) );
538
	}
539
540
	// Returns the site's blog_id in the WP.com ecosystem
541
	function get_blog_id_for_output() {
542
		return $this->token_details['blog_id'];
543
	}
544
545
	// Returns the site's local blog_id
546
	function get_blog_id( $blog_id ) {
547
		return $GLOBALS['blog_id'];
548
	}
549
550
	function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
551
		if ( $this->is_restricted_blog( $blog_id ) ) {
552
			return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
553
		}
554
555
		if ( -1 == get_option( 'blog_public' ) && !current_user_can( 'read' ) ) {
556
			return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
557
		}
558
559
		return $blog_id;
560
	}
561
562
	// Returns true if the specified blog ID is a restricted blog
563
	function is_restricted_blog( $blog_id ) {
564
		/**
565
		 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
566
		 *
567
		 * @module json-api
568
		 *
569
		 * @since 3.4.0
570
		 *
571
		 * @param array $array Array of Blog IDs.
572
		 */
573
		$restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
574
		return true === in_array( $blog_id, $restricted_blog_ids );
575
	}
576
577
	function post_like_count( $blog_id, $post_id ) {
578
		return 0;
579
	}
580
581
	function is_liked( $blog_id, $post_id ) {
582
		return false;
583
	}
584
585
	function is_reblogged( $blog_id, $post_id ) {
586
		return false;
587
	}
588
589
	function is_following( $blog_id ) {
590
		return false;
591
	}
592
593
	function add_global_ID( $blog_id, $post_id ) {
594
		return '';
595
	}
596
597
	function get_avatar_url( $email, $avatar_size = null ) {
598
		if ( function_exists( 'wpcom_get_avatar_url' ) ) {
599
			return null === $avatar_size
600
				? wpcom_get_avatar_url( $email )
601
				: wpcom_get_avatar_url( $email, $avatar_size );
602
		} else {
603
			return null === $avatar_size
604
				? get_avatar_url( $email )
605
				: get_avatar_url( $email, $avatar_size );
606
		}
607
	}
608
609
	/**
610
	 * traps `wp_die()` calls and outputs a JSON response instead.
611
	 * The result is always output, never returned.
612
	 *
613
	 * @param string|null $error_code  Call with string to start the trapping.  Call with null to stop.
614
	 * @param int         $http_status  HTTP status code, 400 by default.
615
	 */
616
	function trap_wp_die( $error_code = null, $http_status = 400 ) {
617
		if ( is_null( $error_code ) ) {
618
			$this->trapped_error = null;
619
			// Stop trapping
620
			remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
621
			return;
622
		}
623
624
		// If API called via PHP, bail: don't do our custom wp_die().  Do the normal wp_die().
625
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
626
			if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
627
				return;
628
			}
629
		} else {
630
			if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
631
				return;
632
			}
633
		}
634
635
		$this->trapped_error = array(
636
			'status'  => $http_status,
637
			'code'    => $error_code,
638
			'message' => '',
639
		);
640
		// Start trapping
641
		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
642
	}
643
644
	function wp_die_handler_callback() {
645
		return array( $this, 'wp_die_handler' );
646
	}
647
648
	function wp_die_handler( $message, $title = '', $args = array() ) {
649
		// Allow wp_die calls to override HTTP status code...
650
		$args = wp_parse_args( $args, array(
651
			'response' => $this->trapped_error['status'],
652
		) );
653
654
		// ... unless it's 500 ( see http://wp.me/pMz3w-5VV )
655
		if ( (int) $args['response'] !== 500 ) {
656
			$this->trapped_error['status'] = $args['response'];
657
		}
658
659
		if ( $title ) {
660
			$message = "$title: $message";
661
		}
662
663
		$this->trapped_error['message'] = wp_kses( $message, array() );
664
665
		switch ( $this->trapped_error['code'] ) {
666
			case 'comment_failure' :
667
				if ( did_action( 'comment_duplicate_trigger' ) ) {
668
					$this->trapped_error['code'] = 'comment_duplicate';
669
				} else if ( did_action( 'comment_flood_trigger' ) ) {
670
					$this->trapped_error['code'] = 'comment_flood';
671
				}
672
				break;
673
		}
674
675
		// We still want to exit so that code execution stops where it should.
676
		// Attach the JSON output to the WordPress shutdown handler
677
		add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
678
		exit;
679
	}
680
681
	function output_trapped_error() {
682
		$this->exit = false; // We're already exiting once.  Don't do it twice.
683
		$this->output( $this->trapped_error['status'], (object) array(
684
			'error'   => $this->trapped_error['code'],
685
			'message' => $this->trapped_error['message'],
686
		) );
687
	}
688
689
	function finish_request() {
690
		if ( function_exists( 'fastcgi_finish_request' ) )
691
			return fastcgi_finish_request();
692
	}
693
}
694