PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 7.8.3
Jetpack – WP Security, Backup, Speed, & Growth v7.8.3
16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 All 501 releases
jetpack / class.json-api.php
class.json-api.php
811 lines 22.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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(
48 array(
49 $endpoint->path,
50 $endpoint->min_version,
51 $endpoint->max_version,
52 )
53 );
54 if ( ! isset( $this->endpoints[ $path_versions ] ) ) {
55 $this->endpoints[ $path_versions ] = array();
56 }
57 $this->endpoints[ $path_versions ][ $endpoint->method ] = $endpoint;
58 }
59
60 static function is_truthy( $value ) {
61 switch ( strtolower( (string) $value ) ) {
62 case '1':
63 case 't':
64 case 'true':
65 return true;
66 }
67
68 return false;
69 }
70
71 static function is_falsy( $value ) {
72 switch ( strtolower( (string) $value ) ) {
73 case '0':
74 case 'f':
75 case 'false':
76 return true;
77 }
78
79 return false;
80 }
81
82 function __construct() {
83 $args = func_get_args();
84 call_user_func_array( array( $this, 'setup_inputs' ), $args );
85 }
86
87 function setup_inputs( $method = null, $url = null, $post_body = null ) {
88 if ( is_null( $method ) ) {
89 $this->method = strtoupper( $_SERVER['REQUEST_METHOD'] );
90 } else {
91 $this->method = strtoupper( $method );
92 }
93 if ( is_null( $url ) ) {
94 $this->url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
95 } else {
96 $this->url = $url;
97 }
98
99 $parsed = parse_url( $this->url );
100 if ( ! empty( $parsed['path'] ) ) {
101 $this->path = $parsed['path'];
102 }
103
104 if ( ! empty( $parsed['query'] ) ) {
105 wp_parse_str( $parsed['query'], $this->query );
106 }
107
108 if ( isset( $_SERVER['HTTP_ACCEPT'] ) && $_SERVER['HTTP_ACCEPT'] ) {
109 $this->accept = $_SERVER['HTTP_ACCEPT'];
110 }
111
112 if ( 'POST' === $this->method ) {
113 if ( is_null( $post_body ) ) {
114 $this->post_body = file_get_contents( 'php://input' );
115
116 if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) && $_SERVER['HTTP_CONTENT_TYPE'] ) {
117 $this->content_type = $_SERVER['HTTP_CONTENT_TYPE'];
118 } elseif ( isset( $_SERVER['CONTENT_TYPE'] ) && $_SERVER['CONTENT_TYPE'] ) {
119 $this->content_type = $_SERVER['CONTENT_TYPE'];
120 } elseif ( '{' === $this->post_body[0] ) {
121 $this->content_type = 'application/json';
122 } else {
123 $this->content_type = 'application/x-www-form-urlencoded';
124 }
125
126 if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
127 $this->post_body = http_build_query( stripslashes_deep( $_POST ) );
128 $this->files = $_FILES;
129 $this->content_type = 'multipart/form-data';
130 }
131 } else {
132 $this->post_body = $post_body;
133 $this->content_type = '{' === isset( $this->post_body[0] ) && $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
134 }
135 } else {
136 $this->post_body = null;
137 $this->content_type = null;
138 }
139
140 $this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? $_SERVER['HTTPS'] : '--UNset--';
141 }
142
143 function initialize() {
144 $this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
145 }
146
147 function serve( $exit = true ) {
148 ini_set( 'display_errors', false );
149
150 $this->exit = (bool) $exit;
151
152 // This was causing problems with Jetpack, but is necessary for wpcom
153 // @see https://github.com/Automattic/jetpack/pull/2603
154 // @see r124548-wpcom
155 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
156 add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
157 }
158
159 add_filter( 'user_can_richedit', '__return_true' );
160
161 add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
162
163 $initialization = $this->initialize();
164 if ( 'OPTIONS' == $this->method ) {
165 /**
166 * Fires before the page output.
167 * Can be used to specify custom header options.
168 *
169 * @module json-api
170 *
171 * @since 3.1.0
172 */
173 do_action( 'wpcom_json_api_options' );
174 return $this->output( 200, '', 'text/plain' );
175 }
176
177 if ( is_wp_error( $initialization ) ) {
178 $this->output_error( $initialization );
179 return;
180 }
181
182 // Normalize path and extract API version
183 $this->path = untrailingslashit( $this->path );
184 preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
185 $this->path = substr( $this->path, strlen( $matches[0] ) );
186 $this->version = $matches[1];
187
188 $allowed_methods = array( 'GET', 'POST' );
189 $four_oh_five = false;
190
191 $is_help = preg_match( '#/help/?$#i', $this->path );
192 $matching_endpoints = array();
193
194 if ( $is_help ) {
195 $origin = get_http_origin();
196
197 if ( ! empty( $origin ) && 'GET' == $this->method ) {
198 header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
199 }
200
201 $this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
202 // Show help for all matching endpoints regardless of method
203 $methods = $allowed_methods;
204 $find_all_matching_endpoints = true;
205 // How deep to truncate each endpoint's path to see if it matches this help request
206 $depth = substr_count( $this->path, '/' ) + 1;
207 if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
208 $help_content_type = 'json';
209 } else {
210 $help_content_type = 'html';
211 }
212 } else {
213 if ( in_array( $this->method, $allowed_methods ) ) {
214 // Only serve requested method
215 $methods = array( $this->method );
216 $find_all_matching_endpoints = false;
217 } else {
218 // We don't allow this requested method - find matching endpoints and send 405
219 $methods = $allowed_methods;
220 $find_all_matching_endpoints = true;
221 $four_oh_five = true;
222 }
223 }
224
225 // Find which endpoint to serve
226 $found = false;
227 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
228 $endpoint_path_versions = unserialize( $endpoint_path_versions );
229 $endpoint_path = $endpoint_path_versions[0];
230 $endpoint_min_version = $endpoint_path_versions[1];
231 $endpoint_max_version = $endpoint_path_versions[2];
232
233 // Make sure max_version is not less than min_version
234 if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
235 $endpoint_max_version = $endpoint_min_version;
236 }
237
238 foreach ( $methods as $method ) {
239 if ( ! isset( $endpoints_by_method[ $method ] ) ) {
240 continue;
241 }
242
243 // Normalize
244 $endpoint_path = untrailingslashit( $endpoint_path );
245 if ( $is_help ) {
246 // Truncate path at help depth
247 $endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
248 }
249
250 // Generate regular expression from sprintf()
251 $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
252
253 if ( ! preg_match( "#^$endpoint_path_regex\$#", $this->path, $path_pieces ) ) {
254 // This endpoint does not match the requested path.
255 continue;
256 }
257
258 if ( version_compare( $this->version, $endpoint_min_version, '<' ) || version_compare( $this->version, $endpoint_max_version, '>' ) ) {
259 // This endpoint does not match the requested version.
260 continue;
261 }
262
263 $found = true;
264
265 if ( $find_all_matching_endpoints ) {
266 $matching_endpoints[] = array( $endpoints_by_method[ $method ], $path_pieces );
267 } else {
268 // The method parameters are now in $path_pieces
269 $endpoint = $endpoints_by_method[ $method ];
270 break 2;
271 }
272 }
273 }
274
275 if ( ! $found ) {
276 return $this->output( 404, '', 'text/plain' );
277 }
278
279 if ( $four_oh_five ) {
280 $allowed_methods = array();
281 foreach ( $matching_endpoints as $matching_endpoint ) {
282 $allowed_methods[] = $matching_endpoint[0]->method;
283 }
284
285 header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
286 return $this->output(
287 405,
288 array(
289 'error' => 'not_allowed',
290 'error_message' => 'Method not allowed',
291 )
292 );
293 }
294
295 if ( $is_help ) {
296 /**
297 * Fires before the API output.
298 *
299 * @since 1.9.0
300 *
301 * @param string help.
302 */
303 do_action( 'wpcom_json_api_output', 'help' );
304 $proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
305 if ( 'json' === $help_content_type ) {
306 $docs = array();
307 foreach ( $matching_endpoints as $matching_endpoint ) {
308 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
309 $docs[] = call_user_func( array( $matching_endpoint[0], 'generate_documentation' ) );
310 }
311 }
312 return $this->output( 200, $docs );
313 } else {
314 status_header( 200 );
315 foreach ( $matching_endpoints as $matching_endpoint ) {
316 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
317 call_user_func( array( $matching_endpoint[0], 'document' ) );
318 }
319 }
320 }
321 exit;
322 }
323
324 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
325 return $this->output( 404, '', 'text/plain' );
326 }
327
328 /** This action is documented in class.json-api.php */
329 do_action( 'wpcom_json_api_output', $endpoint->stat );
330
331 $response = $this->process_request( $endpoint, $path_pieces );
332
333 if ( ! $response && ! is_array( $response ) ) {
334 return $this->output( 500, '', 'text/plain' );
335 } elseif ( is_wp_error( $response ) ) {
336 return $this->output_error( $response );
337 }
338
339 $output_status_code = $this->output_status_code;
340 $this->set_output_status_code();
341
342 return $this->output( $output_status_code, $response, 'application/json', $this->extra_headers );
343 }
344
345 function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
346 $this->endpoint = $endpoint;
347 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
348 }
349
350 function output_early( $status_code, $response = null, $content_type = 'application/json' ) {
351 $exit = $this->exit;
352 $this->exit = false;
353 if ( is_wp_error( $response ) ) {
354 $this->output_error( $response );
355 } else {
356 $this->output( $status_code, $response, $content_type );
357 }
358 $this->exit = $exit;
359 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
360 $this->finish_request();
361 }
362 }
363
364 function set_output_status_code( $code = 200 ) {
365 $this->output_status_code = $code;
366 }
367
368 function output( $status_code, $response = null, $content_type = 'application/json', $extra = array() ) {
369 // In case output() was called before the callback returned
370 if ( $this->did_output ) {
371 if ( $this->exit ) {
372 exit;
373 }
374 return $content_type;
375 }
376 $this->did_output = true;
377
378 // 400s and 404s are allowed for all origins
379 if ( 404 == $status_code || 400 == $status_code ) {
380 header( 'Access-Control-Allow-Origin: *' );
381 }
382
383 if ( is_null( $response ) ) {
384 $response = new stdClass();
385 }
386
387 if ( 'text/plain' === $content_type ) {
388 status_header( (int) $status_code );
389 header( 'Content-Type: text/plain' );
390 foreach ( $extra as $key => $value ) {
391 header( "$key: $value" );
392 }
393 echo $response;
394 if ( $this->exit ) {
395 exit;
396 }
397
398 return $content_type;
399 }
400
401 $response = $this->filter_fields( $response );
402
403 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
404 $headers = array(
405 array(
406 'name' => 'Content-Type',
407 'value' => $content_type,
408 ),
409 );
410
411 foreach ( $extra as $key => $value ) {
412 $headers[] = array(
413 'name' => $key,
414 'value' => $value,
415 );
416 }
417
418 $response = array(
419 'code' => (int) $status_code,
420 'headers' => $headers,
421 'body' => $response,
422 );
423 $status_code = 200;
424 $content_type = 'application/json';
425 }
426
427 status_header( (int) $status_code );
428 header( "Content-Type: $content_type" );
429 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
430 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
431 } else {
432 $callback = false;
433 }
434
435 if ( $callback ) {
436 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
437 // and by prepending the JSONP response with a JS comment.
438 // [1] https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html
439 echo "/**/$callback(";
440
441 }
442 echo $this->json_encode( $response );
443 if ( $callback ) {
444 echo ');';
445 }
446
447 if ( $this->exit ) {
448 exit;
449 }
450
451 return $content_type;
452 }
453
454 public static function serializable_error( $error ) {
455
456 $status_code = $error->get_error_data();
457
458 if ( is_array( $status_code ) ) {
459 $status_code = $status_code['status_code'];
460 }
461
462 if ( ! $status_code ) {
463 $status_code = 400;
464 }
465 $response = array(
466 'error' => $error->get_error_code(),
467 'message' => $error->get_error_message(),
468 );
469
470 if ( $additional_data = $error->get_error_data( 'additional_data' ) ) {
471 $response['data'] = $additional_data;
472 }
473
474 return array(
475 'status_code' => $status_code,
476 'errors' => $response,
477 );
478 }
479
480 function output_error( $error ) {
481 $error_response = $this->serializable_error( $error );
482
483 return $this->output( $error_response['status_code'], $error_response['errors'] );
484 }
485
486 function filter_fields( $response ) {
487 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
488 return $response;
489 }
490
491 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
492
493 if ( is_object( $response ) ) {
494 $response = (array) $response;
495 }
496
497 $has_filtered = false;
498 if ( is_array( $response ) && empty( $response['ID'] ) ) {
499 $keys_to_filter = array(
500 'categories',
501 'comments',
502 'connections',
503 'domains',
504 'groups',
505 'likes',
506 'media',
507 'notes',
508 'posts',
509 'services',
510 'sites',
511 'suggestions',
512 'tags',
513 'themes',
514 'topics',
515 'users',
516 );
517
518 foreach ( $keys_to_filter as $key_to_filter ) {
519 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
520 continue;
521 }
522
523 foreach ( $response[ $key_to_filter ] as $key => $values ) {
524 if ( is_object( $values ) ) {
525 if ( is_object( $response[ $key_to_filter ] ) ) {
526 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
527 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
528 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
529 }
530 } elseif ( is_array( $values ) ) {
531 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
532 }
533 }
534
535 $has_filtered = true;
536 }
537 }
538
539 if ( ! $has_filtered ) {
540 if ( is_object( $response ) ) {
541 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
542 } elseif ( is_array( $response ) ) {
543 $response = array_intersect_key( $response, array_flip( $fields ) );
544 }
545 }
546
547 return $response;
548 }
549
550 function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
551 if ( $original_scheme ) {
552 return $url;
553 }
554
555 return preg_replace( '#^https:#', 'http:', $url );
556 }
557
558 function comment_edit_pre( $comment_content ) {
559 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
560 }
561
562 function json_encode( $data ) {
563 return wp_json_encode( $data );
564 }
565
566 function ends_with( $haystack, $needle ) {
567 return $needle === substr( $haystack, -strlen( $needle ) );
568 }
569
570 // Returns the site's blog_id in the WP.com ecosystem
571 function get_blog_id_for_output() {
572 return $this->token_details['blog_id'];
573 }
574
575 // Returns the site's local blog_id
576 function get_blog_id( $blog_id ) {
577 return $GLOBALS['blog_id'];
578 }
579
580 function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
581 if ( $this->is_restricted_blog( $blog_id ) ) {
582 return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
583 }
584
585 if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
586 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
587 }
588
589 return $blog_id;
590 }
591
592 // Returns true if the specified blog ID is a restricted blog
593 function is_restricted_blog( $blog_id ) {
594 /**
595 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
596 *
597 * @module json-api
598 *
599 * @since 3.4.0
600 *
601 * @param array $array Array of Blog IDs.
602 */
603 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
604 return true === in_array( $blog_id, $restricted_blog_ids );
605 }
606
607 function post_like_count( $blog_id, $post_id ) {
608 return 0;
609 }
610
611 function is_liked( $blog_id, $post_id ) {
612 return false;
613 }
614
615 function is_reblogged( $blog_id, $post_id ) {
616 return false;
617 }
618
619 function is_following( $blog_id ) {
620 return false;
621 }
622
623 function add_global_ID( $blog_id, $post_id ) {
624 return '';
625 }
626
627 function get_avatar_url( $email, $avatar_size = null ) {
628 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
629 return null === $avatar_size
630 ? wpcom_get_avatar_url( $email )
631 : wpcom_get_avatar_url( $email, $avatar_size );
632 } else {
633 return null === $avatar_size
634 ? get_avatar_url( $email )
635 : get_avatar_url( $email, $avatar_size );
636 }
637 }
638
639 /**
640 * Counts the number of comments on a site, excluding certain comment types.
641 *
642 * @param $post_id int Post ID.
643 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
644 */
645 public function wp_count_comments( $post_id ) {
646 global $wpdb;
647 if ( 0 !== $post_id ) {
648 return wp_count_comments( $post_id );
649 }
650
651 $counts = array(
652 'total_comments' => 0,
653 'all' => 0,
654 );
655
656 /**
657 * Exclude certain comment types from comment counts in the REST API.
658 *
659 * @since 6.9.0
660 * @module json-api
661 *
662 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
663 */
664 $exclude = apply_filters(
665 'jetpack_api_exclude_comment_types_count',
666 array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
667 );
668
669 if ( empty( $exclude ) ) {
670 return wp_count_comments( $post_id );
671 }
672
673 array_walk( $exclude, 'esc_sql' );
674 $where = sprintf(
675 "WHERE comment_type NOT IN ( '%s' )",
676 implode( "','", $exclude )
677 );
678
679 $count = $wpdb->get_results(
680 "SELECT comment_approved, COUNT(*) AS num_comments
681 FROM $wpdb->comments
682 {$where}
683 GROUP BY comment_approved
684 "
685 );
686
687 $approved = array(
688 '0' => 'moderated',
689 '1' => 'approved',
690 'spam' => 'spam',
691 'trash' => 'trash',
692 'post-trashed' => 'post-trashed',
693 );
694
695 // https://developer.wordpress.org/reference/functions/get_comment_count/#source
696 foreach ( $count as $row ) {
697 if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
698 $counts['all'] += $row->num_comments;
699 $counts['total_comments'] += $row->num_comments;
700 } elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
701 $counts['total_comments'] += $row->num_comments;
702 }
703 if ( isset( $approved[ $row->comment_approved ] ) ) {
704 $counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
705 }
706 }
707
708 foreach ( $approved as $key ) {
709 if ( empty( $counts[ $key ] ) ) {
710 $counts[ $key ] = 0;
711 }
712 }
713
714 $counts = (object) $counts;
715
716 return $counts;
717 }
718
719 /**
720 * traps `wp_die()` calls and outputs a JSON response instead.
721 * The result is always output, never returned.
722 *
723 * @param string|null $error_code Call with string to start the trapping. Call with null to stop.
724 * @param int $http_status HTTP status code, 400 by default.
725 */
726 function trap_wp_die( $error_code = null, $http_status = 400 ) {
727 if ( is_null( $error_code ) ) {
728 $this->trapped_error = null;
729 // Stop trapping
730 remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
731 return;
732 }
733
734 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
735 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
736 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
737 return;
738 }
739 } else {
740 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
741 return;
742 }
743 }
744
745 $this->trapped_error = array(
746 'status' => $http_status,
747 'code' => $error_code,
748 'message' => '',
749 );
750 // Start trapping
751 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
752 }
753
754 function wp_die_handler_callback() {
755 return array( $this, 'wp_die_handler' );
756 }
757
758 function wp_die_handler( $message, $title = '', $args = array() ) {
759 // Allow wp_die calls to override HTTP status code...
760 $args = wp_parse_args(
761 $args,
762 array(
763 'response' => $this->trapped_error['status'],
764 )
765 );
766
767 // ... unless it's 500
768 if ( (int) $args['response'] !== 500 ) {
769 $this->trapped_error['status'] = $args['response'];
770 }
771
772 if ( $title ) {
773 $message = "$title: $message";
774 }
775
776 $this->trapped_error['message'] = wp_kses( $message, array() );
777
778 switch ( $this->trapped_error['code'] ) {
779 case 'comment_failure':
780 if ( did_action( 'comment_duplicate_trigger' ) ) {
781 $this->trapped_error['code'] = 'comment_duplicate';
782 } elseif ( did_action( 'comment_flood_trigger' ) ) {
783 $this->trapped_error['code'] = 'comment_flood';
784 }
785 break;
786 }
787
788 // We still want to exit so that code execution stops where it should.
789 // Attach the JSON output to the WordPress shutdown handler
790 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
791 exit;
792 }
793
794 function output_trapped_error() {
795 $this->exit = false; // We're already exiting once. Don't do it twice.
796 $this->output(
797 $this->trapped_error['status'],
798 (object) array(
799 'error' => $this->trapped_error['code'],
800 'message' => $this->trapped_error['message'],
801 )
802 );
803 }
804
805 function finish_request() {
806 if ( function_exists( 'fastcgi_finish_request' ) ) {
807 return fastcgi_finish_request();
808 }
809 }
810 }
811