<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
* @package automattic/jetpack
use Automattic\Jetpack\Status;
if ( ! defined( 'WPCOM_JSON_API__DEBUG' ) ) {
define( 'WPCOM_JSON_API__DEBUG', false );
require_once __DIR__ . '/sal/class.json-api-platform.php';
* @todo This should be private.
public static $self = null;
* @var WPCOM_JSON_API_Endpoint[]
public $endpoints = array();
* Endpoint being processed.
* @var WPCOM_JSON_API_Endpoint
public $token_details = array();
* Path part of the request URL.
* Version extracted from the request URL.
* Post body, if the request is a POST.
public $post_body = null;
* Copy of `$_FILES` if the request is a POST.
* Content type of the request.
public $content_type = null;
* Value of `$_SERVER['HTTP_ACCEPT']`, if any
* Value of `$_SERVER['HTTPS']`, or "--UNset--" if unset.
public $_server_https; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
* Whether to exit after serving a response.
public $public_api_scheme = 'https';
public $output_status_code = 200;
public $trapped_error = null;
* Whether output has been done.
public $did_output = false;
public $extra_headers = array();
public $amp_source_origin = null;
* @param string|null $method As for `$this->setup_inputs()`.
* @param string|null $url As for `$this->setup_inputs()`.
* @param string|null $post_body As for `$this->setup_inputs()`.
* @return WPCOM_JSON_API instance
public static function init( $method = null, $url = null, $post_body = null ) {
self::$self = new static( $method, $url, $post_body );
* @param WPCOM_JSON_API_Endpoint $endpoint Endpoint to add.
public function add( WPCOM_JSON_API_Endpoint $endpoint ) {
// @todo Determine if anything depends on this being serialized rather than e.g. JSON.
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Legacy, possibly depended on elsewhere.
$path_versions = serialize(
if ( ! isset( $this->endpoints[ $path_versions ] ) ) {
$this->endpoints[ $path_versions ] = array();
$this->endpoints[ $path_versions ][ $endpoint->method ] = $endpoint;
* Determine if a string is truthy. If it's not a string, which can happen with
* not well-formed data coming from Jetpack sites, we still consider it a truthy value.
* @param mixed $value true, 1, "1", "t", and "true" (case insensitive) are truthy, everything else isn't.
public static function is_truthy( $value ) {
if ( ! is_string( $value ) ) {
switch ( strtolower( (string) $value ) ) {
* Determine if a string is falsey.
* @param mixed $value false, 0, "0", "f", and "false" (case insensitive) are falsey, everything else isn't.
public static function is_falsy( $value ) {
if ( false === $value ) {
if ( ! is_string( $value ) ) {
switch ( strtolower( (string) $value ) ) {
* @todo This should be private.
* @param string|null $method As for `$this->setup_inputs()`.
* @param string|null $url As for `$this->setup_inputs()`.
* @param string|null $post_body As for `$this->setup_inputs()`.
public function __construct( $method = null, $url = null, $post_body = null ) {
$this->setup_inputs( $method, $url, $post_body );
* @param string|null $method Request HTTP method. Fetched from `$_SERVER` if null.
* @param string|null $url URL requested. Determined from `$_SERVER` if null.
* @param string|null $post_body POST body. Read from `php://input` if null and method is POST.
public function setup_inputs( $method = null, $url = null, $post_body = null ) {
if ( $method === null ) {
$this->method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : '';
$this->method = strtoupper( $method );
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sniff misses the esc_url_raw.
$this->url = esc_url_raw( set_url_scheme( 'http://' . ( isset( $_SERVER['HTTP_HOST'] ) ? wp_unslash( $_SERVER['HTTP_HOST'] ) : '' ) . ( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' ) ) );
$parsed = wp_parse_url( $this->url );
if ( ! empty( $parsed['path'] ) ) {
$this->path = $parsed['path'];
if ( ! empty( $parsed['query'] ) ) {
wp_parse_str( $parsed['query'], $this->query );
if ( ! empty( $_SERVER['HTTP_ACCEPT'] ) ) {
$this->accept = filter_var( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
if ( 'POST' === $this->method ) {
if ( $post_body === null ) {
$this->post_body = file_get_contents( 'php://input' );
if ( ! empty( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
$this->content_type = filter_var( wp_unslash( $_SERVER['HTTP_CONTENT_TYPE'] ) );
} elseif ( ! empty( $_SERVER['CONTENT_TYPE'] ) ) {
$this->content_type = filter_var( wp_unslash( $_SERVER['CONTENT_TYPE'] ) );
} elseif ( isset( $this->post_body[0] ) && '{' === $this->post_body[0] ) {
$this->content_type = 'application/json';
$this->content_type = 'application/x-www-form-urlencoded';
if ( str_starts_with( strtolower( $this->content_type ), 'multipart/' ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$this->post_body = http_build_query( stripslashes_deep( $_POST ) );
$this->content_type = 'multipart/form-data';
$this->post_body = $post_body;
$this->content_type = isset( $this->post_body[0] ) && '{' === $this->post_body[0] ? 'application/json' : 'application/x-www-form-urlencoded';
$this->content_type = null;
$this->_server_https = array_key_exists( 'HTTPS', $_SERVER ) ? filter_var( wp_unslash( $_SERVER['HTTPS'] ) ) : '--UNset--';
* @return null|WP_Error (although this implementation always returns null)
public function initialize() {
$this->token_details['blog_id'] = Jetpack_Options::get_option( 'id' );
* Checks if the current request is authorized with a blog token.
* This method is overridden by a child class in WPCOM.
* @param boolean|int $site_id The site id.
public function is_jetpack_authorized_for_site( $site_id = false ) {
if ( ! $this->token_details ) {
$token_details = (object) $this->token_details;
$site_in_token = (int) $token_details->blog_id;
if ( $site_in_token < 1 ) {
if ( $site_id && $site_in_token !== (int) $site_id ) {
if ( (int) get_current_user_id() !== 0 ) {
// If Jetpack blog token is used, no logged-in user should exist.
* Checks if the current request is authorized with an upload token.
* This method is overridden by a child class in WPCOM.
public function is_authorized_with_upload_token() {
* @param bool $exit Whether to exit.
* @return string|null Content type (assuming it didn't exit), or null in certain error cases.
public function serve( $exit = true ) {
ini_set( 'display_errors', false ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Blacklisted
$this->exit = (bool) $exit;
// This was causing problems with Jetpack, but is necessary for wpcom
// @see https://github.com/Automattic/jetpack/pull/2603
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
add_filter( 'home_url', array( $this, 'ensure_http_scheme_of_home_url' ), 10, 3 );
add_filter( 'user_can_richedit', '__return_true' );
add_filter( 'comment_edit_pre', array( $this, 'comment_edit_pre' ) );
$initialization = $this->initialize();
if ( 'OPTIONS' === $this->method ) {
* Fires before the page output.
* Can be used to specify custom header options.
do_action( 'wpcom_json_api_options' );
return $this->output( 200, '', 'text/plain' );
if ( is_wp_error( $initialization ) ) {
$this->output_error( $initialization );
// Normalize path and extract API version.
$this->path = untrailingslashit( $this->path );
if ( preg_match( '#^/rest/v(\d+(\.\d+)*)#', $this->path, $matches ) ) {
$this->path = substr( $this->path, strlen( $matches[0] ) );
$this->version = $matches[1];
$allowed_methods = array( 'GET', 'POST' );
$is_help = preg_match( '#/help/?$#i', $this->path );
$matching_endpoints = array();
$origin = get_http_origin();
if ( ! empty( $origin ) && 'GET' === $this->method ) {
header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
$this->path = substr( rtrim( $this->path, '/' ), 0, -5 );
// Show help for all matching endpoints regardless of method.
$methods = $allowed_methods;
$find_all_matching_endpoints = true;
// How deep to truncate each endpoint's path to see if it matches this help request.
$depth = substr_count( $this->path, '/' ) + 1;
if ( false !== stripos( $this->accept, 'javascript' ) || false !== stripos( $this->accept, 'json' ) ) {
$help_content_type = 'json';
$help_content_type = 'html';
} elseif ( in_array( $this->method, $allowed_methods, true ) ) {
// Only serve requested method.
$methods = array( $this->method );
$find_all_matching_endpoints = false;
// We don't allow this requested method - find matching endpoints and send 405.
$methods = $allowed_methods;
$find_all_matching_endpoints = true;
// Find which endpoint to serve.
foreach ( $this->endpoints as $endpoint_path_versions => $endpoints_by_method ) {
// @todo Determine if anything depends on this being serialized rather than e.g. JSON.
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, possibly depended on elsewhere.
$endpoint_path_versions = unserialize( $endpoint_path_versions );
$endpoint_path = $endpoint_path_versions[0];
$endpoint_min_version = $endpoint_path_versions[1];
$endpoint_max_version = $endpoint_path_versions[2];
// Make sure max_version is not less than min_version.
if ( version_compare( $endpoint_max_version, $endpoint_min_version, '<' ) ) {
$endpoint_max_version = $endpoint_min_version;
foreach ( $methods as $method ) {
if ( ! isset( $endpoints_by_method[ $method ] ) ) {
$endpoint_path = untrailingslashit( $endpoint_path );