| @@ -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 | } |