Completed
Push — update/api-blog-token-needed-p... ( d85f87 )
by
unknown
40:39 queued 32:33
created

class.json-api.php (1 issue)

Labels
Severity

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

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

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