class.json-api-links.php
15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
/**
* WPCOM_JSON_API_Links class.
*
* @package automattic/jetpack
*/
require_once __DIR__ . '/../class.json-api.php';
/**
* Base class for WPCOM_JSON_API_Links.
*/
class WPCOM_JSON_API_Links {
/**
* An instance of the WPCOM_JSON_API.
*
* @var WPCOM_JSON_API
*/
private $api;
/**
* A WPCOM_JSON_API_Links instance.
*
* @var WPCOM_JSON_API_Links
*/
private static $instance;
/**
* An array of the closest supported version of an endpoint to the current endpoint.
*
* @var array
*/
private $closest_endpoint_cache_by_version = array();
/**
* An array including the current api endpoint as well as the max versions found if that endpoint doesn't exist.
*
* @var array
*/
private $matches_by_version = array();
/**
* An array including the cached endpoint path versions.
*
* @var array
*/
private $cache_result = null;
/**
* 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
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* WPCOM_JSON_API_Links constructor.
*
* Method protected for singleton.
*/
protected function __construct() {
$this->api = WPCOM_JSON_API::init();
}
/**
* An empty, private __clone method to prohibit cloning of this instance.
*/
private function __clone() { }
/**
* Overriding PHP's default __wakeup method to prvent unserializing of the instance, and return an error message.
*/
public function __wakeup() {
die( "Please don't __wakeup WPCOM_JSON_API_Links" );
}
/**
* Generate a URL to an endpoint
*
* Used to construct meta links in API responses
*
* @param mixed ...$args Optional arguments to be appended to URL.
* @return string Endpoint URL
**/
public function get_link( ...$args ) {
$format = array_shift( $args );
$base = WPCOM_JSON_API__BASE;
$path = array_pop( $args );
if ( $path ) {
$path = '/' . ltrim( $path, '/' );
// tack the path onto the end of the format string.
// have to escape %'s in the path as %% because
// we're about to pass it through sprintf and we don't
// want it to see the % as a placeholder.
$format .= str_replace( '%', '%%', $path );
}
// Escape any % in args before using sprintf.
$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, '.' ) ) {
// Generic version. Match the requested version as best we can.
$api_version = $this->get_closest_version_of_endpoint( $format, $relative_path );
$base = substr( $base, 0, - 1 ) . $api_version;
}
// escape any % in the relative path before running it through sprintf again.
$relative_path = str_replace( '%', '%%', $relative_path );
// http, WPCOM_JSON_API__BASE, ... , path.
// %s , %s , $format, %s.
return esc_url_raw( sprintf( "https://%s$relative_path", $base ) );
}
/**
* 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 = '' ) {
return $this->get_link( '/me', $path );
}
/**
* 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 = '' ) {
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 );
}
}
/**
* 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 = '' ) {
return $this->get_link( '/sites/%d/media/%d', $blog_id, $media_id, $path );
}
/**
* 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 = '' ) {
return $this->get_link( '/sites/%d', $blog_id, $path );
}
/**
* 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 = '' ) {
return $this->get_link( '/sites/%d/posts/%d', $blog_id, $post_id, $path );
}
/**
* 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 = '' ) {
return $this->get_link( '/sites/%d/comments/%d', $blog_id, $comment_id, $path );
}
/**
* 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 = '' ) {
return $this->get_link( '.1/sites/%d/publicize-connections/%d', $blog_id, $publicize_connection_id, $path );
}
/**
* 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 = '' ) {
return $this->get_link( '.1/me/publicize-connections/?keyring_connection_ID=%d', $keyring_token_id, $path );
}
/**
* 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 = '' ) {
return $this->get_link( '.1/me/keyring-connections/%d', $keyring_token_id, $path );
}
/**
* 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 = '' ) {
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.
*
* @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 .
* @return string The current version, or otherwise the maximum version available
*/
public function get_closest_version_of_endpoint( $template_path, $path, $request_method = 'GET' ) {
$closest_endpoint_cache_by_version = & $this->closest_endpoint_cache_by_version;
$closest_endpoint_cache = & $closest_endpoint_cache_by_version[ $this->api->version ];
if ( ! $closest_endpoint_cache ) {
$closest_endpoint_cache_by_version[ $this->api->version ] = array();
$closest_endpoint_cache = & $closest_endpoint_cache_by_version[ $this->api->version ];
}
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;
// try to match out of saved matches.
if ( ! isset( $matches_by_version[ $this->api->version ] ) ) {
$matches_by_version[ $this->api->version ] = array();
}
foreach ( $matches_by_version[ $this->api->version ] as $match ) {
$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();
$last_path_segment = $this->get_last_segment_of_relative_path( $path );
$max_version_found = null;
foreach ( $endpoint_path_versions as $endpoint_last_path_segment => $endpoints ) {
// Does the last part of the path match the path key? (e.g. 'posts')
// If the last part contains a placeholder (e.g. %s), we want to carry on.
// phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
if ( $last_path_segment != $endpoint_last_path_segment && ! strstr( $endpoint_last_path_segment, '%' ) ) {
continue;
}
foreach ( $endpoints as $endpoint ) {
// Does the request method match?
if ( ! in_array( $request_method, $endpoint['request_methods'], true ) ) {
continue;
}
$endpoint_path = untrailingslashit( $endpoint['path'] );
$endpoint_path_regex = str_replace( array( '%s', '%d' ), array( '([^/?&]+)', '(\d+)' ), $endpoint_path );
if ( ! preg_match( "#^$endpoint_path_regex\$#", $path ) ) {
continue;
}
// Make sure the endpoint exists at the same version.
if ( null !== $this->api->version &&
version_compare( $this->api->version, $endpoint['min_version'], '>=' ) &&
version_compare( $this->api->version, $endpoint['max_version'], '<=' )
) {
array_push(
$matches_by_version[ $this->api->version ],
(object) array(
'version' => $this->api->version,
'regex' => $endpoint_path_regex,
)
);
$closest_endpoint_cache[ $template_path ][ $request_method ] = $this->api->version;
return $this->api->version;
}
// If the endpoint doesn't exist at the same version, record the max version we found.
if ( empty( $max_version_found ) || version_compare( $max_version_found['version'], $endpoint['max_version'], '<' ) ) {
$max_version_found = array(
'version' => $endpoint['max_version'],
'regex' => $endpoint_path_regex,
);
}
}
}
// If the endpoint version is less than the requested endpoint version, return the max version found.
if ( ! empty( $max_version_found ) ) {
array_push(
$matches_by_version[ $this->api->version ],
(object) $max_version_found
);
$closest_endpoint_cache[ $template_path ][ $request_method ] = $max_version_found['version'];
return $max_version_found['version'];
}
// Otherwise, use the API version of the current request.
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() {
if ( ! empty( $this->cache_result ) ) {
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 ) {
// @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.
// Grab the last component of the relative path to use as the top-level key.
$last_path_segment = $this->get_last_segment_of_relative_path( $path );
$endpoint_path_versions[ $last_path_segment ][] = array(
'path' => $path,
'min_version' => $min_version,
'max_version' => $max_version,
'request_methods' => array_keys( $endpoint_objects ),
);
}
$this->cache_result = $endpoint_path_versions;
return $endpoint_path_versions;
}
/**
* Grab the last segment of a relative path
*
* @param string $path Path.
* @return string Last path segment
*/
protected function get_last_segment_of_relative_path( $path ) {
$path_parts = array_filter( explode( '/', $path ) );
if ( empty( $path_parts ) ) {
return null;
}
return end( $path_parts );
}
}