PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 8.5
Jetpack – WP Security, Backup, Speed, & Growth v8.5
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 in Jetpack – WP Security, Backup, Speed, & Growth 8.5, at class.json-api.php

819 lines 22.6 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 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 status_header( (int) $status_code );
397 header( 'Content-Type: text/plain' );
398 foreach ( $extra as $key => $value ) {
399 header( "$key: $value" );
400 }
401 echo $response;
402 if ( $this->exit ) {
403 exit;
404 }
405
406 return $content_type;
407 }
408
409 $response = $this->filter_fields( $response );
410
411 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
412 $headers = array(
413 array(
414 'name' => 'Content-Type',
415 'value' => $content_type,
416 ),
417 );
418
419 foreach ( $extra as $key => $value ) {
420 $headers[] = array(
421 'name' => $key,
422 'value' => $value,
423 );
424 }
425
426 $response = array(
427 'code' => (int) $status_code,
428 'headers' => $headers,
429 'body' => $response,
430 );
431 $status_code = 200;
432 $content_type = 'application/json';
433 }
434
435 status_header( (int) $status_code );
436 header( "Content-Type: $content_type" );
437 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
438 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
439 } else {
440 $callback = false;
441 }
442
443 if ( $callback ) {
444 // Mitigate Rosetta Flash [1] by setting the Content-Type-Options: nosniff header
445 // and by prepending the JSONP response with a JS comment.
446 // [1] https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html
447 echo "/**/$callback(";
448
449 }
450 echo $this->json_encode( $response );
451 if ( $callback ) {
452 echo ');';
453 }
454
455 if ( $this->exit ) {
456 exit;
457 }
458
459 return $content_type;
460 }
461
462 public static function serializable_error( $error ) {
463
464 $status_code = $error->get_error_data();
465
466 if ( is_array( $status_code ) ) {
467 $status_code = $status_code['status_code'];
468 }
469
470 if ( ! $status_code ) {
471 $status_code = 400;
472 }
473 $response = array(
474 'error' => $error->get_error_code(),
475 'message' => $error->get_error_message(),
476 );
477
478 if ( $additional_data = $error->get_error_data( 'additional_data' ) ) {
479 $response['data'] = $additional_data;
480 }
481
482 return array(
483 'status_code' => $status_code,
484 'errors' => $response,
485 );
486 }
487
488 function output_error( $error ) {
489 $error_response = $this->serializable_error( $error );
490
491 return $this->output( $error_response['status_code'], $error_response['errors'] );
492 }
493
494 function filter_fields( $response ) {
495 if ( empty( $this->query['fields'] ) || ( is_array( $response ) && ! empty( $response['error'] ) ) || ! empty( $this->endpoint->custom_fields_filtering ) ) {
496 return $response;
497 }
498
499 $fields = array_map( 'trim', explode( ',', $this->query['fields'] ) );
500
501 if ( is_object( $response ) ) {
502 $response = (array) $response;
503 }
504
505 $has_filtered = false;
506 if ( is_array( $response ) && empty( $response['ID'] ) ) {
507 $keys_to_filter = array(
508 'categories',
509 'comments',
510 'connections',
511 'domains',
512 'groups',
513 'likes',
514 'media',
515 'notes',
516 'posts',
517 'services',
518 'sites',
519 'suggestions',
520 'tags',
521 'themes',
522 'topics',
523 'users',
524 );
525
526 foreach ( $keys_to_filter as $key_to_filter ) {
527 if ( ! isset( $response[ $key_to_filter ] ) || $has_filtered ) {
528 continue;
529 }
530
531 foreach ( $response[ $key_to_filter ] as $key => $values ) {
532 if ( is_object( $values ) ) {
533 if ( is_object( $response[ $key_to_filter ] ) ) {
534 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
535 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
536 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
537 }
538 } elseif ( is_array( $values ) ) {
539 $response[ $key_to_filter ][ $key ] = array_intersect_key( $values, array_flip( $fields ) );
540 }
541 }
542
543 $has_filtered = true;
544 }
545 }
546
547 if ( ! $has_filtered ) {
548 if ( is_object( $response ) ) {
549 $response = (object) array_intersect_key( (array) $response, array_flip( $fields ) );
550 } elseif ( is_array( $response ) ) {
551 $response = array_intersect_key( $response, array_flip( $fields ) );
552 }
553 }
554
555 return $response;
556 }
557
558 function ensure_http_scheme_of_home_url( $url, $path, $original_scheme ) {
559 if ( $original_scheme ) {
560 return $url;
561 }
562
563 return preg_replace( '#^https:#', 'http:', $url );
564 }
565
566 function comment_edit_pre( $comment_content ) {
567 return htmlspecialchars_decode( $comment_content, ENT_QUOTES );
568 }
569
570 function json_encode( $data ) {
571 return wp_json_encode( $data );
572 }
573
574 function ends_with( $haystack, $needle ) {
575 return $needle === substr( $haystack, -strlen( $needle ) );
576 }
577
578 // Returns the site's blog_id in the WP.com ecosystem
579 function get_blog_id_for_output() {
580 return $this->token_details['blog_id'];
581 }
582
583 // Returns the site's local blog_id
584 function get_blog_id( $blog_id ) {
585 return $GLOBALS['blog_id'];
586 }
587
588 function switch_to_blog_and_validate_user( $blog_id = 0, $verify_token_for_blog = true ) {
589 if ( $this->is_restricted_blog( $blog_id ) ) {
590 return new WP_Error( 'unauthorized', 'User cannot access this restricted blog', 403 );
591 }
592
593 if ( -1 == get_option( 'blog_public' ) && ! current_user_can( 'read' ) ) {
594 return new WP_Error( 'unauthorized', 'User cannot access this private blog.', 403 );
595 }
596
597 return $blog_id;
598 }
599
600 // Returns true if the specified blog ID is a restricted blog
601 function is_restricted_blog( $blog_id ) {
602 /**
603 * Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
604 *
605 * @module json-api
606 *
607 * @since 3.4.0
608 *
609 * @param array $array Array of Blog IDs.
610 */
611 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
612 return true === in_array( $blog_id, $restricted_blog_ids );
613 }
614
615 function post_like_count( $blog_id, $post_id ) {
616 return 0;
617 }
618
619 function is_liked( $blog_id, $post_id ) {
620 return false;
621 }
622
623 function is_reblogged( $blog_id, $post_id ) {
624 return false;
625 }
626
627 function is_following( $blog_id ) {
628 return false;
629 }
630
631 function add_global_ID( $blog_id, $post_id ) {
632 return '';
633 }
634
635 function get_avatar_url( $email, $avatar_size = null ) {
636 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
637 return null === $avatar_size
638 ? wpcom_get_avatar_url( $email )
639 : wpcom_get_avatar_url( $email, $avatar_size );
640 } else {
641 return null === $avatar_size
642 ? get_avatar_url( $email )
643 : get_avatar_url( $email, $avatar_size );
644 }
645 }
646
647 /**
648 * Counts the number of comments on a site, excluding certain comment types.
649 *
650 * @param $post_id int Post ID.
651 * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
652 */
653 public function wp_count_comments( $post_id ) {
654 global $wpdb;
655 if ( 0 !== $post_id ) {
656 return wp_count_comments( $post_id );
657 }
658
659 $counts = array(
660 'total_comments' => 0,
661 'all' => 0,
662 );
663
664 /**
665 * Exclude certain comment types from comment counts in the REST API.
666 *
667 * @since 6.9.0
668 * @module json-api
669 *
670 * @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
671 */
672 $exclude = apply_filters(
673 'jetpack_api_exclude_comment_types_count',
674 array( 'order_note', 'webhook_delivery', 'review', 'action_log' )
675 );
676
677 if ( empty( $exclude ) ) {
678 return wp_count_comments( $post_id );
679 }
680
681 array_walk( $exclude, 'esc_sql' );
682 $where = sprintf(
683 "WHERE comment_type NOT IN ( '%s' )",
684 implode( "','", $exclude )
685 );
686
687 $count = $wpdb->get_results(
688 "SELECT comment_approved, COUNT(*) AS num_comments
689 FROM $wpdb->comments
690 {$where}
691 GROUP BY comment_approved
692 "
693 );
694
695 $approved = array(
696 '0' => 'moderated',
697 '1' => 'approved',
698 'spam' => 'spam',
699 'trash' => 'trash',
700 'post-trashed' => 'post-trashed',
701 );
702
703 // https://developer.wordpress.org/reference/functions/get_comment_count/#source
704 foreach ( $count as $row ) {
705 if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
706 $counts['all'] += $row->num_comments;
707 $counts['total_comments'] += $row->num_comments;
708 } elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
709 $counts['total_comments'] += $row->num_comments;
710 }
711 if ( isset( $approved[ $row->comment_approved ] ) ) {
712 $counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
713 }
714 }
715
716 foreach ( $approved as $key ) {
717 if ( empty( $counts[ $key ] ) ) {
718 $counts[ $key ] = 0;
719 }
720 }
721
722 $counts = (object) $counts;
723
724 return $counts;
725 }
726
727 /**
728 * traps `wp_die()` calls and outputs a JSON response instead.
729 * The result is always output, never returned.
730 *
731 * @param string|null $error_code Call with string to start the trapping. Call with null to stop.
732 * @param int $http_status HTTP status code, 400 by default.
733 */
734 function trap_wp_die( $error_code = null, $http_status = 400 ) {
735 if ( is_null( $error_code ) ) {
736 $this->trapped_error = null;
737 // Stop trapping
738 remove_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
739 return;
740 }
741
742 // If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
743 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
744 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
745 return;
746 }
747 } else {
748 if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
749 return;
750 }
751 }
752
753 $this->trapped_error = array(
754 'status' => $http_status,
755 'code' => $error_code,
756 'message' => '',
757 );
758 // Start trapping
759 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler_callback' ) );
760 }
761
762 function wp_die_handler_callback() {
763 return array( $this, 'wp_die_handler' );
764 }
765
766 function wp_die_handler( $message, $title = '', $args = array() ) {
767 // Allow wp_die calls to override HTTP status code...
768 $args = wp_parse_args(
769 $args,
770 array(
771 'response' => $this->trapped_error['status'],
772 )
773 );
774
775 // ... unless it's 500
776 if ( (int) $args['response'] !== 500 ) {
777 $this->trapped_error['status'] = $args['response'];
778 }
779
780 if ( $title ) {
781 $message = "$title: $message";
782 }
783
784 $this->trapped_error['message'] = wp_kses( $message, array() );
785
786 switch ( $this->trapped_error['code'] ) {
787 case 'comment_failure':
788 if ( did_action( 'comment_duplicate_trigger' ) ) {
789 $this->trapped_error['code'] = 'comment_duplicate';
790 } elseif ( did_action( 'comment_flood_trigger' ) ) {
791 $this->trapped_error['code'] = 'comment_flood';
792 }
793 break;
794 }
795
796 // We still want to exit so that code execution stops where it should.
797 // Attach the JSON output to the WordPress shutdown handler
798 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
799 exit;
800 }
801
802 function output_trapped_error() {
803 $this->exit = false; // We're already exiting once. Don't do it twice.
804 $this->output(
805 $this->trapped_error['status'],
806 (object) array(
807 'error' => $this->trapped_error['code'],
808 'message' => $this->trapped_error['message'],
809 )
810 );
811 }
812
813 function finish_request() {
814 if ( function_exists( 'fastcgi_finish_request' ) ) {
815 return fastcgi_finish_request();
816 }
817 }
818 }
819