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 +178 -62 13.3.316.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;
@@ -381,8 +381,19 @@
381 381 return true;
382 382 }
383 383
384 384 /**
385 + * Checks if the current request is authorized with an upload token.
386 + * This method is overridden by a child class in WPCOM.
387 + *
388 + * @since 13.5
389 + * @return boolean
390 + */
391 + public function is_authorized_with_upload_token() {
392 + return false;
393 + }
394 +
395 + /**
385 396 * Serve.
386 397 *
387 398 * @param bool $exit Whether to exit.
388 399 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
@@ -387,9 +398,9 @@
387 398 * @param bool $exit Whether to exit.
388 399 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
389 400 */
390 401 public function serve( $exit = true ) {
391 - 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
392 403
393 404 $this->exit = (bool) $exit;
394 405
395 406 // This was causing problems with Jetpack, but is necessary for wpcom
@@ -464,9 +475,10 @@
464 475 $four_oh_five = true;
465 476 }
466 477
467 478 // Find which endpoint to serve.
468 - $found = false;
479 + $found = false;
480 + $path_pieces = array();
469 481 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
470 482 // @todo Determine if anything depends on this being serialized rather than e.g. JSON.
471 483 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, possibly depended on elsewhere.
472 484 $endpoint_path_versions = unserialize( $endpoint_path_versions );
@@ -487,8 +499,9 @@
487 499 // Normalize.
488 500 $endpoint_path = untrailingslashit( $endpoint_path );
489 501 if ( $is_help ) {
490 502 // Truncate path at help depth.
503 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $depth is set when $is_help is true.
491 504 $endpoint_path = implode( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
492 505 }
493 506
494 507 // Generate regular expression from sprintf().
@@ -545,8 +558,9 @@
545 558 * @param string help.
546 559 */
547 560 do_action( 'wpcom_json_api_output', 'help' );
548 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.
549 563 if ( 'json' === $help_content_type ) {
550 564 $docs = array();
551 565 foreach ( $matching_endpoints as $matching_endpoint ) {
552 566 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
@@ -561,18 +575,21 @@
561 575 call_user_func( array( $matching_endpoint[0], 'document' ) );
562 576 }
563 577 }
564 578 }
565 - exit;
579 + exit( 0 );
566 580 }
567 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.
568 583 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
569 584 return $this->output( 404, '', 'text/plain' );
570 585 }
571 586
572 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.
573 589 do_action( 'wpcom_json_api_output', $endpoint->stat );
574 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.
575 592 $response = $this->process_request( $endpoint, $path_pieces );
576 593
577 594 if ( ! $response && ! is_array( $response ) ) {
578 595 return $this->output( 500, '', 'text/plain' );
@@ -594,8 +611,9 @@
594 611 * @return array|WP_Error Return value from the endpoint's callback.
595 612 */
596 613 public function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
597 614 $this->endpoint = $endpoint;
615 + $this->maybe_switch_to_token_user_and_site();
598 616 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
599 617 }
600 618
601 619 /**
@@ -642,9 +660,9 @@
642 660
643 661 // In case output() was called before the callback returned.
644 662 if ( $this->did_output ) {
645 663 if ( $this->exit ) {
646 - exit;
664 + exit( 0 );
647 665 }
648 666 return $content_type;
649 667 }
650 668 $this->did_output = true;
@@ -665,9 +683,9 @@
665 683 }
666 684
667 685 if ( 'text/plain' === $content_type ||
668 686 'text/html' === $content_type ) {
669 - status_header( (int) $status_code );
687 + status_header( $status_code );
670 688 header( 'Content-Type: ' . $content_type );
671 689 foreach ( $extra as $key => $value ) {
672 690 header( "$key: $value" );
673 691 }
@@ -672,9 +690,9 @@
672 690 header( "$key: $value" );
673 691 }
674 692 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
675 693 if ( $this->exit ) {
676 - exit;
694 + exit( 0 );
677 695 }
678 696
679 697 return $content_type;
680 698 }
@@ -681,32 +699,15 @@
681 699
682 700 $response = $this->filter_fields( $response );
683 701
684 702 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
685 - $headers = array(
686 - array(
687 - 'name' => 'Content-Type',
688 - 'value' => $content_type,
689 - ),
690 - );
703 + $response = static::wrap_http_envelope( $status_code, $response, $content_type, $extra );
691 704
692 - foreach ( $extra as $key => $value ) {
693 - $headers[] = array(
694 - 'name' => $key,
695 - 'value' => $value,
696 - );
697 - }
698 -
699 - $response = array(
700 - 'code' => (int) $status_code,
701 - 'headers' => $headers,
702 - 'body' => $response,
703 - );
704 705 $status_code = 200;
705 706 $content_type = 'application/json';
706 707 }
707 708
708 - status_header( (int) $status_code );
709 + status_header( $status_code );
709 710 header( "Content-Type: $content_type" );
710 711 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
711 712 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
712 713 } else {
@@ -719,15 +720,15 @@
719 720 // [1] <https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html>.
720 721 echo "/**/$callback("; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSONP output, not HTML.
721 722
722 723 }
723 - 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.
724 725 if ( $callback ) {
725 726 echo ');';
726 727 }
727 728
728 729 if ( $this->exit ) {
729 - exit;
730 + exit( 0 );
730 731 }
731 732
732 733 return $content_type;
733 734 }
@@ -732,8 +733,42 @@
732 733 return $content_type;
733 734 }
734 735
735 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 + /**
736 771 * Serialize an error.
737 772 *
738 773 * @param WP_Error $error Error.
739 774 * @return array with 'status_code' and 'errors' data.
@@ -739,14 +774,13 @@
739 774 * @return array with 'status_code' and 'errors' data.
740 775 */
741 776 public static function serializable_error( $error ) {
742 777
743 - $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;
744 782
745 - if ( is_array( $status_code ) && isset( $status_code['status_code'] ) ) {
746 - $status_code = $status_code['status_code'];
747 - }
748 -
749 783 if ( ! $status_code ) {
750 784 $status_code = 400;
751 785 }
752 786 $response = array(
@@ -822,9 +856,8 @@
822 856
823 857 foreach ( $response[ $key_to_filter ] as $key => $values ) {
824 858 if ( is_object( $values ) ) {
825 859 if ( is_object( $response[ $key_to_filter ] ) ) {
826 - // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found -- False positive.
827 860 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
828 861 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
829 862 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
830 863 }
@@ -878,13 +911,16 @@
878 911
879 912 /**
880 913 * JSON encode.
881 914 *
882 - * @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 + *
883 919 * @return string|false
884 920 */
885 - public function json_encode( $data ) {
886 - return wp_json_encode( $data );
921 + public function json_encode( $value, $flags = 0, $depth = 512 ) {
922 + return wp_json_encode( $value, $flags, $depth );
887 923 }
888 924
889 925 /**
890 926 * Test if a string ends with a string.
@@ -942,8 +978,37 @@
942 978 return $blog_id;
943 979 }
944 980
945 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 + /**
946 1011 * Returns true if the specified blog ID is a restricted blog
947 1012 *
948 1013 * @param int $blog_id Blog ID.
949 1014 * @return bool
@@ -958,9 +1023,9 @@
958 1023 *
959 1024 * @param array $array Array of Blog IDs.
960 1025 */
961 1026 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
962 - 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.
963 1028 }
964 1029
965 1030 /**
966 1031 * Post like count.
@@ -1016,23 +1081,34 @@
1016 1081 return '';
1017 1082 }
1018 1083
1019 1084 /**
1085 + * Return a count of comment likes.
1086 + * This method is overridden by a child class in WPCOM.
1087 + *
1088 + * @since 13.5
1089 + * @return int
1090 + */
1091 + public function comment_like_count() {
1092 + func_get_args(); // @phan-suppress-current-line PhanPluginUseReturnValueInternalKnown -- This is just here so Phan realizes the wpcom version does this.
1093 + return 0;
1094 + }
1095 +
1096 + /**
1020 1097 * Get avatar URL.
1021 1098 *
1022 1099 * @param string $email Email.
1023 - * @param array $avatar_size Args for `get_avatar_url()`.
1100 + * @param array $args Args for `get_avatar_url()`.
1024 1101 * @return string|false
1025 1102 */
1026 - public function get_avatar_url( $email, $avatar_size = null ) {
1103 + public function get_avatar_url( $email, $args = null ) {
1027 1104 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
1028 - return null === $avatar_size
1029 - ? wpcom_get_avatar_url( $email )
1030 - : wpcom_get_avatar_url( $email, $avatar_size );
1105 + $ret = wpcom_get_avatar_url( $email, $args['size'] ?? 96, $args['default'] ?? '', false, $args['force_default'] ?? false );
1106 + return $ret ? $ret[0] : false;
1031 1107 } else {
1032 - return null === $avatar_size
1108 + return null === $args
1033 1109 ? get_avatar_url( $email )
1034 - : get_avatar_url( $email, $avatar_size );
1110 + : get_avatar_url( $email, $args );
1035 1111 }
1036 1112 }
1037 1113
1038 1114 /**
@@ -1038,9 +1114,9 @@
1038 1114 /**
1039 1115 * Counts the number of comments on a site, including certain comment types.
1040 1116 *
1041 1117 * @param int $post_id Post ID.
1042 - * @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/.
1043 1119 */
1044 1120 public function wp_count_comments( $post_id ) {
1045 1121 global $wpdb;
1046 1122 if ( 0 !== $post_id ) {
@@ -1081,24 +1157,36 @@
1081 1157 if ( empty( $include ) ) {
1082 1158 return wp_count_comments( $post_id );
1083 1159 }
1084 1160
1085 - array_walk( $include, 'esc_sql' );
1086 - $where = sprintf(
1087 - "WHERE comment_type IN ( '%s' )",
1088 - implode( "','", $include )
1089 - );
1161 + // The following caching mechanism is based on what the get_comments() function uses.
1090 1162
1091 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1092 - $count = $wpdb->get_results(
1093 - "SELECT comment_approved, COUNT(*) AS num_comments
1094 - FROM $wpdb->comments
1095 - {$where}
1096 - GROUP BY comment_approved
1097 - "
1098 - );
1099 - // 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' );
1100 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 +
1101 1189 $approved = array(
1102 1190 '0' => 'moderated',
1103 1191 '1' => 'approved',
1104 1192 'spam' => 'spam',
@@ -1223,9 +1311,9 @@
1223 1311
1224 1312 // We still want to exit so that code execution stops where it should.
1225 1313 // Attach the JSON output to the WordPress shutdown handler.
1226 1314 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1227 - exit;
1315 + exit( 0 );
1228 1316 }
1229 1317
1230 1318 /**
1231 1319 * Output the trapped error.
@@ -1246,7 +1334,35 @@
1246 1334 */
1247 1335 public function finish_request() {
1248 1336 if ( function_exists( 'fastcgi_finish_request' ) ) {
1249 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 + }
1250 1366 }
1251 1367 }
1252 1368 }