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 +217 -75 12.1.316.2 View file →
@@ -174,10 +174,9 @@
174 174 * @return WPCOM_JSON_API instance
175 175 */
176 176 public static function init( $method = null, $url = null, $post_body = null ) {
177 177 if ( ! self::$self ) {
178 - $class = function_exists( 'get_called_class' ) ? get_called_class() : __CLASS__; // phpcs:ignore PHPCompatibility.PHP.NewFunctions.get_called_classFound
179 - self::$self = new $class( $method, $url, $post_body );
178 + self::$self = new static( $method, $url, $post_body );
180 179 }
181 180 return self::$self;
182 181 }
183 182
@@ -202,15 +201,28 @@
202 201 $this->endpoints[ $path_versions ][ $endpoint->method ] = $endpoint;
203 202 }
204 203
205 204 /**
206 - * 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.
207 207 *
208 - * @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.
209 209 * @return bool
210 210 */
211 211 public static function is_truthy( $value ) {
212 - switch ( strtolower( (string) $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 +
224 + switch ( strtolower( $value ) ) {
213 225 case '1':
214 226 case 't':
215 227 case 'true':
216 228 return true;
@@ -221,13 +233,25 @@
221 233
222 234 /**
223 235 * Determine if a string is falsey.
224 236 *
225 - * @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.
226 238 * @return bool
227 239 */
228 240 public static function is_falsy( $value ) {
229 - switch ( strtolower( (string) $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 +
253 + switch ( strtolower( $value ) ) {
230 254 case '0':
231 255 case 'f':
232 256 case 'false':
233 257 return true;
@@ -288,15 +312,15 @@
288 312 if ( ! empty( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
289 313 $this->content_type = filter_var( wp_unslash( $_SERVER['HTTP_CONTENT_TYPE'] ) );
290 314 } elseif ( ! empty( $_SERVER['CONTENT_TYPE'] ) ) {
291 315 $this->content_type = filter_var( wp_unslash( $_SERVER['CONTENT_TYPE'] ) );
292 - } elseif ( '{' === $this->post_body[0] ) {
316 + } elseif ( isset( $this->post_body[0] ) && '{' === $this->post_body[0] ) {
293 317 $this->content_type = 'application/json';
294 318 } else {
295 319 $this->content_type = 'application/x-www-form-urlencoded';
296 320 }
297 321
298 - if ( 0 === strpos( strtolower( $this->content_type ), 'multipart/' ) ) {
322 + if ( str_starts_with( strtolower( $this->content_type ), 'multipart/' ) ) {
299 323 // phpcs:ignore WordPress.Security.NonceVerification.Missing
300 324 $this->post_body = http_build_query( stripslashes_deep( $_POST ) );
301 325 $this->files = $_FILES;
302 326 $this->content_type = 'multipart/form-data';
@@ -357,8 +381,19 @@
357 381 return true;
358 382 }
359 383
360 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 + /**
361 396 * Serve.
362 397 *
363 398 * @param bool $exit Whether to exit.
364 399 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
@@ -363,9 +398,9 @@
363 398 * @param bool $exit Whether to exit.
364 399 * @return string|null Content type (assuming it didn't exit), or null in certain error cases.
365 400 */
366 401 public function serve( $exit = true ) {
367 - 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
368 403
369 404 $this->exit = (bool) $exit;
370 405
371 406 // This was causing problems with Jetpack, but is necessary for wpcom
@@ -399,11 +434,12 @@
399 434 }
400 435
401 436 // Normalize path and extract API version.
402 437 $this->path = untrailingslashit( $this->path );
403 - preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches );
404 - $this->path = substr( $this->path, strlen( $matches[0] ) );
405 - $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 + }
406 442
407 443 $allowed_methods = array( 'GET', 'POST' );
408 444 $four_oh_five = false;
409 445
@@ -439,9 +475,10 @@
439 475 $four_oh_five = true;
440 476 }
441 477
442 478 // Find which endpoint to serve.
443 - $found = false;
479 + $found = false;
480 + $path_pieces = array();
444 481 foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
445 482 // @todo Determine if anything depends on this being serialized rather than e.g. JSON.
446 483 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, possibly depended on elsewhere.
447 484 $endpoint_path_versions = unserialize( $endpoint_path_versions );
@@ -462,9 +499,10 @@
462 499 // Normalize.
463 500 $endpoint_path = untrailingslashit( $endpoint_path );
464 501 if ( $is_help ) {
465 502 // Truncate path at help depth.
466 - $endpoint_path = join( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
503 + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable -- $depth is set when $is_help is true.
504 + $endpoint_path = implode( '/', array_slice( explode( '/', $endpoint_path ), 0, $depth ) );
467 505 }
468 506
469 507 // Generate regular expression from sprintf().
470 508 $endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
@@ -500,9 +538,9 @@
500 538 foreach ( $matching_endpoints as $matching_endpoint ) {
501 539 $allowed_methods[] = $matching_endpoint[0]->method;
502 540 }
503 541
504 - header( 'Allow: ' . strtoupper( join( ',', array_unique( $allowed_methods ) ) ) );
542 + header( 'Allow: ' . strtoupper( implode( ',', array_unique( $allowed_methods ) ) ) );
505 543 return $this->output(
506 544 405,
507 545 array(
508 546 'error' => 'not_allowed',
@@ -520,8 +558,9 @@
520 558 * @param string help.
521 559 */
522 560 do_action( 'wpcom_json_api_output', 'help' );
523 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.
524 563 if ( 'json' === $help_content_type ) {
525 564 $docs = array();
526 565 foreach ( $matching_endpoints as $matching_endpoint ) {
527 566 if ( $matching_endpoint[0]->is_publicly_documentable() || $proxied || WPCOM_JSON_API__DEBUG ) {
@@ -536,18 +575,21 @@
536 575 call_user_func( array( $matching_endpoint[0], 'document' ) );
537 576 }
538 577 }
539 578 }
540 - exit;
579 + exit( 0 );
541 580 }
542 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.
543 583 if ( $endpoint->in_testing && ! WPCOM_JSON_API__DEBUG ) {
544 584 return $this->output( 404, '', 'text/plain' );
545 585 }
546 586
547 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.
548 589 do_action( 'wpcom_json_api_output', $endpoint->stat );
549 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.
550 592 $response = $this->process_request( $endpoint, $path_pieces );
551 593
552 594 if ( ! $response && ! is_array( $response ) ) {
553 595 return $this->output( 500, '', 'text/plain' );
@@ -569,8 +611,9 @@
569 611 * @return array|WP_Error Return value from the endpoint's callback.
570 612 */
571 613 public function process_request( WPCOM_JSON_API_Endpoint $endpoint, $path_pieces ) {
572 614 $this->endpoint = $endpoint;
615 + $this->maybe_switch_to_token_user_and_site();
573 616 return call_user_func_array( array( $endpoint, 'callback' ), $path_pieces );
574 617 }
575 618
576 619 /**
@@ -617,9 +660,9 @@
617 660
618 661 // In case output() was called before the callback returned.
619 662 if ( $this->did_output ) {
620 663 if ( $this->exit ) {
621 - exit;
664 + exit( 0 );
622 665 }
623 666 return $content_type;
624 667 }
625 668 $this->did_output = true;
@@ -640,9 +683,9 @@
640 683 }
641 684
642 685 if ( 'text/plain' === $content_type ||
643 686 'text/html' === $content_type ) {
644 - status_header( (int) $status_code );
687 + status_header( $status_code );
645 688 header( 'Content-Type: ' . $content_type );
646 689 foreach ( $extra as $key => $value ) {
647 690 header( "$key: $value" );
648 691 }
@@ -647,9 +690,9 @@
647 690 header( "$key: $value" );
648 691 }
649 692 echo $response; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
650 693 if ( $this->exit ) {
651 - exit;
694 + exit( 0 );
652 695 }
653 696
654 697 return $content_type;
655 698 }
@@ -656,32 +699,15 @@
656 699
657 700 $response = $this->filter_fields( $response );
658 701
659 702 if ( isset( $this->query['http_envelope'] ) && self::is_truthy( $this->query['http_envelope'] ) ) {
660 - $headers = array(
661 - array(
662 - 'name' => 'Content-Type',
663 - 'value' => $content_type,
664 - ),
665 - );
703 + $response = static::wrap_http_envelope( $status_code, $response, $content_type, $extra );
666 704
667 - foreach ( $extra as $key => $value ) {
668 - $headers[] = array(
669 - 'name' => $key,
670 - 'value' => $value,
671 - );
672 - }
673 -
674 - $response = array(
675 - 'code' => (int) $status_code,
676 - 'headers' => $headers,
677 - 'body' => $response,
678 - );
679 705 $status_code = 200;
680 706 $content_type = 'application/json';
681 707 }
682 708
683 - status_header( (int) $status_code );
709 + status_header( $status_code );
684 710 header( "Content-Type: $content_type" );
685 711 if ( isset( $this->query['callback'] ) && is_string( $this->query['callback'] ) ) {
686 712 $callback = preg_replace( '/[^a-z0-9_.]/i', '', $this->query['callback'] );
687 713 } else {
@@ -694,15 +720,15 @@
694 720 // [1] <https://blog.miki.it/2014/7/8/abusing-jsonp-with-rosetta-flash/index.html>.
695 721 echo "/**/$callback("; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is JSONP output, not HTML.
696 722
697 723 }
698 - 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.
699 725 if ( $callback ) {
700 726 echo ');';
701 727 }
702 728
703 729 if ( $this->exit ) {
704 - exit;
730 + exit( 0 );
705 731 }
706 732
707 733 return $content_type;
708 734 }
@@ -707,8 +733,42 @@
707 733 return $content_type;
708 734 }
709 735
710 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 + /**
711 771 * Serialize an error.
712 772 *
713 773 * @param WP_Error $error Error.
714 774 * @return array with 'status_code' and 'errors' data.
@@ -714,14 +774,13 @@
714 774 * @return array with 'status_code' and 'errors' data.
715 775 */
716 776 public static function serializable_error( $error ) {
717 777
718 - $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;
719 782
720 - if ( is_array( $status_code ) ) {
721 - $status_code = $status_code['status_code'];
722 - }
723 -
724 783 if ( ! $status_code ) {
725 784 $status_code = 400;
726 785 }
727 786 $response = array(
@@ -746,9 +805,9 @@
746 805 * @param WP_Error $error Error.
747 806 * @return string Content type (assuming it didn't exit).
748 807 */
749 808 public function output_error( $error ) {
750 - $error_response = $this->serializable_error( $error );
809 + $error_response = static::serializable_error( $error );
751 810
752 811 return $this->output( $error_response['status_code'], $error_response['errors'] );
753 812 }
754 813
@@ -797,9 +856,8 @@
797 856
798 857 foreach ( $response[ $key_to_filter ] as $key => $values ) {
799 858 if ( is_object( $values ) ) {
800 859 if ( is_object( $response[ $key_to_filter ] ) ) {
801 - // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.Found -- False positive.
802 860 $response[ $key_to_filter ]->$key = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
803 861 } elseif ( is_array( $response[ $key_to_filter ] ) ) {
804 862 $response[ $key_to_filter ][ $key ] = (object) array_intersect_key( ( (array) $values ), array_flip( $fields ) );
805 863 }
@@ -853,13 +911,16 @@
853 911
854 912 /**
855 913 * JSON encode.
856 914 *
857 - * @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 + *
858 919 * @return string|false
859 920 */
860 - public function json_encode( $data ) {
861 - return wp_json_encode( $data );
921 + public function json_encode( $value, $flags = 0, $depth = 512 ) {
922 + return wp_json_encode( $value, $flags, $depth );
862 923 }
863 924
864 925 /**
865 926 * Test if a string ends with a string.
@@ -917,8 +978,37 @@
917 978 return $blog_id;
918 979 }
919 980
920 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 + /**
921 1011 * Returns true if the specified blog ID is a restricted blog
922 1012 *
923 1013 * @param int $blog_id Blog ID.
924 1014 * @return bool
@@ -933,9 +1023,9 @@
933 1023 *
934 1024 * @param array $array Array of Blog IDs.
935 1025 */
936 1026 $restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
937 - 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.
938 1028 }
939 1029
940 1030 /**
941 1031 * Post like count.
@@ -991,23 +1081,34 @@
991 1081 return '';
992 1082 }
993 1083
994 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 + /**
995 1097 * Get avatar URL.
996 1098 *
997 1099 * @param string $email Email.
998 - * @param array $avatar_size Args for `get_avatar_url()`.
1100 + * @param array $args Args for `get_avatar_url()`.
999 1101 * @return string|false
1000 1102 */
1001 - public function get_avatar_url( $email, $avatar_size = null ) {
1103 + public function get_avatar_url( $email, $args = null ) {
1002 1104 if ( function_exists( 'wpcom_get_avatar_url' ) ) {
1003 - return null === $avatar_size
1004 - ? wpcom_get_avatar_url( $email )
1005 - : 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;
1006 1107 } else {
1007 - return null === $avatar_size
1108 + return null === $args
1008 1109 ? get_avatar_url( $email )
1009 - : get_avatar_url( $email, $avatar_size );
1110 + : get_avatar_url( $email, $args );
1010 1111 }
1011 1112 }
1012 1113
1013 1114 /**
@@ -1013,9 +1114,9 @@
1013 1114 /**
1014 1115 * Counts the number of comments on a site, including certain comment types.
1015 1116 *
1016 1117 * @param int $post_id Post ID.
1017 - * @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/.
1018 1119 */
1019 1120 public function wp_count_comments( $post_id ) {
1020 1121 global $wpdb;
1021 1122 if ( 0 !== $post_id ) {
@@ -1056,24 +1157,36 @@
1056 1157 if ( empty( $include ) ) {
1057 1158 return wp_count_comments( $post_id );
1058 1159 }
1059 1160
1060 - array_walk( $include, 'esc_sql' );
1061 - $where = sprintf(
1062 - "WHERE comment_type IN ( '%s' )",
1063 - implode( "','", $include )
1064 - );
1161 + // The following caching mechanism is based on what the get_comments() function uses.
1065 1162
1066 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
1067 - $count = $wpdb->get_results(
1068 - "SELECT comment_approved, COUNT(*) AS num_comments
1069 - FROM $wpdb->comments
1070 - {$where}
1071 - GROUP BY comment_approved
1072 - "
1073 - );
1074 - // 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' );
1075 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 +
1076 1189 $approved = array(
1077 1190 '0' => 'moderated',
1078 1191 '1' => 'approved',
1079 1192 'spam' => 'spam',
@@ -1163,8 +1276,9 @@
1163 1276 *
1164 1277 * @param string|WP_Error $message As for `wp_die()`.
1165 1278 * @param string|int $title As for `wp_die()`.
1166 1279 * @param string|array|int $args As for `wp_die()`.
1280 + * @return never
1167 1281 */
1168 1282 public function wp_die_handler( $message, $title = '', $args = array() ) {
1169 1283 // Allow wp_die calls to override HTTP status code...
1170 1284 $args = wp_parse_args(
@@ -1197,9 +1311,9 @@
1197 1311
1198 1312 // We still want to exit so that code execution stops where it should.
1199 1313 // Attach the JSON output to the WordPress shutdown handler.
1200 1314 add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
1201 - exit;
1315 + exit( 0 );
1202 1316 }
1203 1317
1204 1318 /**
1205 1319 * Output the trapped error.
@@ -1220,7 +1334,35 @@
1220 1334 */
1221 1335 public function finish_request() {
1222 1336 if ( function_exists( 'fastcgi_finish_request' ) ) {
1223 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 + }
1224 1366 }
1225 1367 }
1226 1368 }