PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 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 All 506 releases
← All changes | class.json-api.php +184 -66 13.1.5 → 16.3-a.7 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;
@@ -312,9 +312,9 @@
312 312 if ( ! empty( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
313 313 $this->content_type = filter_var( wp_unslash( $_SERVER['HTTP_CONTENT_TYPE'] ) );
314 314 } elseif ( ! empty( $_SERVER['CONTENT_TYPE'] ) ) {
315 315 $this->content_type = filter_var( wp_unslash( $_SERVER['CONTENT_TYPE'] ) );
316 - } elseif ( '{' === $this->post_body[0] ) {
316 + } elseif ( isset( $this->post_body[0] ) && '{' === $this->post_body[0] ) {
317 317 $this->content_type = 'application/json';
318 318 } else {
319 319 $this->content_type = 'application/x-www-form-urlencoded';
320 320 }
@@ -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
@@ -423,11 +434,12 @@
423 434 }
424 435
425 436 // Normalize path and extract API version.
426 437 $this->path = untrailingslashit( $this->path );
427 - preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
428 - $this->path = substr( $this->path, strlen( $matches[0] ) );
429 - $this->version = $matches[1];
438 + if ( preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches ) ) {
439 + $this->path = substr( $this->path, strlen( $matches[0] ) );
440 + $this->version = $matches[1];
441 + }
430 442
431 443 $allowed_methods = array( 'GET', 'POST' );
432 444 $four_oh_five = false;
433 445
@@ -463,9 +475,10 @@
463 475 $four_oh_five = true;
464 476 }
465 477
466 478 // Find which endpoint to serve.
467 - $found = false;
479 + $found = false;
480 + $path_pieces = array();
468 481 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
469 482 // @todo Determine if anything depends on this being serialized rather than e.g. JSON.
470 483 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, possibly depended on elsewhere.
471 484 $endpoint_path_versions = unserialize( $endpoint_path_versions );
@@ -486,8 +499,9 @@
486 499 // Normalize.
487 500 $endpoint_path = untrailingslashit( $endpoint_path );
488 501 if ( $is_help ) {
489 502 // Truncate path at help depth.
503 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $depth is set when $is_help is true.
490 504 $endpoint_path = implode( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
491 505 }
492 506
493 507 // Generate regular expression from sprintf().
@@ -544,8 +558,9 @@
544 558 * @param string help.
545 559 */
546 560 do_action( 'wpcom_json_api_output', 'help' );
547 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.
548 563 if ( 'json' === $help_content_type ) {
549 564 $docs = array();
550 565 foreach ( $matching_endpoints as $matching_endpoint ) {
551 566 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
@@ -560,18 +575,21 @@
560 575 call_user_func( array( $matching_endpoint[0], 'document' ) );
561 576 }
562 577 }
563 578 }
564 - exit;
579 + exit( 0 );
565 580 }
566 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.
567 583 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
568 584 return $this->output( 404, '', 'text/plain' );
569 585 }
570 586
571 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.
572 589 do_action( 'wpcom_json_api_output', $endpoint->stat );
573 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.
574 592 $response = $this->process_request( $endpoint, $path_pieces );
575 593
576 594 if ( ! $response && ! is_array( $response ) ) {
577 595 return $this->output( 500, '', 'text/plain' );
@@ -593,8 +611,9 @@
593 611 * @return array|WP_Error Return value from the endpoint's callback.
594 612 */
595 613 public function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
596 614 $this->endpoint = $endpoint;
615 + $this->maybe_switch_to_token_user_and_site();
597 616 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
598 617 }
599 618
600 619 /**
@@ -641,9 +660,9 @@
641 660
642 661 // In case output() was called before the callback returned.
643 662 if ( $this->did_output ) {
644 663 if ( $this->exit ) {
645 - exit;
664 + exit( 0 );
646 665 }
647 666 return $content_type;
648 667 }
649 668 $this->did_output = true;
@@ -664,9 +683,9 @@
664 683 }
665 684
666 685 if ( 'text/plain' === $content_type ||
667 686 'text/html' === $content_type ) {
668 - status_header( (int) $status_code );
687 + status_header( $status_code );
669 688 header( 'Content-Type: ' . $content_type );
670 689 foreach ( $extra as $key => $value ) {
671 690 header( "$key: $value" );
672 691 }
@@ -671,9 +690,9 @@
671 690 header( "$key: $value" );
672 691 }
673 692 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
674 693 if ( $this->exit ) {
675 - exit;
694 + exit( 0 );
676 695 }
677 696
678 697 return $content_type;
679 698 }
@@ -680,32 +699,15 @@
680 699
681 700 $response = $this->filter_fields( $response );
682 701
683 702 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
684 - $headers = array(
685 - array(
686 - 'name' => 'Content-Type',
687 - 'value' => $content_type,
688 - ),
689 - );
703 + $response = static::wrap_http_envelope( $status_code, $response, $content_type, $extra );
690 704
691 - foreach ( $extra as $key => $value ) {
692 - $headers[] = array(
693 - 'name' => $key,
694 - 'value' => $value,
695 - );
696 - }
697 -
698 - $response = array(
699 - 'code' => (int) $status_code,
700 - 'headers' => $headers,
701 - 'body' => $response,
702 - );
703 705 $status_code = 200;
704 706 $content_type = 'application/json';
705 707 }
706 708
707 - status_header( (int) $status_code );
709 + status_header( $status_code );
708 710 header( "Content-Type: $content_type" );
709 711 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
710 712 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
711 713 } else {
@@ -718,15 +720,15 @@
718 720 // [1] <https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html>.
719 721 echo "/**/$callback("; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSONP output, not HTML.
720 722
721 723 }
722 - 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.
723 725 if ( $callback ) {
724 726 echo ');';
725 727 }
726 728
727 729 if ( $this->exit ) {
728 - exit;
730 + exit( 0 );
729 731 }
730 732
731 733 return $content_type;
732 734 }
@@ -731,8 +733,42 @@
731 733 return $content_type;
732 734 }
733 735
734 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 + /**
735 771 * Serialize an error.
736 772 *
737 773 * @param WP_Error $error Error.
738 774 * @return array with 'status_code' and 'errors' data.
@@ -738,14 +774,13 @@
738 774 * @return array with 'status_code' and 'errors' data.
739 775 */
740 776 public static function serializable_error( $error ) {
741 777
742 - $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;
743 782
744 - if ( is_array( $status_code ) ) {
745 - $status_code = $status_code['status_code'];
746 - }
747 -
748 783 if ( ! $status_code ) {
749 784 $status_code = 400;
750 785 }
751 786 $response = array(
@@ -821,9 +856,8 @@
821 856
822 857 foreach ( $response[ $key_to_filter ] as $key => $values ) {
823 858 if ( is_object( $values ) ) {
824 859 if ( is_object( $response[ $key_to_filter ] ) ) {
825 - // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found -- False positive.
826 860 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
827 861 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
828 862 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
829 863 }
@@ -877,13 +911,16 @@
877 911
878 912 /**
879 913 * JSON encode.
880 914 *
881 - * @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 + *
882 919 * @return string|false
883 920 */
884 - public function json_encode( $data ) {
885 - return wp_json_encode( $data );
921 + public function json_encode( $value, $flags = 0, $depth = 512 ) {
922 + return wp_json_encode( $value, $flags, $depth );
886 923 }
887 924
888 925 /**
889 926 * Test if a string ends with a string.
@@ -941,8 +978,37 @@
941 978 return $blog_id;
942 979 }
943 980
944 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 + /**
945 1011 * Returns true if the specified blog ID is a restricted blog
946 1012 *
947 1013 * @param int $blog_id Blog ID.
948 1014 * @return bool
@@ -957,9 +1023,9 @@
957 1023 *
958 1024 * @param array $array Array of Blog IDs.
959 1025 */
960 1026 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
961 - 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.
962 1028 }
963 1029
964 1030 /**
965 1031 * Post like count.
@@ -1015,23 +1081,34 @@
1015 1081 return '';
1016 1082 }
1017 1083
1018 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 + /**
1019 1097 * Get avatar URL.
1020 1098 *
1021 1099 * @param string $email Email.
1022 - * @param array $avatar_size Args for `get_avatar_url()`.
1100 + * @param array $args Args for `get_avatar_url()`.
1023 1101 * @return string|false
1024 1102 */
1025 - public function get_avatar_url( $email, $avatar_size = null ) {
1103 + public function get_avatar_url( $email, $args = null ) {
1026 1104 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
1027 - return null === $avatar_size
1028 - ? wpcom_get_avatar_url( $email )
1029 - : 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;
1030 1107 } else {
1031 - return null === $avatar_size
1108 + return null === $args
1032 1109 ? get_avatar_url( $email )
1033 - : get_avatar_url( $email, $avatar_size );
1110 + : get_avatar_url( $email, $args );
1034 1111 }
1035 1112 }
1036 1113
1037 1114 /**
@@ -1037,9 +1114,9 @@
1037 1114 /**
1038 1115 * Counts the number of comments on a site, including certain comment types.
1039 1116 *
1040 1117 * @param int $post_id Post ID.
1041 - * @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/.
1042 1119 */
1043 1120 public function wp_count_comments( $post_id ) {
1044 1121 global $wpdb;
1045 1122 if ( 0 !== $post_id ) {
@@ -1080,24 +1157,36 @@
1080 1157 if ( empty( $include ) ) {
1081 1158 return wp_count_comments( $post_id );
1082 1159 }
1083 1160
1084 - array_walk( $include, 'esc_sql' );
1085 - $where = sprintf(
1086 - "WHERE comment_type IN ( '%s' )",
1087 - implode( "','", $include )
1088 - );
1161 + // The following caching mechanism is based on what the get_comments() function uses.
1089 1162
1090 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1091 - $count = $wpdb->get_results(
1092 - "SELECT comment_approved, COUNT(*) AS num_comments
1093 - FROM $wpdb->comments
1094 - {$where}
1095 - GROUP BY comment_approved
1096 - "
1097 - );
1098 - // 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' );
1099 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 +
1100 1189 $approved = array(
1101 1190 '0' => 'moderated',
1102 1191 '1' => 'approved',
1103 1192 'spam' => 'spam',
@@ -1187,8 +1276,9 @@
1187 1276 *
1188 1277 * @param string|WP_Error $message As for `wp_die()`.
1189 1278 * @param string|int $title As for `wp_die()`.
1190 1279 * @param string|array|int $args As for `wp_die()`.
1280 + * @return never
1191 1281 */
1192 1282 public function wp_die_handler( $message, $title = '', $args = array() ) {
1193 1283 // Allow wp_die calls to override HTTP status code...
1194 1284 $args = wp_parse_args(
@@ -1221,9 +1311,9 @@
1221 1311
1222 1312 // We still want to exit so that code execution stops where it should.
1223 1313 // Attach the JSON output to the WordPress shutdown handler.
1224 1314 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1225 - exit;
1315 + exit( 0 );
1226 1316 }
1227 1317
1228 1318 /**
1229 1319 * Output the trapped error.
@@ -1244,7 +1334,35 @@
1244 1334 */
1245 1335 public function finish_request() {
1246 1336 if ( function_exists( 'fastcgi_finish_request' ) ) {
1247 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 + }
1248 1366 }
1249 1367 }
1250 1368 }