/class.json-api.php

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