Completed
Push — fix/untranslated-module-names ( 4226a9...c5f2cb )
by
unknown
11:13
created

class.json-api.php (1 issue)

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

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

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