Brak opisu

class.json-api.php 24KB

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