2022-06-16 14:01:47 +02:00
< ? php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* WPCOM_JSON_API_Links class .
*
* @ package automattic / jetpack
*/
2025-08-27 08:44:30 +02:00
if ( ! defined ( 'ABSPATH' ) ) {
exit ( 0 );
}
2022-06-16 14:01:47 +02:00
require_once __DIR__ . '/../class.json-api.php' ;
/**
* Base class for WPCOM_JSON_API_Links .
*/
2019-11-15 23:26:29 +01:00
class WPCOM_JSON_API_Links {
2022-06-16 14:01:47 +02:00
/**
* An instance of the WPCOM_JSON_API .
*
* @ var WPCOM_JSON_API
*/
2019-11-15 23:26:29 +01:00
private $api ;
2022-06-16 14:01:47 +02:00
/**
* A WPCOM_JSON_API_Links instance .
*
* @ var WPCOM_JSON_API_Links
*/
2019-11-15 23:26:29 +01:00
private static $instance ;
2022-06-16 14:01:47 +02:00
/**
* An array of the closest supported version of an endpoint to the current endpoint .
*
* @ var array
*/
2019-11-15 23:26:29 +01:00
private $closest_endpoint_cache_by_version = array ();
2022-06-16 14:01:47 +02:00
/**
* An array including the current api endpoint as well as the max versions found if that endpoint doesn ' t exist .
*
* @ var array
*/
2019-11-15 23:26:29 +01:00
private $matches_by_version = array ();
2022-06-16 14:01:47 +02:00
/**
* An array including the cached endpoint path versions .
*
* @ var array
*/
2019-11-15 23:26:29 +01:00
private $cache_result = null ;
2022-06-16 14:01:47 +02:00
/**
* Creates a new instance of the WPCOM_JSON_API_Links class .
*
* @ return WPCOM_JSON_API_Links
*/
public static function getInstance () { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid
2019-11-15 23:26:29 +01:00
if ( null === self :: $instance ) {
self :: $instance = new self ();
}
return self :: $instance ;
}
2022-06-16 14:01:47 +02:00
/**
* WPCOM_JSON_API_Links constructor .
*
* Method protected for singleton .
*/
2019-11-15 23:26:29 +01:00
protected function __construct () {
$this -> api = WPCOM_JSON_API :: init ();
}
2022-06-16 14:01:47 +02:00
/**
* An empty , private __clone method to prohibit cloning of this instance .
*/
2019-11-15 23:26:29 +01:00
private function __clone () { }
2022-06-16 14:01:47 +02:00
/**
* Overriding PHP ' s default __wakeup method to prvent unserializing of the instance , and return an error message .
2024-04-17 11:32:24 +02:00
*
* @ return never
2022-06-16 14:01:47 +02:00
*/
2020-10-20 18:05:12 +02:00
public function __wakeup () {
die ( " Please don't __wakeup WPCOM_JSON_API_Links " );
}
2019-11-15 23:26:29 +01:00
/**
* Generate a URL to an endpoint
*
* Used to construct meta links in API responses
*
2022-06-16 14:01:47 +02:00
* @ param mixed ... $args Optional arguments to be appended to URL .
2019-11-15 23:26:29 +01:00
* @ return string Endpoint URL
**/
2022-06-16 14:01:47 +02:00
public function get_link ( ... $args ) {
2019-11-15 23:26:29 +01:00
$format = array_shift ( $args );
2022-06-16 14:01:47 +02:00
$base = WPCOM_JSON_API__BASE ;
2019-11-15 23:26:29 +01:00
$path = array_pop ( $args );
if ( $path ) {
$path = '/' . ltrim ( $path , '/' );
2022-06-16 14:01:47 +02:00
// tack the path onto the end of the format string.
2019-11-15 23:26:29 +01:00
// have to escape %'s in the path as %% because
// we're about to pass it through sprintf and we don't
2022-06-16 14:01:47 +02:00
// want it to see the % as a placeholder.
2019-11-15 23:26:29 +01:00
$format .= str_replace ( '%' , '%%' , $path );
}
2022-06-16 14:01:47 +02:00
// Escape any % in args before using sprintf.
2019-11-15 23:26:29 +01:00
$escaped_args = array ();
foreach ( $args as $arg_key => $arg_value ) {
$escaped_args [ $arg_key ] = str_replace ( '%' , '%%' , $arg_value );
}
$relative_path = vsprintf ( $format , $escaped_args );
if ( ! wp_startswith ( $relative_path , '.' ) ) {
2022-06-16 14:01:47 +02:00
// Generic version. Match the requested version as best we can.
2019-11-15 23:26:29 +01:00
$api_version = $this -> get_closest_version_of_endpoint ( $format , $relative_path );
$base = substr ( $base , 0 , - 1 ) . $api_version ;
}
2022-06-16 14:01:47 +02:00
// escape any % in the relative path before running it through sprintf again.
2019-11-15 23:26:29 +01:00
$relative_path = str_replace ( '%' , '%%' , $relative_path );
2022-06-16 14:01:47 +02:00
// http, WPCOM_JSON_API__BASE, ... , path.
// %s , %s , $format, %s.
2019-11-15 23:26:29 +01:00
return esc_url_raw ( sprintf ( " https://%s $relative_path " , $base ) );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the / me prefixed endpoint URL
*
* Used to construct meta links in API responses , specific to WordPress . com user account pages .
*
* @ param string $path Optional path to be appended to the URL .
* @ return string / me endpoint URL
**/
public function get_me_link ( $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '/me' , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the endpoint URL for taxonomies
*
* Used to construct meta links in API responses , specific to taxonomies .
*
* @ param int $blog_id The site ' s Jetpack blog ID .
* @ param int $taxonomy_id The taxonomy ID ( for example of the category , tag ) .
* @ param string $taxonomy_type The taxonomy type ( for example category , tag ) .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including taxonomy information .
**/
public function get_taxonomy_link ( $blog_id , $taxonomy_id , $taxonomy_type , $path = '' ) {
2019-11-15 23:26:29 +01:00
switch ( $taxonomy_type ) {
case 'category' :
return $this -> get_link ( '/sites/%d/categories/slug:%s' , $blog_id , $taxonomy_id , $path );
case 'post_tag' :
return $this -> get_link ( '/sites/%d/tags/slug:%s' , $blog_id , $taxonomy_id , $path );
default :
return $this -> get_link ( '/sites/%d/taxonomies/%s/terms/slug:%s' , $blog_id , $taxonomy_type , $taxonomy_id , $path );
}
}
2022-06-16 14:01:47 +02:00
/**
* Generate the endpoint URL for media links
*
* Used to construct meta links in API responses , specific to media links .
*
* @ param int $blog_id The site ' s Jetpack blog ID .
* @ param int $media_id The media item ID .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including media information .
**/
public function get_media_link ( $blog_id , $media_id , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '/sites/%d/media/%d' , $blog_id , $media_id , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the site link endpoint URL
*
* Used to construct meta links in API responses , specific to / site links .
*
* @ param int $blog_id The site ' s Jetpack blog ID .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including site information .
**/
public function get_site_link ( $blog_id , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '/sites/%d' , $blog_id , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the posts endpoint URL
*
* Used to construct meta links in API responses , specific to posts links .
*
* @ param int $blog_id The site ' s Jetpack blog ID .
* @ param int $post_id The post ID .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including post information .
**/
public function get_post_link ( $blog_id , $post_id , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '/sites/%d/posts/%d' , $blog_id , $post_id , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the comments endpoint URL
*
* Used to construct meta links in API responses , specific to comments links .
*
* @ param int $blog_id The site ' s Jetpack blog ID .
* @ param int $comment_id The comment ID .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including comment information .
**/
public function get_comment_link ( $blog_id , $comment_id , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '/sites/%d/comments/%d' , $blog_id , $comment_id , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the endpoint URL for Publicize connections
*
* Used to construct meta links in API responses , specific to Publicize connections .
*
* @ param int $blog_id The site ' s Jetpack blog ID .
* @ param int $publicize_connection_id The ID of the Publicize connection .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including Publicize connection information .
**/
public function get_publicize_connection_link ( $blog_id , $publicize_connection_id , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '.1/sites/%d/publicize-connections/%d' , $blog_id , $publicize_connection_id , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the endpoint URL for a single Publicize connection including a Keyring connection
*
* Used to construct meta links in API responses , specific to a single Publicize and Keyring connection .
*
* @ param int $keyring_token_id The ID of the Keyring connection .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including specific Keyring connection information for a specific Publicize connection .
**/
public function get_publicize_connections_link ( $keyring_token_id , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '.1/me/publicize-connections/?keyring_connection_ID=%d' , $keyring_token_id , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the endpoint URL for a single Keyring connection
*
* Used to construct meta links in API responses , specific to a Keyring connections .
*
* @ param int $keyring_token_id The ID of the Keyring connection .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including specific Keyring connection .
**/
public function get_keyring_connection_link ( $keyring_token_id , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '.1/me/keyring-connections/%d' , $keyring_token_id , $path );
}
2022-06-16 14:01:47 +02:00
/**
* Generate the endpoint URL for an external service that can be integrated with via Keyring
*
* Used to construct meta links in API responses , specific to an external service .
*
* @ param int $external_service The ID of the external service .
* @ param string $path Optional path to be appended to the URL .
* @ return string Endpoint URL including information about an external service that WordPress . com or Jetpack sites can integrate with via keyring .
**/
public function get_external_service_link ( $external_service , $path = '' ) {
2019-11-15 23:26:29 +01:00
return $this -> get_link ( '.1/meta/external-services/%s' , $external_service , $path );
}
/**
* Try to find the closest supported version of an endpoint to the current endpoint
*
* For example , if we were looking at the path / animals / panda :
* - if the current endpoint is v1 . 3 and there is a v1 . 3 of / animals /% s available , we return 1.3
* - if the current endpoint is v1 . 3 and there is no v1 . 3 of / animals /% s known , we fall back to the
* maximum available version of / animals /% s , e . g . 1.1
*
* This method is used in get_link () to construct meta links for API responses .
*
2022-06-16 14:01:47 +02:00
* @ param string $template_path The generic endpoint path , e . g . / sites /% s .
* @ param string $path The current endpoint path , relative to the version , e . g . / sites / 12345 .
* @ param string $request_method Request method used to access the endpoint path .
2019-11-15 23:26:29 +01:00
* @ return string The current version , or otherwise the maximum version available
*/
2022-06-16 14:01:47 +02:00
public function get_closest_version_of_endpoint ( $template_path , $path , $request_method = 'GET' ) {
2019-11-15 23:26:29 +01:00
$closest_endpoint_cache_by_version = & $this -> closest_endpoint_cache_by_version ;
2025-12-12 13:13:07 +01:00
$api_version = $this -> api -> version ? ? '' ;
$closest_endpoint_cache = & $closest_endpoint_cache_by_version [ $api_version ];
2022-06-16 14:01:47 +02:00
if ( ! $closest_endpoint_cache ) {
2025-12-12 13:13:07 +01:00
$closest_endpoint_cache_by_version [ $api_version ] = array ();
$closest_endpoint_cache = & $closest_endpoint_cache_by_version [ $api_version ];
2019-11-15 23:26:29 +01:00
}
if ( ! isset ( $closest_endpoint_cache [ $template_path ] ) ) {
$closest_endpoint_cache [ $template_path ] = array ();
} elseif ( isset ( $closest_endpoint_cache [ $template_path ][ $request_method ] ) ) {
return $closest_endpoint_cache [ $template_path ][ $request_method ];
}
$path = untrailingslashit ( $path );
// /help is a special case - always use the current request version
if ( wp_endswith ( $path , '/help' ) ) {
$closest_endpoint_cache [ $template_path ][ $request_method ] = $this -> api -> version ;
return $this -> api -> version ;
}
$matches_by_version = & $this -> matches_by_version ;
2022-06-16 14:01:47 +02:00
// try to match out of saved matches.
2025-12-12 13:13:07 +01:00
if ( ! isset ( $matches_by_version [ $api_version ] ) ) {
$matches_by_version [ $api_version ] = array ();
2019-11-15 23:26:29 +01:00
}
2025-12-12 13:13:07 +01:00
foreach ( $matches_by_version [ $api_version ] as $match ) {
2019-11-15 23:26:29 +01:00
$regex = $match -> regex ;
if ( preg_match ( " #^ $regex\ $ # " , $path ) ) {
$closest_endpoint_cache [ $template_path ][ $request_method ] = $match -> version ;
return $match -> version ;
}
}
$endpoint_path_versions = $this -> get_endpoint_path_versions ();
2022-06-16 14:01:47 +02:00
$last_path_segment = $this -> get_last_segment_of_relative_path ( $path );
$max_version_found = null ;
2019-11-15 23:26:29 +01:00
foreach ( $endpoint_path_versions as $endpoint_last_path_segment => $endpoints ) {
// Does the last part of the path match the path key? (e.g. 'posts')
2022-06-16 14:01:47 +02:00
// If the last part contains a placeholder (e.g. %s), we want to carry on.
// phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
2019-11-15 23:26:29 +01:00
if ( $last_path_segment != $endpoint_last_path_segment && ! strstr ( $endpoint_last_path_segment , '%' ) ) {
continue ;
}
foreach ( $endpoints as $endpoint ) {
// Does the request method match?
2022-06-16 14:01:47 +02:00
if ( ! in_array ( $request_method , $endpoint [ 'request_methods' ], true ) ) {
2019-11-15 23:26:29 +01:00
continue ;
}
2025-06-09 09:58:01 +02:00
$endpoint_path = untrailingslashit ( ( string ) $endpoint [ 'path' ] );
2019-11-15 23:26:29 +01:00
$endpoint_path_regex = str_replace ( array ( '%s' , '%d' ), array ( '([^/?&]+)' , '(\d+)' ), $endpoint_path );
if ( ! preg_match ( " #^ $endpoint_path_regex\ $ # " , $path ) ) {
continue ;
}
2022-06-16 14:01:47 +02:00
// Make sure the endpoint exists at the same version.
2022-04-02 10:26:41 +02:00
if ( null !== $this -> api -> version &&
version_compare ( $this -> api -> version , $endpoint [ 'min_version' ], '>=' ) &&
version_compare ( $this -> api -> version , $endpoint [ 'max_version' ], '<=' )
) {
2019-11-15 23:26:29 +01:00
array_push (
$matches_by_version [ $this -> api -> version ],
2022-06-16 14:01:47 +02:00
( object ) array (
'version' => $this -> api -> version ,
'regex' => $endpoint_path_regex ,
)
2019-11-15 23:26:29 +01:00
);
$closest_endpoint_cache [ $template_path ][ $request_method ] = $this -> api -> version ;
return $this -> api -> version ;
}
2022-06-16 14:01:47 +02:00
// If the endpoint doesn't exist at the same version, record the max version we found.
2019-11-15 23:26:29 +01:00
if ( empty ( $max_version_found ) || version_compare ( $max_version_found [ 'version' ], $endpoint [ 'max_version' ], '<' ) ) {
2022-06-16 14:01:47 +02:00
$max_version_found = array (
'version' => $endpoint [ 'max_version' ],
'regex' => $endpoint_path_regex ,
);
2019-11-15 23:26:29 +01:00
}
}
}
2022-06-16 14:01:47 +02:00
// If the endpoint version is less than the requested endpoint version, return the max version found.
2019-11-15 23:26:29 +01:00
if ( ! empty ( $max_version_found ) ) {
array_push (
2025-12-12 13:13:07 +01:00
$matches_by_version [ $api_version ],
2019-11-15 23:26:29 +01:00
( object ) $max_version_found
);
$closest_endpoint_cache [ $template_path ][ $request_method ] = $max_version_found [ 'version' ];
return $max_version_found [ 'version' ];
}
2022-06-16 14:01:47 +02:00
// Otherwise, use the API version of the current request.
2019-11-15 23:26:29 +01:00
return $this -> api -> version ;
}
/**
* Get an array of endpoint paths with their associated versions
*
* @ return array Array of endpoint paths , min_versions and max_versions , keyed by last segment of path
**/
protected function get_endpoint_path_versions () {
2022-06-16 14:01:47 +02:00
if ( ! empty ( $this -> cache_result ) ) {
2019-11-15 23:26:29 +01:00
return $this -> cache_result ;
}
/*
* Create a map of endpoints and their min / max versions keyed by the last segment of the path ( e . g . 'posts' )
* This reduces the search space when finding endpoint matches in get_closest_version_of_endpoint ()
*/
$endpoint_path_versions = array ();
foreach ( $this -> api -> endpoints as $key => $endpoint_objects ) {
2022-06-16 14:01:47 +02:00
// @todo As with the todo in class.json-api.php, we need to determine if anything depends on this being serialized and hence unserialized, rather than e.g. JSON.
// The key contains a serialized path, min_version and max_version.
list ( $path , $min_version , $max_version ) = unserialize ( $key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Legacy, see serialization at class.json-api.php.
2019-11-15 23:26:29 +01:00
2022-06-16 14:01:47 +02:00
// Grab the last component of the relative path to use as the top-level key.
2025-12-12 13:13:07 +01:00
$last_path_segment = $this -> get_last_segment_of_relative_path ( $path ) ? ? '' ;
2019-11-15 23:26:29 +01:00
$endpoint_path_versions [ $last_path_segment ][] = array (
2022-06-16 14:01:47 +02:00
'path' => $path ,
'min_version' => $min_version ,
'max_version' => $max_version ,
'request_methods' => array_keys ( $endpoint_objects ),
2019-11-15 23:26:29 +01:00
);
}
$this -> cache_result = $endpoint_path_versions ;
return $endpoint_path_versions ;
}
/**
* Grab the last segment of a relative path
*
2022-06-16 14:01:47 +02:00
* @ param string $path Path .
2019-11-15 23:26:29 +01:00
* @ return string Last path segment
*/
2022-06-16 14:01:47 +02:00
protected function get_last_segment_of_relative_path ( $path ) {
2019-11-15 23:26:29 +01:00
$path_parts = array_filter ( explode ( '/' , $path ) );
if ( empty ( $path_parts ) ) {
return null ;
}
return end ( $path_parts );
}
}