* Returns true if the specified blog ID is a restricted blog
* @param int $blog_id Blog ID.
public function is_restricted_blog( $blog_id ) {
* Filters all REST API access and return a 403 unauthorized response for all Restricted blog IDs.
* @param array $array Array of Blog IDs.
$restricted_blog_ids = apply_filters( 'wpcom_json_api_restricted_blog_ids', array() );
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.
* @param int $blog_id Blog ID.
* @param int $post_id Post ID.
public function post_like_count( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
* @param int $blog_id Blog ID.
* @param int $post_id Post ID.
public function is_liked( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
* @param int $blog_id Blog ID.
* @param int $post_id Post ID.
public function is_reblogged( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
* @param int $blog_id Blog ID.
public function is_following( $blog_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
* @param int $blog_id Blog ID.
* @param int $post_id Post ID.
public function add_global_ID( $blog_id, $post_id ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable, WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
* Return a count of comment likes.
* This method is overridden by a child class in WPCOM.
public function comment_like_count() {
func_get_args(); // @phan-suppress-current-line PhanPluginUseReturnValueInternalKnown -- This is just here so Phan realizes the wpcom version does this.
* @param string $email Email.
* @param array $args Args for `get_avatar_url()`.
public function get_avatar_url( $email, $args = null ) {
if ( function_exists( 'wpcom_get_avatar_url' ) ) {
$ret = wpcom_get_avatar_url( $email, $args['size'] ?? 96, $args['default'] ?? '', false, $args['force_default'] ?? false );
return $ret ? $ret[0] : false;
? get_avatar_url( $email )
: get_avatar_url( $email, $args );
* Counts the number of comments on a site, including certain comment types.
* @param int $post_id Post ID.
* @return array Array of counts, matching the output of https://developer.wordpress.org/reference/functions/get_comment_count/.
public function wp_count_comments( $post_id ) {
return wp_count_comments( $post_id );
* Exclude certain comment types from comment counts in the REST API.
* @param array Array of comment types to exclude (default: 'order_note', 'webhook_delivery', 'review', 'action_log')
$exclude = apply_filters_deprecated( 'jetpack_api_exclude_comment_types_count', array( 'order_note', 'webhook_delivery', 'review', 'action_log' ), 'jetpack-11.1', 'jetpack_api_include_comment_types_count' ); // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
* Include certain comment types in comment counts in the REST API.
* Note: the default array of comment types includes an empty string,
* to support comments posted before WP 5.5, that used an empty string as comment type.
* @param array Array of comment types to include (default: 'comment', 'pingback', 'trackback')
$include = apply_filters(
'jetpack_api_include_comment_types_count',
array( 'comment', 'pingback', 'trackback', '' )
if ( empty( $include ) ) {
return wp_count_comments( $post_id );
// The following caching mechanism is based on what the get_comments() function uses.
$key = md5( serialize( $include ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$last_changed = wp_cache_get_last_changed( 'comment' );
$cache_key = "wp_count_comments:$key:$last_changed";
$count = wp_cache_get( $cache_key, 'jetpack-json-api' );
if ( false === $count ) {
array_walk( $include, 'esc_sql' );
"WHERE comment_type IN ( '%s' )",
implode( "','", $include )
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- `$where` is built with escaping just above.
$count = $wpdb->get_results(
"SELECT comment_approved, COUNT(*) AS num_comments
GROUP BY comment_approved
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
wp_cache_add( $cache_key, $count, 'jetpack-json-api' );
'post-trashed' => 'post-trashed',
// <https://developer.wordpress.org/reference/functions/get_comment_count/#source>
foreach ( $count as $row ) {
if ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash', 'spam' ), true ) ) {
$counts['all'] += $row->num_comments;
$counts['total_comments'] += $row->num_comments;
} elseif ( ! in_array( $row->comment_approved, array( 'post-trashed', 'trash' ), true ) ) {
$counts['total_comments'] += $row->num_comments;
if ( isset( $approved[ $row->comment_approved ] ) ) {
$counts[ $approved[ $row->comment_approved ] ] = $row->num_comments;
foreach ( $approved as $key ) {
if ( empty( $counts[ $key ] ) ) {
$counts = (object) $counts;
* Traps `wp_die()` calls and outputs a JSON response instead.
* The result is always output, never returned.
* @param string|null $error_code Call with string to start the trapping. Call with null to stop.
* @param int $http_status HTTP status code, 400 by default.
public function trap_wp_die( $error_code = null, $http_status = 400 ) {
// Determine the filter name; based on the conditionals inside the wp_die function.
if ( wp_is_json_request() ) {
$die_handler = 'wp_die_json_handler';
} elseif ( wp_is_jsonp_request() ) {
$die_handler = 'wp_die_jsonp_handler';
} elseif ( wp_is_xml_request() ) {
$die_handler = 'wp_die_xml_handler';
$die_handler = 'wp_die_handler';
if ( $error_code === null ) {
$this->trapped_error = null;
remove_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
// If API called via PHP, bail: don't do our custom wp_die(). Do the normal wp_die().
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
} elseif ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) {
$this->trapped_error = array(
'status' => $http_status,
add_filter( $die_handler, array( $this, 'wp_die_handler_callback' ) );
* Filter function for `wp_die_handler` and similar filters.
public function wp_die_handler_callback() {
return array( $this, 'wp_die_handler' );
* Handler for `wp_die` calls.
* @param string|WP_Error $message As for `wp_die()`.
* @param string|int $title As for `wp_die()`.
* @param string|array|int $args As for `wp_die()`.
public function wp_die_handler( $message, $title = '', $args = array() ) {
// Allow wp_die calls to override HTTP status code...
'response' => $this->trapped_error['status'],
if ( 500 !== (int) $args['response'] ) {
$this->trapped_error['status'] = $args['response'];
$message = "$title: $message";
$this->trapped_error['message'] = wp_kses( $message, array() );
switch ( $this->trapped_error['code'] ) {
if ( did_action( 'comment_duplicate_trigger' ) ) {
$this->trapped_error['code'] = 'comment_duplicate';
} elseif ( did_action( 'comment_flood_trigger' ) ) {
$this->trapped_error['code'] = 'comment_flood';
// We still want to exit so that code execution stops where it should.
// Attach the JSON output to the WordPress shutdown handler.
add_action( 'shutdown', array( $this, 'output_trapped_error' ), 0 );
* Output the trapped error.
public function output_trapped_error() {
$this->exit = false; // We're already exiting once. Don't do it twice.
$this->trapped_error['status'],
'error' => $this->trapped_error['code'],
'message' => $this->trapped_error['message'],
public function finish_request() {
if ( function_exists( 'fastcgi_finish_request' ) ) {
return fastcgi_finish_request();
* Initialize the locale if different from 'en'.
* @param string $locale The locale to initialize.
public function init_locale( $locale ) {
if ( 'en' !== $locale ) {
// .org mo files are named slightly different from .com, and all we have is this the locale -- try to guess them.
if ( str_contains( $locale, '-' ) ) {
$locale_pieces = explode( '-', $locale );
$new_locale = $locale_pieces[0];
$new_locale .= ( ! empty( $locale_pieces[1] ) ) ? '_' . strtoupper( $locale_pieces[1] ) : '';
} else { // phpcs:ignore Universal.ControlStructures.DisallowLonelyIf.Found
// .com might pass 'fr' because thats what our language files are named as, where core seems
// to do fr_FR - so try that if we don't think we can load the file.
if ( ! file_exists( WP_LANG_DIR . '/' . $locale . '.mo' ) ) {
$new_locale = $locale . '_' . strtoupper( $locale );
if ( file_exists( WP_LANG_DIR . '/' . $new_locale . '.mo' ) ) {
unload_textdomain( 'default' );
load_textdomain( 'default', WP_LANG_DIR . '/' . $new_locale . '.mo' );