PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.2 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 All 502 releases
← All changes | class.json-api.php +149 -55 13.9.216.2 View file →
@@ -220,9 +220,9 @@
220 220 if ( ! is_string( $value ) ) {
221 221 return false;
222 222 }
223 223
224 - switch ( strtolower( (string) $value ) ) {
224 + switch ( strtolower( $value ) ) {
225 225 case '1':
226 226 case 't':
227 227 case 'true':
228 228 return true;
@@ -249,9 +249,9 @@
249 249 if ( ! is_string( $value ) ) {
250 250 return false;
251 251 }
252 252
253 - switch ( strtolower( (string) $value ) ) {
253 + switch ( strtolower( $value ) ) {
254 254 case '0':
255 255 case 'f':
256 256 case 'false':
257 257 return true;
@@ -398,9 +398,9 @@
398 398 * @param bool $exit Whether to exit.
399 399 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
400 400 */
401 401 public function serve( $exit = true ) {
402 - ini_set( 'display_errors', false ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Blacklisted
402 + ini_set( 'display_errors', false ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed
403 403
404 404 $this->exit = (bool) $exit;
405 405
406 406 // This was causing problems with Jetpack, but is necessary for wpcom
@@ -475,9 +475,10 @@
475 475 $four_oh_five = true;
476 476 }
477 477
478 478 // Find which endpoint to serve.
479 - $found = false;
479 + $found = false;
480 + $path_pieces = array();
480 481 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
481 482 // @todo Determine if anything depends on this being serialized rather than e.g. JSON.
482 483 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, possibly depended on elsewhere.
483 484 $endpoint_path_versions = unserialize( $endpoint_path_versions );
@@ -498,8 +499,9 @@
498 499 // Normalize.
499 500 $endpoint_path = untrailingslashit( $endpoint_path );
500 501 if ( $is_help ) {
501 502 // Truncate path at help depth.
503 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $depth is set when $is_help is true.
502 504 $endpoint_path = implode( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
503 505 }
504 506
505 507 // Generate regular expression from sprintf().
@@ -556,8 +558,9 @@
556 558 * @param string help.
557 559 */
558 560 do_action( 'wpcom_json_api_output', 'help' );
559 561 $proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
562 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $help_content_type is set when $is_help is true.
560 563 if ( 'json' === $help_content_type ) {
561 564 $docs = array();
562 565 foreach ( $matching_endpoints as $matching_endpoint ) {
563 566 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
@@ -572,18 +575,21 @@
572 575 call_user_func( array( $matching_endpoint[0], 'document' ) );
573 576 }
574 577 }
575 578 }
576 - exit;
579 + exit( 0 );
577 580 }
578 581
582 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $endpoint is set when $find_all_matching_endpoints is false and $found is true, which is guaranteed here.
579 583 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
580 584 return $this->output( 404, '', 'text/plain' );
581 585 }
582 586
583 587 /** This action is documented in class.json-api.php */
588 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $endpoint is set when $find_all_matching_endpoints is false and $found is true, which is guaranteed here.
584 589 do_action( 'wpcom_json_api_output', $endpoint->stat );
585 590
591 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $endpoint is set when $find_all_matching_endpoints is false and $found is true, which is guaranteed here.
586 592 $response = $this->process_request( $endpoint, $path_pieces );
587 593
588 594 if ( ! $response && ! is_array( $response ) ) {
589 595 return $this->output( 500, '', 'text/plain' );
@@ -605,8 +611,9 @@
605 611 * @return array|WP_Error Return value from the endpoint's callback.
606 612 */
607 613 public function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
608 614 $this->endpoint = $endpoint;
615 + $this->maybe_switch_to_token_user_and_site();
609 616 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
610 617 }
611 618
612 619 /**
@@ -653,9 +660,9 @@
653 660
654 661 // In case output() was called before the callback returned.
655 662 if ( $this->did_output ) {
656 663 if ( $this->exit ) {
657 - exit;
664 + exit( 0 );
658 665 }
659 666 return $content_type;
660 667 }
661 668 $this->did_output = true;
@@ -676,9 +683,9 @@
676 683 }
677 684
678 685 if ( 'text/plain' === $content_type ||
679 686 'text/html' === $content_type ) {
680 - status_header( (int) $status_code );
687 + status_header( $status_code );
681 688 header( 'Content-Type: ' . $content_type );
682 689 foreach ( $extra as $key => $value ) {
683 690 header( "$key: $value" );
684 691 }
@@ -683,9 +690,9 @@
683 690 header( "$key: $value" );
684 691 }
685 692 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
686 693 if ( $this->exit ) {
687 - exit;
694 + exit( 0 );
688 695 }
689 696
690 697 return $content_type;
691 698 }
@@ -692,32 +699,15 @@
692 699
693 700 $response = $this->filter_fields( $response );
694 701
695 702 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
696 - $headers = array(
697 - array(
698 - 'name' => 'Content-Type',
699 - 'value' => $content_type,
700 - ),
701 - );
703 + $response = static::wrap_http_envelope( $status_code, $response, $content_type, $extra );
702 704
703 - foreach ( $extra as $key => $value ) {
704 - $headers[] = array(
705 - 'name' => $key,
706 - 'value' => $value,
707 - );
708 - }
709 -
710 - $response = array(
711 - 'code' => (int) $status_code,
712 - 'headers' => $headers,
713 - 'body' => $response,
714 - );
715 705 $status_code = 200;
716 706 $content_type = 'application/json';
717 707 }
718 708
719 - status_header( (int) $status_code );
709 + status_header( $status_code );
720 710 header( "Content-Type: $content_type" );
721 711 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
722 712 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
723 713 } else {
@@ -730,15 +720,15 @@
730 720 // [1] <https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html>.
731 721 echo "/**/$callback("; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSONP output, not HTML.
732 722
733 723 }
734 - echo $this->json_encode( $response ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSON or JSONP output, not HTML.
724 + echo $this->json_encode( $response, JSON_UNESCAPED_SLASHES ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSON or JSONP output, not HTML.
735 725 if ( $callback ) {
736 726 echo ');';
737 727 }
738 728
739 729 if ( $this->exit ) {
740 - exit;
730 + exit( 0 );
741 731 }
742 732
743 733 return $content_type;
744 734 }
@@ -743,8 +733,42 @@
743 733 return $content_type;
744 734 }
745 735
746 736 /**
737 + * Wrap JSON API response into an HTTP 200 one.
738 + *
739 + * @param int $status_code HTTP status code.
740 + * @param mixed $response Response body.
741 + * @param string $content_type Content type.
742 + * @param array|null $extra Extra data.
743 + *
744 + * @return array
745 + */
746 + public static function wrap_http_envelope( $status_code, $response, $content_type, $extra = null ) {
747 + $headers = array(
748 + array(
749 + 'name' => 'Content-Type',
750 + 'value' => $content_type,
751 + ),
752 + );
753 +
754 + if ( is_array( $extra ) ) {
755 + foreach ( $extra as $key => $value ) {
756 + $headers[] = array(
757 + 'name' => $key,
758 + 'value' => $value,
759 + );
760 + }
761 + }
762 +
763 + return array(
764 + 'code' => (int) $status_code,
765 + 'headers' => $headers,
766 + 'body' => $response,
767 + );
768 + }
769 +
770 + /**
747 771 * Serialize an error.
748 772 *
749 773 * @param WP_Error $error Error.
750 774 * @return array with 'status_code' and 'errors' data.
@@ -750,14 +774,13 @@
750 774 * @return array with 'status_code' and 'errors' data.
751 775 */
752 776 public static function serializable_error( $error ) {
753 777
754 - $status_code = $error->get_error_data();
778 + // A missing or non-numeric status resolves to 0 and defaults to 400. Valid HTTP codes, including sub-400 ones, are preserved.
779 + $data = $error->get_error_data();
780 + $status_code = ( is_array( $data ) && isset( $data['status_code'] ) ) ? $data['status_code'] : $data;
781 + $status_code = is_numeric( $status_code ) ? (int) $status_code : 0;
755 782
756 - if ( is_array( $status_code ) && isset( $status_code['status_code'] ) ) {
757 - $status_code = $status_code['status_code'];
758 - }
759 -
760 783 if ( ! $status_code ) {
761 784 $status_code = 400;
762 785 }
763 786 $response = array(
@@ -833,9 +856,8 @@
833 856
834 857 foreach ( $response[ $key_to_filter ] as $key => $values ) {
835 858 if ( is_object( $values ) ) {
836 859 if ( is_object( $response[ $key_to_filter ] ) ) {
837 - // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found -- False positive.
838 860 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
839 861 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
840 862 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
841 863 }
@@ -889,13 +911,16 @@
889 911
890 912 /**
891 913 * JSON encode.
892 914 *
893 - * @param mixed $data Data.
915 + * @param mixed $value The value to encode.
916 + * @param int $flags Options to be passed to json_encode(). Default 0.
917 + * @param int $depth Maximum depth to walk through $value. Must be greater than 0.
918 + *
894 919 * @return string|false
895 920 */
896 - public function json_encode( $data ) {
897 - return wp_json_encode( $data );
921 + public function json_encode( $value, $flags = 0, $depth = 512 ) {
922 + return wp_json_encode( $value, $flags, $depth );
898 923 }
899 924
900 925 /**
901 926 * Test if a string ends with a string.
@@ -953,8 +978,37 @@
953 978 return $blog_id;
954 979 }
955 980
956 981 /**
982 + * Switch to a user and blog based on the current request's Jetpack token when the endpoint accepts this feature.
983 + *
984 + * @return void
985 + */
986 + protected function maybe_switch_to_token_user_and_site() {
987 + if ( ! $this->endpoint->allow_jetpack_token_auth ) {
988 + return;
989 + }
990 +
991 + if ( ! class_exists( 'Jetpack_Server_Version' ) ) {
992 + return;
993 + }
994 +
995 + $token = Jetpack_Server_Version::get_token_from_authorization_header();
996 +
997 + if ( ! $token || is_wp_error( $token ) ) {
998 + return;
999 + }
1000 +
1001 + if ( get_current_user_id() !== $token->user_id ) {
1002 + wp_set_current_user( $token->user_id );
1003 + }
1004 +
1005 + if ( get_current_blog_id() !== $token->blog_id ) {
1006 + switch_to_blog( $token->blog_id );
1007 + }
1008 + }
1009 +
1010 + /**
957 1011 * Returns true if the specified blog ID is a restricted blog
958 1012 *
959 1013 * @param int $blog_id Blog ID.
960 1014 * @return bool
@@ -969,9 +1023,9 @@
969 1023 *
970 1024 * @param array $array Array of Blog IDs.
971 1025 */
972 1026 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
973 - return true === in_array( $blog_id, $restricted_blog_ids ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- I don't trust filters to return the right types.
1027 + return in_array( $blog_id, $restricted_blog_ids ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- I don't trust filters to return the right types.
974 1028 }
975 1029
976 1030 /**
977 1031 * Post like count.
@@ -1060,9 +1114,9 @@
1060 1114 /**
1061 1115 * Counts the number of comments on a site, including certain comment types.
1062 1116 *
1063 1117 * @param int $post_id Post ID.
1064 - * @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
1118 + * @return object The number of counts keyed by status, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
1065 1119 */
1066 1120 public function wp_count_comments( $post_id ) {
1067 1121 global $wpdb;
1068 1122 if ( 0 !== $post_id ) {
@@ -1103,24 +1157,36 @@
1103 1157 if ( empty( $include ) ) {
1104 1158 return wp_count_comments( $post_id );
1105 1159 }
1106 1160
1107 - array_walk( $include, 'esc_sql' );
1108 - $where = sprintf(
1109 - "WHERE comment_type IN ( '%s' )",
1110 - implode( "','", $include )
1111 - );
1161 + // The following caching mechanism is based on what the get_comments() function uses.
1112 1162
1113 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1114 - $count = $wpdb->get_results(
1115 - "SELECT comment_approved, COUNT(*) AS num_comments
1116 - FROM $wpdb->comments
1117 - {$where}
1118 - GROUP BY comment_approved
1119 - "
1120 - );
1121 - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1163 + $key = md5( serialize( $include ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1164 + $last_changed = wp_cache_get_last_changed( 'comment' );
1122 1165
1166 + $cache_key = "wp_count_comments:$key:$last_changed";
1167 + $count = wp_cache_get( $cache_key, 'jetpack-json-api' );
1168 +
1169 + if ( false === $count ) {
1170 + array_walk( $include, 'esc_sql' );
1171 + $where = sprintf(
1172 + "WHERE comment_type IN ( '%s' )",
1173 + implode( "','", $include )
1174 + );
1175 +
1176 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1177 + $count = $wpdb->get_results(
1178 + "SELECT comment_approved, COUNT(*) AS num_comments
1179 + FROM $wpdb->comments
1180 + {$where}
1181 + GROUP BY comment_approved
1182 + "
1183 + );
1184 + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1185 +
1186 + wp_cache_add( $cache_key, $count, 'jetpack-json-api' );
1187 + }
1188 +
1123 1189 $approved = array(
1124 1190 '0' => 'moderated',
1125 1191 '1' => 'approved',
1126 1192 'spam' => 'spam',
@@ -1245,9 +1311,9 @@
1245 1311
1246 1312 // We still want to exit so that code execution stops where it should.
1247 1313 // Attach the JSON output to the WordPress shutdown handler.
1248 1314 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1249 - exit;
1315 + exit( 0 );
1250 1316 }
1251 1317
1252 1318 /**
1253 1319 * Output the trapped error.
@@ -1268,7 +1334,35 @@
1268 1334 */
1269 1335 public function finish_request() {
1270 1336 if ( function_exists( 'fastcgi_finish_request' ) ) {
1271 1337 return fastcgi_finish_request();
1338 + }
1339 + }
1340 +
1341 + /**
1342 + * Initialize the locale if different from 'en'.
1343 + *
1344 + * @param string $locale The locale to initialize.
1345 + */
1346 + public function init_locale( $locale ) {
1347 + if ( 'en' !== $locale ) {
1348 + // .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
1349 + $new_locale = $locale;
1350 + if ( str_contains( $locale, '-' ) ) {
1351 + $locale_pieces = explode( '-', $locale );
1352 + $new_locale = $locale_pieces[0];
1353 + $new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
1354 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
1355 + // .com might pass 'fr' because thats what our language files are named as, where core seems
1356 + // to do fr_FR - so try that if we don't think we can load the file.
1357 + if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
1358 + $new_locale = $locale . '_' . strtoupper( $locale );
1359 + }
1360 + }
1361 +
1362 + if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
1363 + unload_textdomain( 'default' );
1364 + load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
1365 + }
1272 1366 }
1273 1367 }
1274 1368 }