PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3-a.1 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 All 503 releases
← All changes | class.json-api-endpoints.php +515 -142 12.7.316.2 View file →
@@ -5,9 +5,13 @@
5 5 * @package automattic/jetpack
6 6 */
7 7
8 8 use Automattic\Jetpack\Connection\Client;
9 +use Automattic\Jetpack\Connection\Manager;
10 +use Automattic\Jetpack\Connection\Rest_Authentication;
11 +use Automattic\Jetpack\Connection\Tokens;
9 12 use Automattic\Jetpack\Status;
13 +use Automattic\Jetpack\Status\Host;
10 14
11 15 require_once __DIR__ . '/json-api-config.php';
12 16 require_once __DIR__ . '/sal/class.json-api-links.php';
13 17 require_once __DIR__ . '/sal/class.json-api-metadata.php';
@@ -122,8 +126,22 @@
122 126 */
123 127 public $path_labels = array();
124 128
125 129 /**
130 + * The REST endpoint if available.
131 + *
132 + * @var string
133 + */
134 + public $rest_route;
135 +
136 + /**
137 + * Jetpack Version in which REST support was introduced.
138 + *
139 + * @var string
140 + */
141 + public $rest_min_jp_version;
142 +
143 + /**
126 144 * Accepted query parameters
127 145 *
128 146 * @var array
129 147 */
@@ -254,8 +272,15 @@
254 272 */
255 273 public $allow_jetpack_site_auth = false;
256 274
257 275 /**
276 + * Set to true if the endpoint should accept user based authentication.
277 + *
278 + * @var bool
279 + */
280 + public $allow_jetpack_token_auth = false;
281 +
282 + /**
258 283 * Set to true if the endpoint should accept auth from an upload token.
259 284 *
260 285 * @var bool
261 286 */
@@ -275,8 +300,34 @@
275 300 */
276 301 public $allow_fallback_to_jetpack_blog_token = false;
277 302
278 303 /**
304 + * REST namespace.
305 + */
306 + const REST_NAMESPACE = 'jetpack/rest';
307 +
308 + /**
309 + * Post object format.
310 + *
311 + * @var array
312 + */
313 + public $post_object_format;
314 +
315 + /**
316 + * Comment object format.
317 + *
318 + * @var array
319 + */
320 + public $comment_object_format;
321 +
322 + /**
323 + * Dropdown page object format.
324 + *
325 + * @var array
326 + */
327 + public $dropdown_page_object_format;
328 +
329 + /**
279 330 * Constructor.
280 331 *
281 332 * @param string|array|object $args Args.
282 333 */
@@ -287,8 +338,9 @@
287 338 'allowed_if_red_flagged' => false,
288 339 'allowed_if_deleted' => false,
289 340 'description' => '',
290 341 'group' => '',
342 + 'stat' => '',
291 343 'method' => 'GET',
292 344 'path' => '/',
293 345 'min_version' => '0',
294 346 'max_version' => WPCOM_JSON_API__CURRENT_VERSION,
@@ -296,8 +348,10 @@
296 348 'deprecated' => false,
297 349 'new_version' => WPCOM_JSON_API__CURRENT_VERSION,
298 350 'jp_disabled' => false,
299 351 'path_labels' => array(),
352 + 'rest_route' => null,
353 + 'rest_min_jp_version' => null,
300 354 'request_format' => array(),
301 355 'response_format' => array(),
302 356 'query_parameters' => array(),
303 357 'version' => 'v1',
@@ -309,8 +363,9 @@
309 363 'custom_fields_filtering' => false,
310 364 'allow_cross_origin_request' => false,
311 365 'allow_unauthorized_request' => false,
312 366 'allow_jetpack_site_auth' => false,
367 + 'allow_jetpack_token_auth' => false,
313 368 'allow_upload_token_auth' => false,
314 369 'allow_fallback_to_jetpack_blog_token' => false,
315 370 );
316 371
@@ -335,8 +390,11 @@
335 390 $this->max_version = $args['max_version'];
336 391 $this->deprecated = $args['deprecated'];
337 392 $this->new_version = $args['new_version'];
338 393
394 + $this->rest_route = $args['rest_route'];
395 + $this->rest_min_jp_version = $args['rest_min_jp_version'];
396 +
339 397 // Ensure max version is not less than min version.
340 398 if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
341 399 $this->max_version = $this->min_version;
342 400 }
@@ -346,8 +404,9 @@
346 404
347 405 $this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
348 406 $this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
349 407 $this->allow_jetpack_site_auth = (bool) $args['allow_jetpack_site_auth'];
408 + $this->allow_jetpack_token_auth = (bool) $args['allow_jetpack_token_auth'];
350 409 $this->allow_upload_token_auth = (bool) $args['allow_upload_token_auth'];
351 410 $this->allow_fallback_to_jetpack_blog_token = (bool) $args['allow_fallback_to_jetpack_blog_token'];
352 411 $this->require_rewind_auth = isset( $args['require_rewind_auth'] ) ? (bool) $args['require_rewind_auth'] : false;
353 412
@@ -383,8 +442,12 @@
383 442 $this->example_request_data = $args['example_request_data'];
384 443 $this->example_response = $args['example_response'];
385 444
386 445 $this->api->add( $this );
446 +
447 + if ( ( ! defined( 'IS_WPCOM' ) || ! IS_WPCOM ) && $this->rest_route && ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) ) {
448 + $this->create_rest_route_for_endpoint();
449 + }
387 450 }
388 451
389 452 /**
390 453 * Get all query args. Prefill with defaults.
@@ -582,9 +645,9 @@
582 645 case 'false':
583 646 $return[ $key ] = false;
584 647 break;
585 648 case 'url':
586 - if ( is_object( $value ) && isset( $value->url ) && false !== strpos( $value->url, 'https://videos.files.wordpress.com/' ) ) {
649 + if ( is_object( $value ) && isset( $value->url ) && str_contains( $value->url, 'https://videos.files.wordpress.com/' ) ) {
587 650 $value = $value->url;
588 651 }
589 652 // Check for string since esc_url_raw() expects one.
590 653 if ( ! is_string( $value ) ) {
@@ -606,8 +669,12 @@
606 669 if ( ! empty( $types[0] ) && 'false' === $types[0]['type'] ) {
607 670 $next_type = array_shift( $types );
608 671 return $this->cast_and_filter_item( $return, $next_type, $key, $value, $types, $for_output );
609 672 }
673 + if ( is_array( $value ) ) {
674 + // Give up rather than setting the value to the string 'Array'.
675 + break;
676 + }
610 677 }
611 678 $return[ $key ] = (string) $value;
612 679 break;
613 680 case 'html':
@@ -763,8 +830,10 @@
763 830 'profile_URL' => '(URL)',
764 831 'is_super_admin' => '(bool)',
765 832 'roles' => '(array:string)',
766 833 'ip_address' => '(string|false)',
834 + 'wpcom_id' => '(int|null)',
835 + 'wpcom_login' => '(string|null)',
767 836 );
768 837 $return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
769 838 break;
770 839 case 'role':
@@ -994,9 +1063,9 @@
994 1063 ':' => 'children',
995 1064 '>' => 'subtype',
996 1065 '=' => 'default',
997 1066 ) as $operator => $meaning ) {
998 - if ( false !== strpos( $type, $operator ) ) {
1067 + if ( str_contains( $type, $operator ) ) {
999 1068 $item = explode( $operator, $type, 2 );
1000 1069 $return[] = array(
1001 1070 'type' => $item[0],
1002 1071 $meaning => $item[1],
@@ -1026,9 +1095,9 @@
1026 1095 * @param bool $show_description Whether to show the description.
1027 1096 */
1028 1097 public function document( $show_description = true ) {
1029 1098 global $wpdb;
1030 - $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
1099 + $original_post = $GLOBALS['post'] ?? 'unset';
1031 1100 unset( $GLOBALS['post'] );
1032 1101
1033 1102 $doc = $this->generate_documentation();
1034 1103
@@ -1078,9 +1147,9 @@
1078 1147 <th scope="row" class="parameter api-index-item-title">Requires authentication?</th>
1079 1148 <?php
1080 1149 $requires_auth = $wpdb->get_row( $wpdb->prepare( 'SELECT requires_authentication FROM rest_api_documentation WHERE `version` = %s AND `path` = %s AND `method` = %s LIMIT 1', $version, untrailingslashit( $doc['path_labeled'] ), $doc['method'] ) );
1081 1150 ?>
1082 - <td class="type api-index-item-title"><?php echo ( true === (bool) $requires_auth->requires_authentication ? 'Yes' : 'No' ); ?></td>
1151 + <td class="type api-index-item-title"><?php echo ( ! empty( $requires_auth->requires_authentication ) ? 'Yes' : 'No' ); ?></td>
1083 1152 </tr>
1084 1153
1085 1154 </tbody>
1086 1155 </table>
@@ -1254,11 +1323,13 @@
1254 1323 $default = "'$default'";
1255 1324 }
1256 1325 }
1257 1326 }
1258 - $type = '(' . implode( '|', $type ) . ')';
1259 - list( , $description ) = explode( ')', $description, 2 );
1260 - $description = trim( $description );
1327 + $type = '(' . implode( '|', $type ) . ')';
1328 + if ( str_contains( $description, ')' ) ) {
1329 + list( , $description ) = explode( ')', $description, 2 );
1330 + }
1331 + $description = trim( $description );
1261 1332 if ( $default ) {
1262 1333 $description .= " Default: $default.";
1263 1334 }
1264 1335 }
@@ -1289,20 +1360,20 @@
1289 1360 }
1290 1361
1291 1362 if ( 'inherit' === $post->post_status ) {
1292 1363 $parent_post = get_post( $post->post_parent );
1293 - $post_status_obj = get_post_status_object( $parent_post->post_status );
1364 + $post_status_obj = get_post_status_object( $parent_post->post_status ?? $post->post_status );
1294 1365 } else {
1295 1366 $post_status_obj = get_post_status_object( $post->post_status );
1296 1367 }
1297 1368
1298 - if ( ! $post_status_obj->public ) {
1369 + if ( empty( $post_status_obj->public ) ) {
1299 1370 if ( is_user_logged_in() ) {
1300 - if ( $post_status_obj->protected ) {
1371 + if ( ! empty( $post_status_obj->protected ) ) {
1301 1372 if ( ! current_user_can( 'edit_post', $post->ID ) ) {
1302 1373 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1303 1374 }
1304 - } elseif ( $post_status_obj->private ) {
1375 + } elseif ( ! empty( $post_status_obj->private ) ) {
1305 1376 if ( ! current_user_can( 'read_post', $post->ID ) ) {
1306 1377 return new WP_Error( 'unauthorized', 'User cannot view post', 403 );
1307 1378 }
1308 1379 } elseif ( in_array( $post->post_status, array( 'inherit', 'trash' ), true ) ) {
@@ -1377,95 +1448,121 @@
1377 1448 $first_name = null;
1378 1449 $last_name = null;
1379 1450 $nice = null;
1380 1451 $url = null;
1381 - $ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1452 + $ip_address = $author->comment_author_IP ?? '';
1453 + $site_id = -1;
1382 1454
1383 1455 if ( isset( $author->comment_author_email ) ) {
1384 - $id = ( isset( $author->user_id ) && $author->user_id ) ? $author->user_id : 0;
1385 - $login = '';
1386 - $email = $author->comment_author_email;
1387 - $name = $author->comment_author;
1388 - $first_name = '';
1389 - $last_name = '';
1390 - $url = $author->comment_author_url;
1391 - $avatar_url = $this->api->get_avatar_url( $author );
1392 - $profile_url = 'https://gravatar.com/' . md5( strtolower( trim( $email ) ) );
1393 - $nice = '';
1394 - $site_id = -1;
1456 + $id = empty( $author->user_id ) ? 0 : (int) $author->user_id;
1457 + $login = '';
1458 + $email = $author->comment_author_email;
1459 + $name = $author->comment_author;
1460 + $first_name = '';
1461 + $last_name = '';
1462 + $avatar_url = $this->api->get_avatar_url( $author );
1463 + $nice = '';
1464 + $url = $author->comment_author_url;
1465 + // Convert Gravatar URLs containing an email address to the hashed version.
1466 + if ( preg_match( '#^https?://(?:www\.)?gravatar\.com/([^/?]+)#i', $url, $matches ) && is_email( $matches[1] ) ) {
1467 + $url = 'https://gravatar.com/' . md5( strtolower( trim( $matches[1] ) ) );
1468 + }
1395 1469
1470 + // Add additional user data to the response if a valid user ID is available.
1471 + if ( 0 < $id ) {
1472 + $user = get_user_by( 'id', $id );
1473 + if ( $user instanceof WP_User ) {
1474 + $login = $user->user_login ?? '';
1475 + $first_name = $user->first_name ?? '';
1476 + $last_name = $user->last_name ?? '';
1477 + $nice = $user->user_nicename ?? '';
1478 + }
1479 + }
1480 +
1396 1481 // Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1397 1482 // "&" is the only email/URL character altered by wp_kses().
1398 1483 foreach ( array( 'email', 'url' ) as $field ) {
1399 1484 $$field = str_replace( '&amp;', '&', $$field );
1400 1485 }
1401 - } else {
1402 - if ( $author instanceof WP_User || isset( $author->user_email ) ) {
1403 - $author = $author->ID;
1404 - } elseif ( isset( $author->user_id ) && $author->user_id ) {
1405 - $author = $author->user_id;
1406 - } elseif ( isset( $author->post_author ) ) {
1407 - // then $author is a Post Object.
1408 - if ( ! $author->post_author ) {
1409 - return null;
1410 - }
1411 - /**
1412 - * Filter whether the current site is a Jetpack site.
1413 - *
1414 - * @module json-api
1415 - *
1416 - * @since 3.3.0
1417 - *
1418 - * @param bool false Is the current site a Jetpack site. Default to false.
1419 - * @param int get_current_blog_id() Blog ID.
1420 - */
1421 - $is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1422 - $post_id = $author->ID;
1423 - if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1424 - $id = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1425 - $email = get_post_meta( $post_id, '_jetpack_author_email', true );
1426 - $login = '';
1427 - $name = get_post_meta( $post_id, '_jetpack_author', true );
1428 - $first_name = '';
1429 - $last_name = '';
1430 - $url = '';
1431 - $nice = '';
1432 - } else {
1433 - $author = $author->post_author;
1434 - }
1486 + } elseif ( $author instanceof WP_User || isset( $author->user_email ) ) {
1487 + $author = $author->ID;
1488 + } elseif ( isset( $author->user_id ) && $author->user_id ) {
1489 + $author = $author->user_id;
1490 + } elseif ( isset( $author->post_author ) ) {
1491 + // then $author is a Post Object.
1492 + if ( ! $author->post_author ) {
1493 + return null;
1435 1494 }
1495 + /**
1496 + * Filter whether the current site is a Jetpack site.
1497 + *
1498 + * @module json-api
1499 + *
1500 + * @since 3.3.0
1501 + *
1502 + * @param bool false Is the current site a Jetpack site. Default to false.
1503 + * @param int get_current_blog_id() Blog ID.
1504 + */
1505 + $is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1506 + $post_id = $author->ID;
1507 + if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1508 + $id = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1509 + $email = get_post_meta( $post_id, '_jetpack_author_email', true );
1510 + $login = '';
1511 + $name = get_post_meta( $post_id, '_jetpack_author', true );
1512 + $first_name = '';
1513 + $last_name = '';
1514 + $url = '';
1515 + $nice = '';
1516 + } else {
1517 + $author = $author->post_author;
1518 + }
1519 + }
1436 1520
1437 - if ( ! isset( $id ) ) {
1438 - $user = get_user_by( 'id', $author );
1439 - if ( ! $user || is_wp_error( $user ) ) {
1440 - trigger_error( 'Unknown user', E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
1441 -
1442 - return null;
1443 - }
1444 - $id = $user->ID;
1445 - $email = $user->user_email;
1446 - $login = $user->user_login;
1447 - $name = $user->display_name;
1448 - $first_name = $user->first_name;
1449 - $last_name = $user->last_name;
1450 - $url = $user->user_url;
1451 - $nice = $user->user_nicename;
1521 + if ( ! isset( $id ) ) {
1522 + $user = get_user_by( 'id', $author );
1523 + if ( ! $user || is_wp_error( $user ) ) {
1524 + return null;
1452 1525 }
1453 - if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1526 + $id = $user->ID;
1527 + $email = $user->user_email;
1528 + $login = $user->user_login;
1529 + $name = $user->display_name;
1530 + $first_name = $user->first_name;
1531 + $last_name = $user->last_name;
1532 + $url = $user->user_url;
1533 + $nice = $user->user_nicename;
1534 + }
1535 + if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack && $id > 0 ) {
1536 + /**
1537 + * Allow customizing the blog ID returned with the author in WordPress.com REST API queries.
1538 + *
1539 + * @since 12.9
1540 + *
1541 + * @module json-api
1542 + *
1543 + * @param bool|int $active_blog Blog ID, or false by default.
1544 + * @param int $id User ID.
1545 + */
1546 + $active_blog = apply_filters( 'wpcom_api_pre_get_active_blog_author', false, $id );
1547 + if ( false === $active_blog ) {
1454 1548 $active_blog = get_active_blog_for_user( $id );
1455 - $site_id = $active_blog->blog_id;
1456 - if ( $site_id > -1 ) {
1457 - $site_visible = (
1458 - -1 !== (int) $active_blog->public ||
1459 - is_private_blog_user( $site_id, get_current_user_id() )
1460 - );
1461 - }
1462 - $profile_url = "https://gravatar.com/{$login}";
1463 - } else {
1464 - $profile_url = 'https://gravatar.com/' . md5( strtolower( trim( $email ) ) );
1465 - $site_id = -1;
1466 1549 }
1550 + if ( ! empty( $active_blog ) ) {
1551 + $site_id = $active_blog->blog_id;
1552 + }
1553 + if ( $site_id > - 1 ) {
1554 + $site_visible = (
1555 + - 1 !== (int) $active_blog->public ||
1556 + is_private_blog_user( $site_id, get_current_user_id() )
1557 + );
1558 + }
1559 + $profile_url = "https://gravatar.com/{$login}";
1560 + } else {
1561 + $profile_url = 'https://gravatar.com/' . md5( strtolower( trim( $email ) ) );
1562 + }
1467 1563
1564 + if ( ! isset( $avatar_url ) ) {
1468 1565 $avatar_url = $this->api->get_avatar_url( $email );
1469 1566 }
1470 1567
1471 1568 if ( $show_email_and_ip ) {
@@ -1491,11 +1588,29 @@
1491 1588 );
1492 1589
1493 1590 if ( $site_id > -1 ) {
1494 1591 $author['site_ID'] = (int) $site_id;
1495 - $author['site_visible'] = $site_visible;
1592 + $author['site_visible'] = $site_visible ?? null;
1496 1593 }
1497 1594
1595 + // Only include WordPress.com user data when author_wpcom_data is enabled.
1596 + $args = $this->query_args();
1597 +
1598 + if ( ! empty( $id ) && ! empty( $args['author_wpcom_data'] ) ) {
1599 + if ( ( new Host() )->is_wpcom_simple() ) {
1600 + $user = get_user_by( 'id', $id );
1601 + $author['wpcom_id'] = isset( $user->ID ) ? (int) $user->ID : null;
1602 + $author['wpcom_login'] = $user->user_login ?? '';
1603 + } else {
1604 + // If this is a Jetpack site, use the connection manager to get the user data.
1605 + $wpcom_user_data = ( new Manager() )->get_connected_user_data( $id );
1606 + if ( $wpcom_user_data && isset( $wpcom_user_data['ID'] ) ) {
1607 + $author['wpcom_id'] = (int) $wpcom_user_data['ID'];
1608 + $author['wpcom_login'] = $wpcom_user_data['login'] ?? '';
1609 + }
1610 + }
1611 + }
1612 +
1498 1613 return (object) $author;
1499 1614 }
1500 1615
1501 1616 /**
@@ -1547,9 +1662,8 @@
1547 1662 * @param string|null $file File path.
1548 1663 * @return object|WP_Error Media item data, or WP_Error.
1549 1664 */
1550 1665 public function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1551 -
1552 1666 if ( ! $media_item ) {
1553 1667 $media_item = get_post( $media_id );
1554 1668 }
1555 1669
@@ -1556,52 +1670,53 @@
1556 1670 if ( ! $media_item || is_wp_error( $media_item ) ) {
1557 1671 return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1558 1672 }
1559 1673
1560 - $attachment_file = get_attached_file( $media_item->ID );
1674 + $attachment_file = isset( $media_item->ID ) ? get_attached_file( $media_item->ID ) : null;
1561 1675
1562 1676 $file = basename( $attachment_file ? $attachment_file : $file );
1563 1677 $file_info = pathinfo( $file );
1564 - $ext = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1678 + $ext = $file_info['extension'] ?? null;
1565 1679
1566 1680 // File operations are handled differently on WordPress.com.
1567 1681 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1568 - $attachment_metadata = wp_get_attachment_metadata( $media_item->ID );
1569 - $filesize = ! empty( $attachment_metadata['filesize'] )
1570 - ? $attachment_metadata['filesize']
1571 - : 0;
1682 + $attachment_metadata = isset( $media_item->ID ) ? wp_get_attachment_metadata( $media_item->ID ) : array();
1683 + $filesize = ! empty( $attachment_metadata['filesize'] ) ? $attachment_metadata['filesize'] : 0;
1572 1684 } else {
1573 1685 // For VideoPress videos, $attachment_file is the video URL.
1574 - $filesize = file_exists( $attachment_file )
1575 - ? filesize( $attachment_file )
1576 - : 0;
1686 + $filesize = ( $attachment_file && file_exists( $attachment_file ) )
1687 + ? filesize( $attachment_file )
1688 + : 0;
1577 1689 }
1578 1690
1579 1691 $response = array(
1580 - 'ID' => $media_item->ID,
1581 - 'URL' => wp_get_attachment_url( $media_item->ID ),
1582 - 'guid' => $media_item->guid,
1583 - 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1584 - 'post_ID' => $media_item->post_parent,
1585 - 'author_ID' => (int) $media_item->post_author,
1692 + 'ID' => $media_item->ID ?? null,
1693 + 'URL' => isset( $media_item->ID ) ? wp_get_attachment_url( $media_item->ID ) : null,
1694 + 'guid' => $media_item->guid ?? null,
1695 + 'date' => ( isset( $media_item->post_date_gmt ) && isset( $media_item->post_date ) ) ?
1696 + (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ) : null,
1697 + 'post_ID' => $media_item->post_parent ?? null,
1698 + 'author_ID' => isset( $media_item->post_author ) ? (int) $media_item->post_author : null,
1586 1699 'file' => $file,
1587 - 'mime_type' => $media_item->post_mime_type,
1700 + 'mime_type' => $media_item->post_mime_type ?? null,
1588 1701 'extension' => $ext,
1589 - 'title' => $media_item->post_title,
1590 - 'caption' => $media_item->post_excerpt,
1591 - 'description' => $media_item->post_content,
1592 - 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1593 - 'icon' => wp_mime_type_icon( $media_item->ID ),
1702 + 'title' => $media_item->post_title ?? '',
1703 + 'caption' => $media_item->post_excerpt ?? '',
1704 + 'description' => $media_item->post_content ?? '',
1705 + 'alt' => isset( $media_item->ID ) ? get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ) : '',
1706 + 'icon' => isset( $media_item->ID ) ? wp_mime_type_icon( $media_item->ID ) : null,
1594 1707 'size' => size_format( (int) $filesize, 2 ),
1595 1708 'thumbnails' => array(),
1596 1709 );
1597 1710
1598 - if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ), true ) ) {
1711 + if ( in_array( $ext, array( 'jpg', 'jpeg', 'png', 'gif', 'webp' ), true ) && isset( $media_item->ID ) ) {
1599 1712 $metadata = wp_get_attachment_metadata( $media_item->ID );
1600 - if ( isset( $metadata['height'], $metadata['width'] ) ) {
1713 + if ( isset( $metadata['height'] ) ) {
1601 1714 $response['height'] = $metadata['height'];
1602 - $response['width'] = $metadata['width'];
1603 1715 }
1716 + if ( isset( $metadata['width'] ) ) {
1717 + $response['width'] = $metadata['width'];
1718 + }
1604 1719
1605 1720 if ( isset( $metadata['sizes'] ) ) {
1606 1721 /**
1607 1722 * Filter the thumbnail sizes available for each attachment ID.
@@ -1615,9 +1730,11 @@
1615 1730 */
1616 1731 $sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1617 1732 if ( is_array( $sizes ) ) {
1618 1733 foreach ( $sizes as $size => $size_details ) {
1619 - $response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1734 + if ( isset( $size_details['file'] ) ) {
1735 + $response['thumbnails'][ $size ] = dirname( $response['URL'] ) . '/' . $size_details['file'];
1736 + }
1620 1737 }
1621 1738 /**
1622 1739 * Filter the thumbnail URLs for attachment files.
1623 1740 *
@@ -1635,12 +1752,15 @@
1635 1752 $response['exif'] = $metadata['image_meta'];
1636 1753 }
1637 1754 }
1638 1755
1639 - if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ), true ) ) {
1640 - $metadata = wp_get_attachment_metadata( $media_item->ID );
1641 - $response['length'] = $metadata['length'];
1642 - $response['exif'] = $metadata;
1756 + if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ), true ) && isset( $media_item->ID ) ) {
1757 + $metadata = wp_get_attachment_metadata( $media_item->ID );
1758 +
1759 + if ( isset( $metadata['length'] ) ) {
1760 + $response['length'] = $metadata['length'];
1761 + }
1762 + $response['exif'] = is_array( $metadata ) ? $metadata : false;
1643 1763 }
1644 1764
1645 1765 $is_video = false;
1646 1766
@@ -1650,20 +1770,30 @@
1650 1770 ) {
1651 1771 $is_video = true;
1652 1772 }
1653 1773
1654 - if ( $is_video ) {
1774 + if ( $is_video && isset( $media_item->ID ) ) {
1655 1775 $metadata = wp_get_attachment_metadata( $media_item->ID );
1656 1776
1657 - if ( isset( $metadata['height'], $metadata['width'] ) ) {
1777 + if ( isset( $metadata['height'] ) ) {
1658 1778 $response['height'] = $metadata['height'];
1659 - $response['width'] = $metadata['width'];
1660 1779 }
1780 + if ( isset( $metadata['width'] ) ) {
1781 + $response['width'] = $metadata['width'];
1782 + }
1661 1783
1662 1784 if ( isset( $metadata['length'] ) ) {
1663 1785 $response['length'] = $metadata['length'];
1664 1786 }
1665 1787
1788 + if ( empty( $response['length'] ) && isset( $metadata['duration'] ) ) {
1789 + $response['length'] = (int) $metadata['duration'];
1790 + }
1791 +
1792 + if ( empty( $response['length'] ) && isset( $metadata['videopress']['duration'] ) ) {
1793 + $response['length'] = ceil( $metadata['videopress']['duration'] / 1000 );
1794 + }
1795 +
1666 1796 // add VideoPress info.
1667 1797 if ( function_exists( 'video_get_info_by_blogpostid' ) ) {
1668 1798 $info = video_get_info_by_blogpostid( $this->api->get_blog_id_for_output(), $media_item->ID );
1669 1799
@@ -1704,9 +1834,9 @@
1704 1834 $response['privacy_setting'] = (int) $metadata['videopress']['privacy_setting'];
1705 1835 }
1706 1836
1707 1837 $thumbnail_query_data = array();
1708 - if ( function_exists( 'video_is_private' ) && video_is_private( $info ) ) {
1838 + if ( ! empty( $info ) && function_exists( 'video_is_private' ) && video_is_private( $info ) ) {
1709 1839 $thumbnail_query_data['metadata_token'] = video_generate_auth_token( $info );
1710 1840 }
1711 1841
1712 1842 // Thumbnails.
@@ -1732,12 +1862,9 @@
1732 1862 // If we didn't get VideoPress information (for some reason) then let's
1733 1863 // not try and include it in the response.
1734 1864 if ( isset( $info->guid ) ) {
1735 1865 $response['videopress_guid'] = $info->guid;
1736 - $response['videopress_processing_done'] = true;
1737 - if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1738 - $response['videopress_processing_done'] = false;
1739 - }
1866 + $response['videopress_processing_done'] = isset( $info->finish_date_gmt ) && '0000-00-00 00:00:00' !== $info->finish_date_gmt;
1740 1867 }
1741 1868 }
1742 1869 }
1743 1870
@@ -1744,10 +1871,10 @@
1744 1871 $response['thumbnails'] = (object) $response['thumbnails'];
1745 1872
1746 1873 $response['meta'] = (object) array(
1747 1874 'links' => (object) array(
1748 - 'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1749 - 'help' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ),
1875 + 'self' => isset( $media_item->ID ) ? (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ) : null,
1876 + 'help' => isset( $media_item->ID ) ? (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID, 'help' ) : null,
1750 1877 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1751 1878 ),
1752 1879 );
1753 1880
@@ -1757,9 +1884,9 @@
1757 1884 $response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1758 1885 }
1759 1886 }
1760 1887
1761 - if ( $media_item->post_parent > 0 ) {
1888 + if ( isset( $media_item->post_parent ) && $media_item->post_parent > 0 ) {
1762 1889 $response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1763 1890 }
1764 1891
1765 1892 return (object) $response;
@@ -1851,15 +1978,15 @@
1851 1978 * particular timezone or offset we will assume it was given in GMT
1852 1979 * relative to now and will convert it to local time using either the
1853 1980 * timezone set in the options table for the blog or the GMT offset.
1854 1981 *
1855 - * @param datetime string $date_string Date to parse.
1982 + * @param string $date_string Date to parse.
1856 1983 *
1857 - * @return array( $local_time_string, $gmt_time_string )
1984 + * @return array{string,string} ( $local_time_string, $gmt_time_string )
1858 1985 */
1859 1986 public function parse_date( $date_string ) {
1860 1987 $date_string_info = date_parse( $date_string );
1861 - if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1988 + if ( 0 === $date_string_info['error_count'] ) {
1862 1989 // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1863 1990 if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1864 1991 $dt_utc = new DateTime( $date_string );
1865 1992 $dt_local = clone $dt_utc;
@@ -1864,10 +1991,10 @@
1864 1991 $dt_utc = new DateTime( $date_string );
1865 1992 $dt_local = clone $dt_utc;
1866 1993 $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1867 1994 return array(
1868 - (string) $dt_local->format( 'Y-m-d H:i:s' ),
1869 - (string) $dt_utc->format( 'Y-m-d H:i:s' ),
1995 + $dt_local->format( 'Y-m-d H:i:s' ),
1996 + $dt_utc->format( 'Y-m-d H:i:s' ),
1870 1997 );
1871 1998 }
1872 1999
1873 2000 // It's parseable but no TZ info so assume UTC.
@@ -1881,10 +2008,10 @@
1881 2008
1882 2009 $dt_local->setTimezone( wp_timezone() );
1883 2010
1884 2011 return array(
1885 - (string) $dt_local->format( 'Y-m-d H:i:s' ),
1886 - (string) $dt_utc->format( 'Y-m-d H:i:s' ),
2012 + $dt_local->format( 'Y-m-d H:i:s' ),
2013 + $dt_utc->format( 'Y-m-d H:i:s' ),
1887 2014 );
1888 2015 }
1889 2016
1890 2017 /**
@@ -1890,9 +2017,9 @@
1890 2017 /**
1891 2018 * Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1892 2019 */
1893 2020 public function load_theme_functions() {
1894 - if ( false === defined( 'STYLESHEETPATH' ) ) {
2021 + if ( ! defined( 'STYLESHEETPATH' ) ) {
1895 2022 wp_templating_constants();
1896 2023 }
1897 2024
1898 2025 // bail if we've done this already (can happen when calling /batch endpoint).
@@ -2005,9 +2132,9 @@
2005 2132 $file_name = $reflection->getFileName();
2006 2133 foreach ( $base_paths as $base_path ) {
2007 2134
2008 2135 // only copy hooks with functions which are part of the specified files.
2009 - if ( 0 === strpos( $file_name, $base_path ) ) {
2136 + if ( str_starts_with( $file_name, $base_path ) ) {
2010 2137 add_action(
2011 2138 $to_hook,
2012 2139 $callback_data['function'],
2013 2140 $priority,
@@ -2219,8 +2346,13 @@
2219 2346 $_FILES['.api.media.item.'] = $media_item;
2220 2347
2221 2348 if ( ! $user_can_upload_files ) {
2222 2349 $media_id = new WP_Error( 'unauthorized', 'User cannot upload media.', 403 );
2350 + } elseif ( ! is_array( $media_item ) ) {
2351 + $media_id = new WP_Error( 'invalid_input', 'Unable to process request.', 400 );
2352 + $media_item = array(
2353 + 'name' => 'invalid_file',
2354 + );
2223 2355 } elseif ( $this->media_item_is_free_video_mobile_upload_and_too_long( $media_item ) ) {
2224 2356 $media_id = new WP_Error( 'upload_video_length', 'Video uploads longer than 5 minutes require a paid plan.', 400 );
2225 2357 } else {
2226 2358 if ( $force_parent_id ) {
@@ -2321,9 +2453,9 @@
2321 2453
2322 2454 if ( ! empty( $id3_meta ) ) {
2323 2455 // Before updating metadata, ensure that the item is audio.
2324 2456 $item = $this->get_media_item_v1_1( $media_id );
2325 - if ( 0 === strpos( $item->mime_type, 'audio/' ) ) {
2457 + if ( str_starts_with( $item->mime_type, 'audio/' ) ) {
2326 2458 wp_update_attachment_metadata( $media_id, $id3_meta );
2327 2459 }
2328 2460 }
2329 2461
@@ -2420,10 +2552,10 @@
2420 2552 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
2421 2553 return $mimes;
2422 2554 }
2423 2555
2424 - // bail early if they already have the upgrade..
2425 - if ( wpcom_site_has_videopress() ) {
2556 + // bail early if they already have video upload capability.
2557 + if ( wpcom_site_can_upload_videos() ) {
2426 2558 return $mimes;
2427 2559 }
2428 2560
2429 2561 // lets whitelist to only specific clients right now.
@@ -2437,9 +2569,9 @@
2437 2569 *
2438 2570 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
2439 2571 */
2440 2572 $clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
2441 - if ( ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- Check what types are expected here.
2573 + if ( ! isset( $this->api->token_details['client_id'] ) || ! in_array( $this->api->token_details['client_id'], $clients_allowed_video_uploads, true ) ) {
2442 2574 return $mimes;
2443 2575 }
2444 2576
2445 2577 $mime_list = wp_get_mime_types();
@@ -2459,9 +2591,9 @@
2459 2591
2460 2592 if ( ! empty( $video_exts ) ) {
2461 2593 foreach ( $video_exts as $ext ) {
2462 2594 foreach ( $mime_list as $ext_pattern => $mime ) {
2463 - if ( '' !== $ext && strpos( $ext_pattern, $ext ) !== false ) {
2595 + if ( '' !== $ext && str_contains( $ext_pattern, $ext ) ) {
2464 2596 $video_mimes[ $ext_pattern ] = $mime;
2465 2597 }
2466 2598 }
2467 2599 }
@@ -2563,10 +2695,13 @@
2563 2695 * - (dash). For example, pub.com will map to pub-com.cdn.ampproject.org."
2564 2696 */
2565 2697 if ( function_exists( 'idn_to_utf8' ) ) {
2566 2698 // The third parameter is set explicitly to prevent issues with newer PHP versions compiled with an old ICU version.
2567 - // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated, PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003DeprecatedRemoved
2568 - $host = idn_to_utf8( $host, IDNA_DEFAULT, defined( 'INTL_IDNA_VARIANT_UTS46' ) ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003 );
2699 + $variant = defined( 'INTL_IDNA_VARIANT_UTS46' )
2700 + ? INTL_IDNA_VARIANT_UTS46
2701 + // phpcs:ignore PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003Deprecated, PHPCompatibility.Constants.RemovedConstants.intl_idna_variant_2003DeprecatedRemoved
2702 + : INTL_IDNA_VARIANT_2003; // @phan-suppress-current-line PhanUndeclaredConstant
2703 + $host = idn_to_utf8( $host, IDNA_DEFAULT, $variant );
2569 2704 }
2570 2705 $subdomain = str_replace( array( '-', '.' ), array( '--', '-' ), $host );
2571 2706 return array(
2572 2707 $siteurl,
@@ -2578,8 +2713,246 @@
2578 2713 sprintf( 'https://%s.amp.cloudflare.com', $subdomain ),
2579 2714 // Bing AMP Cache.
2580 2715 sprintf( 'https://%s.bing-amp.com', $subdomain ),
2581 2716 );
2717 + }
2718 +
2719 + /**
2720 + * Register a REST route for this jsonAPI endpoint.
2721 + *
2722 + * @return void
2723 + * @throws Exception The exception if something goes wrong.
2724 + */
2725 + public function create_rest_route_for_endpoint() {
2726 + register_rest_route(
2727 + static::REST_NAMESPACE,
2728 + $this->build_rest_route_regex(),
2729 + array(
2730 + 'methods' => $this->method,
2731 + 'callback' => array( $this, 'rest_callback' ),
2732 + 'permission_callback' => array( $this, 'rest_permission_callback' ),
2733 + )
2734 + );
2735 + }
2736 +
2737 + /**
2738 + * Handle the rest call.
2739 + *
2740 + * @param WP_REST_Request $request The request object.
2741 + *
2742 + * @return mixed|WP_Error
2743 + */
2744 + public function rest_callback( WP_REST_Request $request ) {
2745 + // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- Making sure random warnings don't break JSON.
2746 + ini_set( 'display_errors', false );
2747 +
2748 + $blog_id = Jetpack_Options::get_option( 'id' );
2749 +
2750 + add_filter( 'user_can_richedit', '__return_true' );
2751 + add_filter( 'comment_edit_pre', array( $this->api, 'comment_edit_pre' ) );
2752 +
2753 + $this->api->initialize();
2754 + $this->api->endpoint = $this;
2755 +
2756 + $this->api->path = $this->path;
2757 + $this->api->version = $this->max_version;
2758 +
2759 + $locale = $request->get_param( 'language' );
2760 + if ( $locale ) {
2761 + $this->api->init_locale( $locale );
2762 + }
2763 +
2764 + if ( $this->in_testing && ! WPCOM_JSON_API__DEBUG ) {
2765 + return new WP_Error( 'endpoint_not_available' );
2766 + }
2767 +
2768 + $token_data = ( new Manager() )->verify_xml_rpc_signature();
2769 + if ( ! $token_data || empty( $token_data['token_key'] ) || ! array_key_exists( 'user_id', $token_data ) ) {
2770 + return new WP_Error( 'response_signature_error' );
2771 + }
2772 +
2773 + $token = ( new Tokens() )->get_access_token( $token_data['user_id'], $token_data['token_key'] );
2774 + if ( is_wp_error( $token ) ) {
2775 + return $token;
2776 + }
2777 + if ( ! $token ) {
2778 + return new WP_Error( 'response_signature_error' );
2779 + }
2780 +
2781 + /** This action is documented in class.json-api.php */
2782 + do_action( 'wpcom_json_api_output', $this->stat );
2783 +
2784 + $response = call_user_func_array(
2785 + array( $this, 'callback' ),
2786 + array_values( array( $this->path, $blog_id ) + $request->get_url_params() )
2787 + );
2788 +
2789 + if ( ! $response && ! is_array( $response ) ) {
2790 + // Dealing with empty non-array response.
2791 + $response = new WP_Error( 'empty_response', 'Endpoint response is empty', 500 );
2792 + }
2793 +
2794 + // Mirror the XML-RPC path, which runs filter_fields() in WPCOM_JSON_API::output() before
2795 + // returning, so a `fields` request yields the same keys on both transports. Endpoints may
2796 + // force-add keys past `fields` for internal processors (e.g. the post type/status/password);
2797 + // without this they would leak on the REST transport only.
2798 + if ( ! is_wp_error( $response ) ) {
2799 + $response = $this->api->filter_fields( $response );
2800 + }
2801 +
2802 + $status_code = 200;
2803 +
2804 + if ( is_wp_error( $response ) ) {
2805 + $status_code = 500;
2806 +
2807 + if ( $response->get_error_data() && is_scalar( $response->get_error_data() )
2808 + && (string) (int) $response->get_error_data() === (string) $response->get_error_data()
2809 + ) {
2810 + $status_code = (int) $response->get_error_data();
2811 + }
2812 +
2813 + $response = WPCOM_JSON_API::serializable_error( $response );
2814 + }
2815 +
2816 + if ( $request->get_param( 'http_envelope' ) ) {
2817 + $response = WPCOM_JSON_API::wrap_http_envelope( $status_code, $response, 'application/json' );
2818 + }
2819 +
2820 + $response = wp_json_encode( $response, JSON_UNESCAPED_SLASHES );
2821 +
2822 + $nonce = wp_generate_password( 10, false );
2823 + $hmac = hash_hmac( 'sha1', $nonce . $response, $token->secret );
2824 +
2825 + return array(
2826 + $response,
2827 + (string) $nonce,
2828 + $hmac,
2829 + );
2830 + }
2831 +
2832 + /**
2833 + * The REST endpoint should only be available for requests signed with a valid blog or user token.
2834 + * Declaring it "final" so individual endpoints couldn't remove this requirement.
2835 + *
2836 + * If you need to add custom permissions to individual endpoints, you can override method `rest_permission_callback_custom()`.
2837 + *
2838 + * @see self::rest_permission_callback_custom()
2839 + *
2840 + * @return true|WP_Error
2841 + */
2842 + final public function rest_permission_callback() {
2843 + $manager = new Manager( 'jetpack' );
2844 + if ( ! $manager->is_connected() ) {
2845 + return new WP_Error( 'site_not_connected' );
2846 + }
2847 +
2848 + if ( ( ( $this->allow_jetpack_site_auth || $this->allow_fallback_to_jetpack_blog_token ) && Rest_Authentication::is_signed_with_blog_token() )
2849 + || ( get_current_user_id() && Rest_Authentication::is_signed_with_user_token() )
2850 + ) {
2851 + $custom_permission_result = $this->rest_permission_callback_custom();
2852 +
2853 + // Successful custom permission check.
2854 + if ( $custom_permission_result === true ) {
2855 + return true;
2856 + }
2857 +
2858 + // Custom permission check errored, returning the error.
2859 + if ( is_wp_error( $custom_permission_result ) ) {
2860 + return $custom_permission_result;
2861 + }
2862 +
2863 + // Custom permission check failed, but didn't return a specific error. Proceed to returning the generic error.
2864 + }
2865 +
2866 + $message = esc_html__(
2867 + 'You do not have the correct user permissions to perform this action. Please contact your site admin if you think this is a mistake.',
2868 + 'jetpack'
2869 + );
2870 + return new WP_Error( 'rest_api_invalid_permission', $message, array( 'status' => rest_authorization_required_code() ) );
2871 + }
2872 +
2873 + /**
2874 + * You can override this method in individual endpoints to add custom permission checks.
2875 + * This will run on top of `rest_permission_callback()`.
2876 + *
2877 + * @see self::rest_permission_callback()
2878 + *
2879 + * @return true|WP_Error
2880 + */
2881 + public function rest_permission_callback_custom() {
2882 + return true;
2883 + }
2884 +
2885 + /**
2886 + * Build the REST endpoint URL.
2887 + *
2888 + * @return string
2889 + */
2890 + public function build_rest_route() {
2891 + $version_prefix = $this->max_version ? 'v' . $this->max_version : '';
2892 + return $version_prefix . $this->rest_route;
2893 + }
2894 +
2895 + /**
2896 + * Whether the endpoint's rest_route carries %d/%s path-parameter tokens.
2897 + *
2898 + * @return bool
2899 + */
2900 + private function rest_route_has_tokens() {
2901 + return str_contains( (string) $this->rest_route, '%' );
2902 + }
2903 +
2904 + /**
2905 + * REST route with %d/%s path tokens converted to named captures, for register_rest_route().
2906 + * Static (token-less) routes are returned unchanged.
2907 + *
2908 + * @return string
2909 + */
2910 + public function build_rest_route_regex() {
2911 + if ( ! $this->rest_route_has_tokens() ) {
2912 + return $this->build_rest_route();
2913 + }
2914 +
2915 + $index = 0;
2916 + return preg_replace_callback(
2917 + '/%[sd]/',
2918 + function ( $matches ) use ( &$index ) {
2919 + $name = 'p' . ( ++$index );
2920 + return '%d' === $matches[0] ? "(?P<$name>\\d+)" : "(?P<$name>[^/]+)";
2921 + },
2922 + $this->build_rest_route()
2923 + );
2924 + }
2925 +
2926 + /**
2927 + * Concrete REST route for a single request: the real path-parameter values (from the request URL,
2928 + * minus the leading site segment) substituted into the tokenized rest_route. Static routes are
2929 + * returned unchanged. Used by the proxy transport.
2930 + *
2931 + * @param string $url Full request URL.
2932 + * @return string
2933 + */
2934 + public function build_concrete_rest_route( $url ) {
2935 + if ( ! $this->rest_route_has_tokens() ) {
2936 + return $this->build_rest_route();
2937 + }
2938 +
2939 + // The request path minus its "/rest/vX.Y/sites/<site>" prefix already IS the concrete route
2940 + // tail. The proxy matched this request to the endpoint's path template first, so the tail is
2941 + // guaranteed to fit the pattern build_rest_route_regex() registered on the remote.
2942 + $path = (string) wp_parse_url( $url, PHP_URL_PATH );
2943 + $path = preg_replace( '#^/rest/v[\d.]+/sites/[^/]+#', '', $path );
2944 +
2945 + return 'v' . $this->max_version . $path;
2946 + }
2947 +
2948 + /**
2949 + * Get Jetpack Version where support for the endpoint was introduced.
2950 + *
2951 + * @return string
2952 + */
2953 + public function get_rest_min_jp_version() {
2954 + return $this->rest_min_jp_version;
2582 2955 }
2583 2956
2584 2957 /**
2585 2958 * Return endpoint response