PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 14.2.2 All 502 releases
← All changes | class.json-api-endpoints.php +481 -145 13.7.216.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';
@@ -76,9 +80,8 @@
76 80 /**
77 81 * Maximum version of the api for which to serve this endpoint
78 82 *
79 83 * @var string
80 - * @phan-suppress PhanUndeclaredConstant -- https://github.com/phan/phan/issues/4855
81 84 */
82 85 public $max_version = WPCOM_JSON_API__CURRENT_VERSION;
83 86
84 87 /**
@@ -98,9 +101,8 @@
98 101 /**
99 102 * Version of the endpoint this endpoint is deprecated in favor of.
100 103 *
101 104 * @var string
102 - * @phan-suppress PhanUndeclaredConstant -- https://github.com/phan/phan/issues/4855
103 105 */
104 106 protected $new_version = WPCOM_JSON_API__CURRENT_VERSION;
105 107
106 108 /**
@@ -124,8 +126,22 @@
124 126 */
125 127 public $path_labels = array();
126 128
127 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 + /**
128 144 * Accepted query parameters
129 145 *
130 146 * @var array
131 147 */
@@ -256,8 +272,15 @@
256 272 */
257 273 public $allow_jetpack_site_auth = false;
258 274
259 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 + /**
260 283 * Set to true if the endpoint should accept auth from an upload token.
261 284 *
262 285 * @var bool
263 286 */
@@ -277,8 +300,34 @@
277 300 */
278 301 public $allow_fallback_to_jetpack_blog_token = false;
279 302
280 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 + /**
281 330 * Constructor.
282 331 *
283 332 * @param string|array|object $args Args.
284 333 */
@@ -299,8 +348,10 @@
299 348 'deprecated' => false,
300 349 'new_version' => WPCOM_JSON_API__CURRENT_VERSION,
301 350 'jp_disabled' => false,
302 351 'path_labels' => array(),
352 + 'rest_route' => null,
353 + 'rest_min_jp_version' => null,
303 354 'request_format' => array(),
304 355 'response_format' => array(),
305 356 'query_parameters' => array(),
306 357 'version' => 'v1',
@@ -312,8 +363,9 @@
312 363 'custom_fields_filtering' => false,
313 364 'allow_cross_origin_request' => false,
314 365 'allow_unauthorized_request' => false,
315 366 'allow_jetpack_site_auth' => false,
367 + 'allow_jetpack_token_auth' => false,
316 368 'allow_upload_token_auth' => false,
317 369 'allow_fallback_to_jetpack_blog_token' => false,
318 370 );
319 371
@@ -338,8 +390,11 @@
338 390 $this->max_version = $args['max_version'];
339 391 $this->deprecated = $args['deprecated'];
340 392 $this->new_version = $args['new_version'];
341 393
394 + $this->rest_route = $args['rest_route'];
395 + $this->rest_min_jp_version = $args['rest_min_jp_version'];
396 +
342 397 // Ensure max version is not less than min version.
343 398 if ( version_compare( $this->min_version, $this->max_version, '>' ) ) {
344 399 $this->max_version = $this->min_version;
345 400 }
@@ -349,8 +404,9 @@
349 404
350 405 $this->allow_cross_origin_request = (bool) $args['allow_cross_origin_request'];
351 406 $this->allow_unauthorized_request = (bool) $args['allow_unauthorized_request'];
352 407 $this->allow_jetpack_site_auth = (bool) $args['allow_jetpack_site_auth'];
408 + $this->allow_jetpack_token_auth = (bool) $args['allow_jetpack_token_auth'];
353 409 $this->allow_upload_token_auth = (bool) $args['allow_upload_token_auth'];
354 410 $this->allow_fallback_to_jetpack_blog_token = (bool) $args['allow_fallback_to_jetpack_blog_token'];
355 411 $this->require_rewind_auth = isset( $args['require_rewind_auth'] ) ? (bool) $args['require_rewind_auth'] : false;
356 412
@@ -386,8 +442,12 @@
386 442 $this->example_request_data = $args['example_request_data'];
387 443 $this->example_response = $args['example_response'];
388 444
389 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 + }
390 450 }
391 451
392 452 /**
393 453 * Get all query args. Prefill with defaults.
@@ -770,8 +830,10 @@
770 830 'profile_URL' => '(URL)',
771 831 'is_super_admin' => '(bool)',
772 832 'roles' => '(array:string)',
773 833 'ip_address' => '(string|false)',
834 + 'wpcom_id' => '(int|null)',
835 + 'wpcom_login' => '(string|null)',
774 836 );
775 837 $return[ $key ] = (object) $this->cast_and_filter( $value, $docs, false, $for_output );
776 838 break;
777 839 case 'role':
@@ -1033,9 +1095,9 @@
1033 1095 * @param bool $show_description Whether to show the description.
1034 1096 */
1035 1097 public function document( $show_description = true ) {
1036 1098 global $wpdb;
1037 - $original_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : 'unset';
1099 + $original_post = $GLOBALS['post'] ?? 'unset';
1038 1100 unset( $GLOBALS['post'] );
1039 1101
1040 1102 $doc = $this->generate_documentation();
1041 1103
@@ -1386,112 +1448,121 @@
1386 1448 $first_name = null;
1387 1449 $last_name = null;
1388 1450 $nice = null;
1389 1451 $url = null;
1390 - $ip_address = isset( $author->comment_author_IP ) ? $author->comment_author_IP : '';
1452 + $ip_address = $author->comment_author_IP ?? '';
1453 + $site_id = -1;
1391 1454
1392 1455 if ( isset( $author->comment_author_email ) ) {
1393 - $id = ( isset( $author->user_id ) && $author->user_id ) ? $author->user_id : 0;
1394 - $login = '';
1395 - $email = $author->comment_author_email;
1396 - $name = $author->comment_author;
1397 - $first_name = '';
1398 - $last_name = '';
1399 - $url = $author->comment_author_url;
1400 - $avatar_url = $this->api->get_avatar_url( $author );
1401 - $profile_url = 'https://gravatar.com/' . md5( strtolower( trim( $email ) ) );
1402 - $nice = '';
1403 - $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 + }
1404 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 +
1405 1481 // Comment author URLs and Emails are sent through wp_kses() on save, which replaces "&" with "&amp;"
1406 1482 // "&" is the only email/URL character altered by wp_kses().
1407 1483 foreach ( array( 'email', 'url' ) as $field ) {
1408 1484 $$field = str_replace( '&amp;', '&', $$field );
1409 1485 }
1410 - } else {
1411 - if ( $author instanceof WP_User || isset( $author->user_email ) ) {
1412 - $author = $author->ID;
1413 - } elseif ( isset( $author->user_id ) && $author->user_id ) {
1414 - $author = $author->user_id;
1415 - } elseif ( isset( $author->post_author ) ) {
1416 - // then $author is a Post Object.
1417 - if ( ! $author->post_author ) {
1418 - return null;
1419 - }
1420 - /**
1421 - * Filter whether the current site is a Jetpack site.
1422 - *
1423 - * @module json-api
1424 - *
1425 - * @since 3.3.0
1426 - *
1427 - * @param bool false Is the current site a Jetpack site. Default to false.
1428 - * @param int get_current_blog_id() Blog ID.
1429 - */
1430 - $is_jetpack = true === apply_filters( 'is_jetpack_site', false, get_current_blog_id() );
1431 - $post_id = $author->ID;
1432 - if ( $is_jetpack && ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) {
1433 - $id = get_post_meta( $post_id, '_jetpack_post_author_external_id', true );
1434 - $email = get_post_meta( $post_id, '_jetpack_author_email', true );
1435 - $login = '';
1436 - $name = get_post_meta( $post_id, '_jetpack_author', true );
1437 - $first_name = '';
1438 - $last_name = '';
1439 - $url = '';
1440 - $nice = '';
1441 - } else {
1442 - $author = $author->post_author;
1443 - }
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;
1444 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 + }
1445 1520
1446 - if ( ! isset( $id ) ) {
1447 - $user = get_user_by( 'id', $author );
1448 - if ( ! $user || is_wp_error( $user ) ) {
1449 - trigger_error( 'Unknown user', E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
1450 -
1451 - return null;
1452 - }
1453 - $id = $user->ID;
1454 - $email = $user->user_email;
1455 - $login = $user->user_login;
1456 - $name = $user->display_name;
1457 - $first_name = $user->first_name;
1458 - $last_name = $user->last_name;
1459 - $url = $user->user_url;
1460 - $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;
1461 1525 }
1462 - if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! $is_jetpack ) {
1463 - $site_id = -1;
1464 -
1465 - /**
1466 - * Allow customizing the blog ID returned with the author in WordPress.com REST API queries.
1467 - *
1468 - * @since 12.9
1469 - *
1470 - * @module json-api
1471 - *
1472 - * @param bool|int $active_blog Blog ID, or false by default.
1473 - * @param int $id User ID.
1474 - */
1475 - $active_blog = apply_filters( 'wpcom_api_pre_get_active_blog_author', false, $id );
1476 - if ( false === $active_blog ) {
1477 - $active_blog = get_active_blog_for_user( $id );
1478 - }
1479 - if ( ! empty( $active_blog ) ) {
1480 - $site_id = $active_blog->blog_id;
1481 - }
1482 - if ( $site_id > -1 ) {
1483 - $site_visible = (
1484 - -1 !== (int) $active_blog->public ||
1485 - is_private_blog_user( $site_id, get_current_user_id() )
1486 - );
1487 - }
1488 - $profile_url = "https://gravatar.com/{$login}";
1489 - } else {
1490 - $profile_url = 'https://gravatar.com/' . md5( strtolower( trim( $email ) ) );
1491 - $site_id = -1;
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 ) {
1548 + $active_blog = get_active_blog_for_user( $id );
1492 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 + }
1493 1563
1564 + if ( ! isset( $avatar_url ) ) {
1494 1565 $avatar_url = $this->api->get_avatar_url( $email );
1495 1566 }
1496 1567
1497 1568 if ( $show_email_and_ip ) {
@@ -1517,11 +1588,29 @@
1517 1588 );
1518 1589
1519 1590 if ( $site_id > -1 ) {
1520 1591 $author['site_ID'] = (int) $site_id;
1521 - $author['site_visible'] = $site_visible;
1592 + $author['site_visible'] = $site_visible ?? null;
1522 1593 }
1523 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 +
1524 1613 return (object) $author;
1525 1614 }
1526 1615
1527 1616 /**
@@ -1573,9 +1662,8 @@
1573 1662 * @param string|null $file File path.
1574 1663 * @return object|WP_Error Media item data, or WP_Error.
1575 1664 */
1576 1665 public function get_media_item_v1_1( $media_id, $media_item = null, $file = null ) {
1577 -
1578 1666 if ( ! $media_item ) {
1579 1667 $media_item = get_post( $media_id );
1580 1668 }
1581 1669
@@ -1582,52 +1670,53 @@
1582 1670 if ( ! $media_item || is_wp_error( $media_item ) ) {
1583 1671 return new WP_Error( 'unknown_media', 'Unknown Media', 404 );
1584 1672 }
1585 1673
1586 - $attachment_file = get_attached_file( $media_item->ID );
1674 + $attachment_file = isset( $media_item->ID ) ? get_attached_file( $media_item->ID ) : null;
1587 1675
1588 1676 $file = basename( $attachment_file ? $attachment_file : $file );
1589 1677 $file_info = pathinfo( $file );
1590 - $ext = isset( $file_info['extension'] ) ? $file_info['extension'] : null;
1678 + $ext = $file_info['extension'] ?? null;
1591 1679
1592 1680 // File operations are handled differently on WordPress.com.
1593 1681 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1594 - $attachment_metadata = wp_get_attachment_metadata( $media_item->ID );
1595 - $filesize = ! empty( $attachment_metadata['filesize'] )
1596 - ? $attachment_metadata['filesize']
1597 - : 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;
1598 1684 } else {
1599 1685 // For VideoPress videos, $attachment_file is the video URL.
1600 - $filesize = file_exists( $attachment_file )
1601 - ? filesize( $attachment_file )
1602 - : 0;
1686 + $filesize = ( $attachment_file && file_exists( $attachment_file ) )
1687 + ? filesize( $attachment_file )
1688 + : 0;
1603 1689 }
1604 1690
1605 1691 $response = array(
1606 - 'ID' => $media_item->ID,
1607 - 'URL' => wp_get_attachment_url( $media_item->ID ),
1608 - 'guid' => $media_item->guid,
1609 - 'date' => (string) $this->format_date( $media_item->post_date_gmt, $media_item->post_date ),
1610 - 'post_ID' => $media_item->post_parent,
1611 - '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,
1612 1699 'file' => $file,
1613 - 'mime_type' => $media_item->post_mime_type,
1700 + 'mime_type' => $media_item->post_mime_type ?? null,
1614 1701 'extension' => $ext,
1615 - 'title' => $media_item->post_title,
1616 - 'caption' => $media_item->post_excerpt,
1617 - 'description' => $media_item->post_content,
1618 - 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1619 - '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,
1620 1707 'size' => size_format( (int) $filesize, 2 ),
1621 1708 'thumbnails' => array(),
1622 1709 );
1623 1710
1624 - 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 ) ) {
1625 1712 $metadata = wp_get_attachment_metadata( $media_item->ID );
1626 - if ( isset( $metadata['height'], $metadata['width'] ) ) {
1713 + if ( isset( $metadata['height'] ) ) {
1627 1714 $response['height'] = $metadata['height'];
1628 - $response['width'] = $metadata['width'];
1629 1715 }
1716 + if ( isset( $metadata['width'] ) ) {
1717 + $response['width'] = $metadata['width'];
1718 + }
1630 1719
1631 1720 if ( isset( $metadata['sizes'] ) ) {
1632 1721 /**
1633 1722 * Filter the thumbnail sizes available for each attachment ID.
@@ -1641,9 +1730,11 @@
1641 1730 */
1642 1731 $sizes = apply_filters( 'rest_api_thumbnail_sizes', $metadata['sizes'], $media_item->ID );
1643 1732 if ( is_array( $sizes ) ) {
1644 1733 foreach ( $sizes as $size => $size_details ) {
1645 - $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 + }
1646 1737 }
1647 1738 /**
1648 1739 * Filter the thumbnail URLs for attachment files.
1649 1740 *
@@ -1661,12 +1752,15 @@
1661 1752 $response['exif'] = $metadata['image_meta'];
1662 1753 }
1663 1754 }
1664 1755
1665 - if ( in_array( $ext, array( 'mp3', 'm4a', 'wav', 'ogg' ), true ) ) {
1666 - $metadata = wp_get_attachment_metadata( $media_item->ID );
1667 - $response['length'] = $metadata['length'];
1668 - $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;
1669 1763 }
1670 1764
1671 1765 $is_video = false;
1672 1766
@@ -1676,15 +1770,17 @@
1676 1770 ) {
1677 1771 $is_video = true;
1678 1772 }
1679 1773
1680 - if ( $is_video ) {
1774 + if ( $is_video && isset( $media_item->ID ) ) {
1681 1775 $metadata = wp_get_attachment_metadata( $media_item->ID );
1682 1776
1683 - if ( isset( $metadata['height'], $metadata['width'] ) ) {
1777 + if ( isset( $metadata['height'] ) ) {
1684 1778 $response['height'] = $metadata['height'];
1685 - $response['width'] = $metadata['width'];
1686 1779 }
1780 + if ( isset( $metadata['width'] ) ) {
1781 + $response['width'] = $metadata['width'];
1782 + }
1687 1783
1688 1784 if ( isset( $metadata['length'] ) ) {
1689 1785 $response['length'] = $metadata['length'];
1690 1786 }
@@ -1738,9 +1834,9 @@
1738 1834 $response['privacy_setting'] = (int) $metadata['videopress']['privacy_setting'];
1739 1835 }
1740 1836
1741 1837 $thumbnail_query_data = array();
1742 - if ( function_exists( 'video_is_private' ) && video_is_private( $info ) ) {
1838 + if ( ! empty( $info ) && function_exists( 'video_is_private' ) && video_is_private( $info ) ) {
1743 1839 $thumbnail_query_data['metadata_token'] = video_generate_auth_token( $info );
1744 1840 }
1745 1841
1746 1842 // Thumbnails.
@@ -1766,12 +1862,9 @@
1766 1862 // If we didn't get VideoPress information (for some reason) then let's
1767 1863 // not try and include it in the response.
1768 1864 if ( isset( $info->guid ) ) {
1769 1865 $response['videopress_guid'] = $info->guid;
1770 - $response['videopress_processing_done'] = true;
1771 - if ( '0000-00-00 00:00:00' === $info->finish_date_gmt ) {
1772 - $response['videopress_processing_done'] = false;
1773 - }
1866 + $response['videopress_processing_done'] = isset( $info->finish_date_gmt ) && '0000-00-00 00:00:00' !== $info->finish_date_gmt;
1774 1867 }
1775 1868 }
1776 1869 }
1777 1870
@@ -1778,10 +1871,10 @@
1778 1871 $response['thumbnails'] = (object) $response['thumbnails'];
1779 1872
1780 1873 $response['meta'] = (object) array(
1781 1874 'links' => (object) array(
1782 - 'self' => (string) $this->links->get_media_link( $this->api->get_blog_id_for_output(), $media_item->ID ),
1783 - '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,
1784 1877 'site' => (string) $this->links->get_site_link( $this->api->get_blog_id_for_output() ),
1785 1878 ),
1786 1879 );
1787 1880
@@ -1791,9 +1884,9 @@
1791 1884 $response['meta']->links->videopress = (string) $this->links->get_link( '/videos/%s', $response['videopress_guid'], '' );
1792 1885 }
1793 1886 }
1794 1887
1795 - if ( $media_item->post_parent > 0 ) {
1888 + if ( isset( $media_item->post_parent ) && $media_item->post_parent > 0 ) {
1796 1889 $response['meta']->links->parent = (string) $this->links->get_post_link( $this->api->get_blog_id_for_output(), $media_item->post_parent );
1797 1890 }
1798 1891
1799 1892 return (object) $response;
@@ -1891,9 +1984,9 @@
1891 1984 * @return array{string,string} ( $local_time_string, $gmt_time_string )
1892 1985 */
1893 1986 public function parse_date( $date_string ) {
1894 1987 $date_string_info = date_parse( $date_string );
1895 - if ( is_array( $date_string_info ) && 0 === $date_string_info['error_count'] ) {
1988 + if ( 0 === $date_string_info['error_count'] ) {
1896 1989 // Check if it's already localized. Can't just check is_localtime because date_parse('oppossum') returns true; WTF, PHP.
1897 1990 if ( isset( $date_string_info['zone'] ) && true === $date_string_info['is_localtime'] ) {
1898 1991 $dt_utc = new DateTime( $date_string );
1899 1992 $dt_local = clone $dt_utc;
@@ -1898,10 +1991,10 @@
1898 1991 $dt_utc = new DateTime( $date_string );
1899 1992 $dt_local = clone $dt_utc;
1900 1993 $dt_utc->setTimezone( new DateTimeZone( 'UTC' ) );
1901 1994 return array(
1902 - (string) $dt_local->format( 'Y-m-d H:i:s' ),
1903 - (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' ),
1904 1997 );
1905 1998 }
1906 1999
1907 2000 // It's parseable but no TZ info so assume UTC.
@@ -1915,10 +2008,10 @@
1915 2008
1916 2009 $dt_local->setTimezone( wp_timezone() );
1917 2010
1918 2011 return array(
1919 - (string) $dt_local->format( 'Y-m-d H:i:s' ),
1920 - (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' ),
1921 2014 );
1922 2015 }
1923 2016
1924 2017 /**
@@ -1924,9 +2017,9 @@
1924 2017 /**
1925 2018 * Load the functions.php file for the current theme to get its post formats, CPTs, etc.
1926 2019 */
1927 2020 public function load_theme_functions() {
1928 - if ( false === defined( 'STYLESHEETPATH' ) ) {
2021 + if ( ! defined( 'STYLESHEETPATH' ) ) {
1929 2022 wp_templating_constants();
1930 2023 }
1931 2024
1932 2025 // bail if we've done this already (can happen when calling /batch endpoint).
@@ -2253,8 +2346,13 @@
2253 2346 $_FILES['.api.media.item.'] = $media_item;
2254 2347
2255 2348 if ( ! $user_can_upload_files ) {
2256 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 + );
2257 2355 } elseif ( $this->media_item_is_free_video_mobile_upload_and_too_long( $media_item ) ) {
2258 2356 $media_id = new WP_Error( 'upload_video_length', 'Video uploads longer than 5 minutes require a paid plan.', 400 );
2259 2357 } else {
2260 2358 if ( $force_parent_id ) {
@@ -2454,10 +2552,10 @@
2454 2552 if ( ! defined( 'REST_API_REQUEST' ) || ! REST_API_REQUEST ) {
2455 2553 return $mimes;
2456 2554 }
2457 2555
2458 - // bail early if they already have the upgrade..
2459 - if ( wpcom_site_has_videopress() ) {
2556 + // bail early if they already have video upload capability.
2557 + if ( wpcom_site_can_upload_videos() ) {
2460 2558 return $mimes;
2461 2559 }
2462 2560
2463 2561 // lets whitelist to only specific clients right now.
@@ -2471,9 +2569,9 @@
2471 2569 *
2472 2570 * @param array $clients_allowed_video_uploads Array of whitelisted Video clients.
2473 2571 */
2474 2572 $clients_allowed_video_uploads = apply_filters( 'rest_api_clients_allowed_video_uploads', $clients_allowed_video_uploads );
2475 - 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 ) ) {
2476 2574 return $mimes;
2477 2575 }
2478 2576
2479 2577 $mime_list = wp_get_mime_types();
@@ -2615,8 +2713,246 @@
2615 2713 sprintf( 'https://%s.amp.cloudflare.com', $subdomain ),
2616 2714 // Bing AMP Cache.
2617 2715 sprintf( 'https://%s.bing-amp.com', $subdomain ),
2618 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;
2619 2955 }
2620 2956
2621 2957 /**
2622 2958 * Return endpoint response