PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 14.4
Jetpack – WP Security, Backup, Speed, & Growth v14.4
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 14.3.1 All 501 releases
← All changes | class.json-api.php +159 -53 12.3.214.4 View file →
@@ -201,14 +201,27 @@
201 201 $this->endpoints[ $path_versions ][ $endpoint->method ] = $endpoint;
202 202 }
203 203
204 204 /**
205 - * Determine if a string is truthy.
205 + * Determine if a string is truthy. If it's not a string, which can happen with
206 + * not well-formed data coming from Jetpack sites, we still consider it a truthy value.
206 207 *
207 - * @param string $value "1", "t", and "true" (case insensitive) are falsey, everything else isn't.
208 + * @param mixed $value true, 1, "1", "t", and "true" (case insensitive) are truthy, everything else isn't.
208 209 * @return bool
209 210 */
210 211 public static function is_truthy( $value ) {
212 + if ( true === $value ) {
213 + return true;
214 + }
215 +
216 + if ( 1 === $value ) {
217 + return true;
218 + }
219 +
220 + if ( ! is_string( $value ) ) {
221 + return false;
222 + }
223 +
211 224 switch ( strtolower( (string) $value ) ) {
212 225 case '1':
213 226 case 't':
214 227 case 'true':
@@ -220,12 +233,24 @@
220 233
221 234 /**
222 235 * Determine if a string is falsey.
223 236 *
224 - * @param string $value "0", "f", and "false" (case insensitive) are falsey, everything else isn't.
237 + * @param mixed $value false, 0, "0", "f", and "false" (case insensitive) are falsey, everything else isn't.
225 238 * @return bool
226 239 */
227 240 public static function is_falsy( $value ) {
241 + if ( false === $value ) {
242 + return true;
243 + }
244 +
245 + if ( 0 === $value ) {
246 + return true;
247 + }
248 +
249 + if ( ! is_string( $value ) ) {
250 + return false;
251 + }
252 +
228 253 switch ( strtolower( (string) $value ) ) {
229 254 case '0':
230 255 case 'f':
231 256 case 'false':
@@ -287,15 +312,15 @@
287 312 if ( ! empty( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
288 313 $this->content_type = filter_var( wp_unslash( $_SERVER['HTTP_CONTENT_TYPE'] ) );
289 314 } elseif ( ! empty( $_SERVER['CONTENT_TYPE'] ) ) {
290 315 $this->content_type = filter_var( wp_unslash( $_SERVER['CONTENT_TYPE'] ) );
291 - } elseif ( '{' === $this->post_body[0] ) {
316 + } elseif ( isset( $this->post_body[0] ) && '{' === $this->post_body[0] ) {
292 317 $this->content_type = 'application/json';
293 318 } else {
294 319 $this->content_type = 'application/x-www-form-urlencoded';
295 320 }
296 321
297 - if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
322 + if ( str_starts_with( strtolower( $this->content_type ), 'multipart/' ) ) {
298 323 // phpcs:ignore WordPress.Security.NonceVerification.Missing
299 324 $this->post_body = http_build_query( stripslashes_deep( $_POST ) );
300 325 $this->files = $_FILES;
301 326 $this->content_type = 'multipart/form-data';
@@ -356,8 +381,19 @@
356 381 return true;
357 382 }
358 383
359 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 + /**
360 396 * Serve.
361 397 *
362 398 * @param bool $exit Whether to exit.
363 399 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
@@ -398,11 +434,12 @@
398 434 }
399 435
400 436 // Normalize path and extract API version.
401 437 $this->path = untrailingslashit( $this->path );
402 - preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
403 - $this->path = substr( $this->path, strlen( $matches[0] ) );
404 - $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 + }
405 442
406 443 $allowed_methods = array( 'GET', 'POST' );
407 444 $four_oh_five = false;
408 445
@@ -535,9 +572,9 @@
535 572 call_user_func( array( $matching_endpoint[0], 'document' ) );
536 573 }
537 574 }
538 575 }
539 - exit;
576 + exit( 0 );
540 577 }
541 578
542 579 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
543 580 return $this->output( 404, '', 'text/plain' );
@@ -616,9 +653,9 @@
616 653
617 654 // In case output() was called before the callback returned.
618 655 if ( $this->did_output ) {
619 656 if ( $this->exit ) {
620 - exit;
657 + exit( 0 );
621 658 }
622 659 return $content_type;
623 660 }
624 661 $this->did_output = true;
@@ -646,9 +683,9 @@
646 683 header( "$key: $value" );
647 684 }
648 685 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
649 686 if ( $this->exit ) {
650 - exit;
687 + exit( 0 );
651 688 }
652 689
653 690 return $content_type;
654 691 }
@@ -655,27 +692,10 @@
655 692
656 693 $response = $this->filter_fields( $response );
657 694
658 695 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
659 - $headers = array(
660 - array(
661 - 'name' => 'Content-Type',
662 - 'value' => $content_type,
663 - ),
664 - );
696 + $response = static::wrap_http_envelope( $status_code, $response, $content_type, $extra );
665 697
666 - foreach ( $extra as $key => $value ) {
667 - $headers[] = array(
668 - 'name' => $key,
669 - 'value' => $value,
670 - );
671 - }
672 -
673 - $response = array(
674 - 'code' => (int) $status_code,
675 - 'headers' => $headers,
676 - 'body' => $response,
677 - );
678 698 $status_code = 200;
679 699 $content_type = 'application/json';
680 700 }
681 701
@@ -699,9 +719,9 @@
699 719 echo ');';
700 720 }
701 721
702 722 if ( $this->exit ) {
703 - exit;
723 + exit( 0 );
704 724 }
705 725
706 726 return $content_type;
707 727 }
@@ -706,8 +726,42 @@
706 726 return $content_type;
707 727 }
708 728
709 729 /**
730 + * Wrap JSON API response into an HTTP 200 one.
731 + *
732 + * @param int $status_code HTTP status code.
733 + * @param mixed $response Response body.
734 + * @param string $content_type Content type.
735 + * @param array|null $extra Extra data.
736 + *
737 + * @return array
738 + */
739 + public static function wrap_http_envelope( $status_code, $response, $content_type, $extra = null ) {
740 + $headers = array(
741 + array(
742 + 'name' => 'Content-Type',
743 + 'value' => $content_type,
744 + ),
745 + );
746 +
747 + if ( is_array( $extra ) ) {
748 + foreach ( $extra as $key => $value ) {
749 + $headers[] = array(
750 + 'name' => $key,
751 + 'value' => $value,
752 + );
753 + }
754 + }
755 +
756 + return array(
757 + 'code' => (int) $status_code,
758 + 'headers' => $headers,
759 + 'body' => $response,
760 + );
761 + }
762 +
763 + /**
710 764 * Serialize an error.
711 765 *
712 766 * @param WP_Error $error Error.
713 767 * @return array with 'status_code' and 'errors' data.
@@ -715,9 +769,9 @@
715 769 public static function serializable_error( $error ) {
716 770
717 771 $status_code = $error->get_error_data();
718 772
719 - if ( is_array( $status_code ) ) {
773 + if ( is_array( $status_code ) && isset( $status_code['status_code'] ) ) {
720 774 $status_code = $status_code['status_code'];
721 775 }
722 776
723 777 if ( ! $status_code ) {
@@ -990,23 +1044,34 @@
990 1044 return '';
991 1045 }
992 1046
993 1047 /**
1048 + * Return a count of comment likes.
1049 + * This method is overridden by a child class in WPCOM.
1050 + *
1051 + * @since 13.5
1052 + * @return int
1053 + */
1054 + public function comment_like_count() {
1055 + func_get_args(); // @phan-suppress-current-line PhanPluginUseReturnValueInternalKnown -- This is just here so Phan realizes the wpcom version does this.
1056 + return 0;
1057 + }
1058 +
1059 + /**
994 1060 * Get avatar URL.
995 1061 *
996 1062 * @param string $email Email.
997 - * @param array $avatar_size Args for `get_avatar_url()`.
1063 + * @param array $args Args for `get_avatar_url()`.
998 1064 * @return string|false
999 1065 */
1000 - public function get_avatar_url( $email, $avatar_size = null ) {
1066 + public function get_avatar_url( $email, $args = null ) {
1001 1067 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
1002 - return null === $avatar_size
1003 - ? wpcom_get_avatar_url( $email )
1004 - : wpcom_get_avatar_url( $email, $avatar_size );
1068 + $ret = wpcom_get_avatar_url( $email, $args['size'] ?? 96, $args['default'] ?? '', false, $args['force_default'] ?? false );
1069 + return $ret ? $ret[0] : false;
1005 1070 } else {
1006 - return null === $avatar_size
1071 + return null === $args
1007 1072 ? get_avatar_url( $email )
1008 - : get_avatar_url( $email, $avatar_size );
1073 + : get_avatar_url( $email, $args );
1009 1074 }
1010 1075 }
1011 1076
1012 1077 /**
@@ -1055,24 +1120,36 @@
1055 1120 if ( empty( $include ) ) {
1056 1121 return wp_count_comments( $post_id );
1057 1122 }
1058 1123
1059 - array_walk( $include, 'esc_sql' );
1060 - $where = sprintf(
1061 - "WHERE comment_type IN ( '%s' )",
1062 - implode( "','", $include )
1063 - );
1124 + // The following caching mechanism is based on what the get_comments() function uses.
1064 1125
1065 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1066 - $count = $wpdb->get_results(
1067 - "SELECT comment_approved, COUNT(*) AS num_comments
1068 - FROM $wpdb->comments
1069 - {$where}
1070 - GROUP BY comment_approved
1071 - "
1072 - );
1073 - // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1126 + $key = md5( serialize( $include ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
1127 + $last_changed = wp_cache_get_last_changed( 'comment' );
1074 1128
1129 + $cache_key = "wp_count_comments:$key:$last_changed";
1130 + $count = wp_cache_get( $cache_key, 'jetpack-json-api' );
1131 +
1132 + if ( false === $count ) {
1133 + array_walk( $include, 'esc_sql' );
1134 + $where = sprintf(
1135 + "WHERE comment_type IN ( '%s' )",
1136 + implode( "','", $include )
1137 + );
1138 +
1139 + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1140 + $count = $wpdb->get_results(
1141 + "SELECT comment_approved, COUNT(*) AS num_comments
1142 + FROM $wpdb->comments
1143 + {$where}
1144 + GROUP BY comment_approved
1145 + "
1146 + );
1147 + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
1148 +
1149 + wp_cache_add( $cache_key, $count, 'jetpack-json-api' );
1150 + }
1151 +
1075 1152 $approved = array(
1076 1153 '0' => 'moderated',
1077 1154 '1' => 'approved',
1078 1155 'spam' => 'spam',
@@ -1162,8 +1239,9 @@
1162 1239 *
1163 1240 * @param string|WP_Error $message As for `wp_die()`.
1164 1241 * @param string|int $title As for `wp_die()`.
1165 1242 * @param string|array|int $args As for `wp_die()`.
1243 + * @return never
1166 1244 */
1167 1245 public function wp_die_handler( $message, $title = '', $args = array() ) {
1168 1246 // Allow wp_die calls to override HTTP status code...
1169 1247 $args = wp_parse_args(
@@ -1196,9 +1274,9 @@
1196 1274
1197 1275 // We still want to exit so that code execution stops where it should.
1198 1276 // Attach the JSON output to the WordPress shutdown handler.
1199 1277 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1200 - exit;
1278 + exit( 0 );
1201 1279 }
1202 1280
1203 1281 /**
1204 1282 * Output the trapped error.
@@ -1219,7 +1297,35 @@
1219 1297 */
1220 1298 public function finish_request() {
1221 1299 if ( function_exists( 'fastcgi_finish_request' ) ) {
1222 1300 return fastcgi_finish_request();
1301 + }
1302 + }
1303 +
1304 + /**
1305 + * Initialize the locale if different from 'en'.
1306 + *
1307 + * @param string $locale The locale to initialize.
1308 + */
1309 + public function init_locale( $locale ) {
1310 + if ( 'en' !== $locale ) {
1311 + // .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
1312 + $new_locale = $locale;
1313 + if ( str_contains( $locale, '-' ) ) {
1314 + $locale_pieces = explode( '-', $locale );
1315 + $new_locale = $locale_pieces[0];
1316 + $new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
1317 + } else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
1318 + // .com might pass 'fr' because thats what our language files are named as, where core seems
1319 + // to do fr_FR - so try that if we don't think we can load the file.
1320 + if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
1321 + $new_locale = $locale . '_' . strtoupper( $locale );
1322 + }
1323 + }
1324 +
1325 + if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
1326 + unload_textdomain( 'default' );
1327 + load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );
1328 + }
1223 1329 }
1224 1330 }
1225 1331 }