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

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
565
		}
566
567
		if ( -1 == get_option( 'blog_public' ) && !current_user_can( 'read' ) ) {
568
			return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
0 ignored issues
show
The call to WP_Error::__construct() has too many arguments starting with 'unauthorized'.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
569
		}
570
571
		return $blog_id;
572
	}
573
574
	// Returns true if the specified blog ID is a restricted blog
575
	function is_restricted_blog( $blog_id ) {
576
		/**
577
		 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
578
		 *
579
		 * @module json-api
580
		 *
581
		 * @since 3.4.0
582
		 *
583
		 * @param array $array Array of Blog IDs.
584
		 */
585
		$restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
586
		return true === in_array( $blog_id, $restricted_blog_ids );
587
	}
588
589
	function post_like_count( $blog_id, $post_id ) {
590
		return 0;
591
	}
592
593
	function is_liked( $blog_id, $post_id ) {
594
		return false;
595
	}
596
597
	function is_reblogged( $blog_id, $post_id ) {
598
		return false;
599
	}
600
601
	function is_following( $blog_id ) {
602
		return false;
603
	}
604
605
	function add_global_ID( $blog_id, $post_id ) {
606
		return '';
607
	}
608
609
	function get_avatar_url( $email, $avatar_size = null ) {
610
		if ( function_exists( 'wpcom_get_avatar_url' ) ) {
611
			return null === $avatar_size
612
				? wpcom_get_avatar_url( $email )
613
				: wpcom_get_avatar_url( $email, $avatar_size );
614
		} else {
615
			return null === $avatar_size
616
				? get_avatar_url( $email )
617
				: get_avatar_url( $email, $avatar_size );
618
		}
619
	}
620
621
	/**
622
	 * Counts the number of comments on a site, excluding certain comment types.
623
	 *
624
	 * @param $post_id int Post ID.
625
	 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
626
	 */
627
	public function wp_count_comments( $post_id ) {
628
		global $wpdb;
629
		if ( 0 !== $post_id ) {
630
			return wp_count_comments( $post_id );
631
		}
632
633
		$counts = array(
634
			'total_comments' => 0,
635
			'all'            => 0,
636
		);
637
638
		/**
639
		 * Exclude certain comment types from comment counts in the REST API.
640
		 *
641
		 * @since 6.9.0
642
		 * @module json-api
643
		 *
644
		 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
645
		 */
646
		$exclude = apply_filters( 'jetpack_api_exclude_comment_types_count',
647
			array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
648
		);
649
650
		if ( empty( $exclude ) ) {
651
			return wp_count_comments( $post_id );
652
		}
653
654
		array_walk( $exclude, 'esc_sql' );
655
		$where = sprintf(
656
			"WHERE comment_type NOT IN ( '%s' )",
657
			implode( "','", $exclude )
658
		);
659
660
		$count = $wpdb->get_results(
661
			"SELECT comment_approved, COUNT(*) AS num_comments
662
				FROM $wpdb->comments
663
				{$where}
664
				GROUP BY comment_approved
665
			"
666
		);
667
668
		$approved = array(
669
			'0'            => 'moderated',
670
			'1'            => 'approved',
671
			'spam'         => 'spam',
672
			'trash'        => 'trash',
673
			'post-trashed' => 'post-trashed',
674
		);
675
676
		// https://developer.wordpress.org/reference/functions/get_comment_count/#source
677
		foreach ( $count as $row ) {
678
			if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
679
				$counts['all']            += $row->num_comments;
680
				$counts['total_comments'] += $row->num_comments;
681
			} elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
682
				$counts['total_comments'] += $row->num_comments;
683
			}
684
			if ( isset( $approved[ $row->comment_approved ] ) ) {
685
				$counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
686
			}
687
		}
688
689
		foreach ( $approved as $key ) {
690
			if ( empty( $counts[ $key ] ) ) {
691
				$counts[ $key ] = 0;
692
			}
693
		}
694
695
		$counts = (object) $counts;
696
697
		return $counts;
698
	}
699
700
	/**
701
	 * traps `wp_die()` calls and outputs a JSON response instead.
702
	 * The result is always output, never returned.
703
	 *
704
	 * @param string|null $error_code  Call with string to start the trapping.  Call with null to stop.
705
	 * @param int         $http_status  HTTP status code, 400 by default.
706
	 */
707
	function trap_wp_die( $error_code = null, $http_status = 400 ) {
708
		if ( is_null( $error_code ) ) {
709
			$this->trapped_error = null;
710
			// Stop trapping
711
			remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
712
			return;
713
		}
714
715
		// If API called via PHP, bail: don't do our custom wp_die().  Do the normal wp_die().
716
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
717
			if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
718
				return;
719
			}
720
		} else {
721
			if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
722
				return;
723
			}
724
		}
725
726
		$this->trapped_error = array(
727
			'status'  => $http_status,
728
			'code'    => $error_code,
729
			'message' => '',
730
		);
731
		// Start trapping
732
		add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
733
	}
734
735
	function wp_die_handler_callback() {
736
		return array( $this, 'wp_die_handler' );
737
	}
738
739
	function wp_die_handler( $message, $title = '', $args = array() ) {
740
		// Allow wp_die calls to override HTTP status code...
741
		$args = wp_parse_args( $args, array(
742
			'response' => $this->trapped_error['status'],
743
		) );
744
745
		// ... unless it's 500 ( see http://wp.me/pMz3w-5VV )
746
		if ( (int) $args['response'] !== 500 ) {
747
			$this->trapped_error['status'] = $args['response'];
748
		}
749
750
		if ( $title ) {
751
			$message = "$title: $message";
752
		}
753
754
		$this->trapped_error['message'] = wp_kses( $message, array() );
755
756
		switch ( $this->trapped_error['code'] ) {
757
			case 'comment_failure' :
758
				if ( did_action( 'comment_duplicate_trigger' ) ) {
759
					$this->trapped_error['code'] = 'comment_duplicate';
760
				} else if ( did_action( 'comment_flood_trigger' ) ) {
761
					$this->trapped_error['code'] = 'comment_flood';
762
				}
763
				break;
764
		}
765
766
		// We still want to exit so that code execution stops where it should.
767
		// Attach the JSON output to the WordPress shutdown handler
768
		add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
769
		exit;
770
	}
771
772
	function output_trapped_error() {
773
		$this->exit = false; // We're already exiting once.  Don't do it twice.
774
		$this->output( $this->trapped_error['status'], (object) array(
775
			'error'   => $this->trapped_error['code'],
776
			'message' => $this->trapped_error['message'],
777
		) );
778
	}
779
780
	function finish_request() {
781
		if ( function_exists( 'fastcgi_finish_request' ) )
782
			return fastcgi_finish_request();
783
	}
784
}
785