| @@ -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 "&" |
| 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( '&', '&', $$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 |