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