← All changes
|
_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-external-media.php
+397
-42
12.0.3
→
16.3-a.5
View file →
| @@ -6,9 +6,14 @@ | ||
| 6 | 6 | * @since 8.7.0 |
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | 9 | use Automattic\Jetpack\Connection\Client; |
| 10 | +use Automattic\Jetpack\Connection\Manager; | |
| 10 | 11 | |
| 12 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 13 | + exit( 0 ); | |
| 14 | +} | |
| 15 | + | |
| 11 | 16 | /** |
| 12 | 17 | * External Media helper API. |
| 13 | 18 | * |
| 14 | 19 | * @since 8.7.0 |
| @@ -74,17 +79,8 @@ | ||
| 74 | 79 | */ |
| 75 | 80 | private static $services_regex = '(?P<service>google_photos|openverse|pexels)'; |
| 76 | 81 | |
| 77 | 82 | /** |
| 78 | - * Temporary filename. | |
| 79 | - * | |
| 80 | - * Needed to cope with Google's very long file names. | |
| 81 | - * | |
| 82 | - * @var string | |
| 83 | - */ | |
| 84 | - private $tmp_name; | |
| 85 | - | |
| 86 | - /** | |
| 87 | 83 | * Constructor. |
| 88 | 84 | */ |
| 89 | 85 | public function __construct() { |
| 90 | 86 | $this->namespace = 'wpcom/v2'; |
| @@ -119,8 +115,12 @@ | ||
| 119 | 115 | ), |
| 120 | 116 | 'page_handle' => array( |
| 121 | 117 | 'type' => 'string', |
| 122 | 118 | ), |
| 119 | + 'session_id' => array( | |
| 120 | + 'description' => __( 'Session id of a service, currently only Google Photos Picker', 'jetpack' ), | |
| 121 | + 'type' => 'string', | |
| 122 | + ), | |
| 123 | 123 | ), |
| 124 | 124 | ) |
| 125 | 125 | ); |
| 126 | 126 | |
| @@ -131,9 +131,9 @@ | ||
| 131 | 131 | 'methods' => \WP_REST_Server::CREATABLE, |
| 132 | 132 | 'callback' => array( $this, 'copy_external_media' ), |
| 133 | 133 | 'permission_callback' => array( $this, 'create_item_permissions_check' ), |
| 134 | 134 | 'args' => array( |
| 135 | - 'media' => array( | |
| 135 | + 'media' => array( | |
| 136 | 136 | 'description' => __( 'Media data to copy.', 'jetpack' ), |
| 137 | 137 | 'items' => $this->media_schema, |
| 138 | 138 | 'required' => true, |
| 139 | 139 | 'type' => 'array', |
| @@ -139,13 +139,18 @@ | ||
| 139 | 139 | 'type' => 'array', |
| 140 | 140 | 'sanitize_callback' => array( $this, 'sanitize_media' ), |
| 141 | 141 | 'validate_callback' => array( $this, 'validate_media' ), |
| 142 | 142 | ), |
| 143 | - 'post_id' => array( | |
| 143 | + 'post_id' => array( | |
| 144 | 144 | 'description' => __( 'The post ID to attach the upload to.', 'jetpack' ), |
| 145 | 145 | 'type' => 'number', |
| 146 | 146 | 'minimum' => 0, |
| 147 | 147 | ), |
| 148 | + 'should_proxy' => array( | |
| 149 | + 'description' => __( 'Whether to proxy the media request.', 'jetpack' ), | |
| 150 | + 'type' => 'boolean', | |
| 151 | + 'default' => false, | |
| 152 | + ), | |
| 148 | 153 | ), |
| 149 | 154 | ) |
| 150 | 155 | ); |
| 151 | 156 | |
| @@ -167,8 +172,68 @@ | ||
| 167 | 172 | 'callback' => array( $this, 'delete_connection' ), |
| 168 | 173 | 'permission_callback' => array( $this, 'permission_callback' ), |
| 169 | 174 | ) |
| 170 | 175 | ); |
| 176 | + | |
| 177 | + register_rest_route( | |
| 178 | + $this->namespace, | |
| 179 | + $this->rest_base . '/connection/(?P<service>google_photos)/picker_status', | |
| 180 | + array( | |
| 181 | + 'methods' => \WP_REST_Server::READABLE, | |
| 182 | + 'callback' => array( $this, 'get_picker_status' ), | |
| 183 | + 'permission_callback' => array( $this, 'permission_callback' ), | |
| 184 | + ) | |
| 185 | + ); | |
| 186 | + | |
| 187 | + // Add new session route, currently for Google Photos Picker only | |
| 188 | + register_rest_route( | |
| 189 | + $this->namespace, | |
| 190 | + $this->rest_base . '/session/(?P<service>google_photos)', | |
| 191 | + array( | |
| 192 | + 'methods' => \WP_REST_Server::CREATABLE, | |
| 193 | + 'callback' => array( $this, 'create_session' ), | |
| 194 | + 'permission_callback' => array( $this, 'permission_callback' ), | |
| 195 | + ) | |
| 196 | + ); | |
| 197 | + | |
| 198 | + // Get new session route, currently for Google Photos Picker only | |
| 199 | + register_rest_route( | |
| 200 | + $this->namespace, | |
| 201 | + $this->rest_base . '/session/(?P<service>google_photos)/(?P<session_id>.*)', | |
| 202 | + array( | |
| 203 | + 'methods' => \WP_REST_Server::READABLE, | |
| 204 | + 'callback' => array( $this, 'get_session' ), | |
| 205 | + 'permission_callback' => array( $this, 'permission_callback' ), | |
| 206 | + ) | |
| 207 | + ); | |
| 208 | + | |
| 209 | + // Delete session route, currently for Google Photos Picker only | |
| 210 | + register_rest_route( | |
| 211 | + $this->namespace, | |
| 212 | + $this->rest_base . '/session/(?P<service>google_photos)/(?P<session_id>.*)', | |
| 213 | + array( | |
| 214 | + 'methods' => \WP_REST_Server::DELETABLE, | |
| 215 | + 'callback' => array( $this, 'delete_session' ), | |
| 216 | + 'permission_callback' => array( $this, 'permission_callback' ), | |
| 217 | + ) | |
| 218 | + ); | |
| 219 | + | |
| 220 | + // Add new proxy route for media files | |
| 221 | + register_rest_route( | |
| 222 | + $this->namespace, | |
| 223 | + $this->rest_base . '/proxy/(?P<service>google_photos)', | |
| 224 | + array( | |
| 225 | + 'methods' => WP_REST_Server::CREATABLE, | |
| 226 | + 'callback' => array( $this, 'proxy_media_request' ), | |
| 227 | + 'permission_callback' => array( $this, 'permission_callback' ), | |
| 228 | + 'args' => array( | |
| 229 | + 'url' => array( | |
| 230 | + 'required' => true, | |
| 231 | + 'type' => 'string', | |
| 232 | + ), | |
| 233 | + ), | |
| 234 | + ) | |
| 235 | + ); | |
| 171 | 236 | } |
| 172 | 237 | |
| 173 | 238 | /** |
| 174 | 239 | * Checks if a given request has access to external media libraries. |
| @@ -173,9 +238,9 @@ | ||
| 173 | 238 | /** |
| 174 | 239 | * Checks if a given request has access to external media libraries. |
| 175 | 240 | */ |
| 176 | 241 | public function permission_callback() { |
| 177 | - return current_user_can( 'edit_posts' ); | |
| 242 | + return current_user_can( 'upload_files' ); | |
| 178 | 243 | } |
| 179 | 244 | |
| 180 | 245 | /** |
| 181 | 246 | * Checks if a given request has access to create an attachment. |
| @@ -209,8 +274,21 @@ | ||
| 209 | 274 | array( 'status' => 400 ) |
| 210 | 275 | ); |
| 211 | 276 | } |
| 212 | 277 | |
| 278 | + // Attaching media to a post requires the ability to edit that post, mirroring | |
| 279 | + // WP_REST_Attachments_Controller::create_item_permissions_check(). Without this | |
| 280 | + // check any user with upload_files could parent an attachment to a post they | |
| 281 | + // cannot edit. | |
| 282 | + $post_id = (int) $request->get_param( 'post_id' ); | |
| 283 | + if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) { | |
| 284 | + return new WP_Error( | |
| 285 | + 'rest_cannot_edit', | |
| 286 | + __( 'Sorry, you are not allowed to upload media to this post.', 'jetpack' ), | |
| 287 | + array( 'status' => rest_authorization_required_code() ) | |
| 288 | + ); | |
| 289 | + } | |
| 290 | + | |
| 213 | 291 | return true; |
| 214 | 292 | } |
| 215 | 293 | |
| 216 | 294 | /** |
| @@ -280,9 +358,9 @@ | ||
| 280 | 358 | // Build query string to pass to wpcom endpoint. |
| 281 | 359 | $service_args = array_filter( |
| 282 | 360 | $params, |
| 283 | 361 | function ( $key ) { |
| 284 | - return in_array( $key, array( 'search', 'number', 'path', 'page_handle', 'filter' ), true ); | |
| 362 | + return in_array( $key, array( 'search', 'number', 'path', 'page_handle', 'filter', 'session_id' ), true ); | |
| 285 | 363 | }, |
| 286 | 364 | ARRAY_FILTER_USE_KEY |
| 287 | 365 | ); |
| 288 | 366 | if ( ! empty( $service_args ) ) { |
| @@ -328,20 +406,60 @@ | ||
| 328 | 406 | * Saves an external media item to the media library. |
| 329 | 407 | * |
| 330 | 408 | * @param \WP_REST_Request $request Full details about the request. |
| 331 | 409 | * @return array|\WP_Error|mixed |
| 332 | - */ | |
| 410 | + **/ | |
| 333 | 411 | public function copy_external_media( \WP_REST_Request $request ) { |
| 334 | 412 | require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 335 | 413 | require_once ABSPATH . 'wp-admin/includes/media.php'; |
| 336 | 414 | require_once ABSPATH . 'wp-admin/includes/image.php'; |
| 337 | 415 | |
| 338 | - $post_id = $request->get_param( 'post_id' ); | |
| 416 | + $post_id = (int) $request->get_param( 'post_id' ); | |
| 417 | + $should_proxy = $request->get_param( 'should_proxy' ); | |
| 418 | + $service = rawurlencode( $request->get_param( 'service' ) ); | |
| 339 | 419 | |
| 420 | + // Fail closed: never parent an attachment to a post the caller cannot edit, | |
| 421 | + // even if a future change lets an unauthorized request reach this handler. | |
| 422 | + // The permission callback already rejects such requests with a 403. | |
| 423 | + if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) { | |
| 424 | + $post_id = 0; | |
| 425 | + } | |
| 426 | + | |
| 340 | 427 | $responses = array(); |
| 428 | + | |
| 341 | 429 | foreach ( $request->get_param( 'media' ) as $item ) { |
| 342 | 430 | // Download file to temp dir. |
| 343 | - $download_url = $this->get_download_url( $item['guid'] ); | |
| 431 | + if ( $should_proxy ) { | |
| 432 | + $wpcom_path = sprintf( '/meta/external-media/proxy/%s', $service ); | |
| 433 | + $wpcom_path .= '?url=' . rawurlencode( $item['guid']['url'] ); | |
| 434 | + $download_url = wp_tempnam(); | |
| 435 | + $response = Client::wpcom_json_api_request_as_user( | |
| 436 | + $wpcom_path, | |
| 437 | + '2', | |
| 438 | + array( | |
| 439 | + 'method' => 'POST', | |
| 440 | + ) | |
| 441 | + ); | |
| 442 | + | |
| 443 | + if ( is_wp_error( $response ) ) { | |
| 444 | + $responses[] = $response; | |
| 445 | + continue; | |
| 446 | + } | |
| 447 | + $wp_filesystem = $this->get_wp_filesystem(); | |
| 448 | + $written = $wp_filesystem->put_contents( $download_url, wp_remote_retrieve_body( $response ) ); | |
| 449 | + | |
| 450 | + if ( false === $written ) { | |
| 451 | + $responses[] = new WP_Error( | |
| 452 | + 'rest_upload_error', | |
| 453 | + __( 'Could not download media file.', 'jetpack' ), | |
| 454 | + array( 'status' => 400 ) | |
| 455 | + ); | |
| 456 | + continue; | |
| 457 | + } | |
| 458 | + } else { | |
| 459 | + $download_url = $this->get_download_url( $item['guid'] ); | |
| 460 | + } | |
| 461 | + | |
| 344 | 462 | if ( is_wp_error( $download_url ) ) { |
| 345 | 463 | $responses[] = $download_url; |
| 346 | 464 | continue; |
| 347 | 465 | } |
| @@ -367,12 +485,12 @@ | ||
| 367 | 485 | * @param \WP_REST_Request $request Full details about the request. |
| 368 | 486 | * @return array|\WP_Error|mixed |
| 369 | 487 | */ |
| 370 | 488 | public function get_connection_details( \WP_REST_Request $request ) { |
| 371 | - $service = rawurlencode( $request->get_param( 'service' ) ); | |
| 372 | - $wpcom_path = sprintf( '/meta/external-media/connection/%s', $service ); | |
| 489 | + $service = $request->get_param( 'service' ); | |
| 373 | 490 | |
| 374 | 491 | if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { |
| 492 | + $wpcom_path = sprintf( '/meta/external-media/connection/%s', rawurlencode( $service ) ); | |
| 375 | 493 | $internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path ); |
| 376 | 494 | $internal_request->set_query_params( $request->get_params() ); |
| 377 | 495 | |
| 378 | 496 | return rest_do_request( $internal_request ); |
| @@ -377,11 +495,29 @@ | ||
| 377 | 495 | |
| 378 | 496 | return rest_do_request( $internal_request ); |
| 379 | 497 | } |
| 380 | 498 | |
| 381 | - $response = Client::wpcom_json_api_request_as_user( $wpcom_path ); | |
| 499 | + $site_id = Manager::get_site_id(); | |
| 500 | + if ( is_wp_error( $site_id ) ) { | |
| 501 | + return $site_id; | |
| 502 | + } | |
| 382 | 503 | |
| 383 | - return json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 504 | + $path = sprintf( '/sites/%d/external-services', $site_id ); | |
| 505 | + $response = Client::wpcom_json_api_request_as_user( $path ); | |
| 506 | + if ( is_wp_error( $response ) ) { | |
| 507 | + return $response; | |
| 508 | + } | |
| 509 | + | |
| 510 | + $body = json_decode( wp_remote_retrieve_body( $response ) ); | |
| 511 | + if ( ! property_exists( $body, 'services' ) || ! property_exists( $body->services, $service ) ) { | |
| 512 | + return new WP_Error( | |
| 513 | + 'bad_request', | |
| 514 | + __( 'An error occurred. Please try again later.', 'jetpack' ), | |
| 515 | + array( 'status' => 400 ) | |
| 516 | + ); | |
| 517 | + } | |
| 518 | + | |
| 519 | + return $body->services->{ $service }; | |
| 384 | 520 | } |
| 385 | 521 | |
| 386 | 522 | /** |
| 387 | 523 | * Deletes a Google Photos connection. |
| @@ -389,20 +525,41 @@ | ||
| 389 | 525 | * @param WP_REST_Request $request Full details about the request. |
| 390 | 526 | * @return array|WP_Error|WP_REST_Response |
| 391 | 527 | */ |
| 392 | 528 | public function delete_connection( WP_REST_Request $request ) { |
| 393 | - global $wp_version; | |
| 394 | - | |
| 395 | 529 | $service = rawurlencode( $request->get_param( 'service' ) ); |
| 396 | 530 | $wpcom_path = sprintf( '/meta/external-media/connection/%s', $service ); |
| 397 | 531 | |
| 398 | - // Remove this check once WordPress 6.2 is the minimum supported version. | |
| 399 | - $delete_request = version_compare( $wp_version, '6.2-alpha', '<' ) | |
| 400 | - ? Requests::DELETE | |
| 401 | - : \WpOrg\Requests\Requests::DELETE; | |
| 532 | + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { | |
| 533 | + $internal_request = new WP_REST_Request( 'DELETE', '/' . $this->namespace . $wpcom_path ); | |
| 534 | + $internal_request->set_query_params( $request->get_params() ); | |
| 402 | 535 | |
| 536 | + return rest_do_request( $internal_request ); | |
| 537 | + } | |
| 538 | + | |
| 539 | + $response = Client::wpcom_json_api_request_as_user( | |
| 540 | + $wpcom_path, | |
| 541 | + '2', | |
| 542 | + array( | |
| 543 | + 'method' => 'DELETE', | |
| 544 | + ) | |
| 545 | + ); | |
| 546 | + | |
| 547 | + return json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 548 | + } | |
| 549 | + | |
| 550 | + /** | |
| 551 | + * Gets Google Photos Picker enabled Status. | |
| 552 | + * | |
| 553 | + * @param \WP_REST_Request $request Full details about the request. | |
| 554 | + * @return array|\WP_Error|mixed | |
| 555 | + */ | |
| 556 | + public function get_picker_status( \WP_REST_Request $request ) { | |
| 557 | + $service = $request->get_param( 'service' ); | |
| 558 | + $wpcom_path = sprintf( '/meta/external-media/connection/%s/picker_status', rawurlencode( $service ) ); | |
| 559 | + | |
| 403 | 560 | if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { |
| 404 | - $internal_request = new WP_REST_Request( $delete_request, '/' . $this->namespace . $wpcom_path ); | |
| 561 | + $internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path ); | |
| 405 | 562 | $internal_request->set_query_params( $request->get_params() ); |
| 406 | 563 | |
| 407 | 564 | return rest_do_request( $internal_request ); |
| 408 | 565 | } |
| @@ -410,9 +567,9 @@ | ||
| 410 | 567 | $response = Client::wpcom_json_api_request_as_user( |
| 411 | 568 | $wpcom_path, |
| 412 | 569 | '2', |
| 413 | 570 | array( |
| 414 | - 'method' => $delete_request, | |
| 571 | + 'method' => 'GET', | |
| 415 | 572 | ) |
| 416 | 573 | ); |
| 417 | 574 | |
| 418 | 575 | return json_decode( wp_remote_retrieve_body( $response ), true ); |
| @@ -418,33 +575,215 @@ | ||
| 418 | 575 | return json_decode( wp_remote_retrieve_body( $response ), true ); |
| 419 | 576 | } |
| 420 | 577 | |
| 421 | 578 | /** |
| 422 | - * Filter callback to provide a shorter file name for google images. | |
| 579 | + * Creates a new session for a service. | |
| 423 | 580 | * |
| 424 | - * @return string | |
| 581 | + * @param \WP_REST_Request $request Full details about the request. | |
| 582 | + * @return array|\WP_Error|mixed | |
| 425 | 583 | */ |
| 426 | - public function tmp_name() { | |
| 427 | - return $this->tmp_name; | |
| 584 | + public function create_session( \WP_REST_Request $request ) { | |
| 585 | + $service = $request->get_param( 'service' ); | |
| 586 | + $wpcom_path = sprintf( '/meta/external-media/session/%s', rawurlencode( $service ) ); | |
| 587 | + | |
| 588 | + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { | |
| 589 | + $internal_request = new \WP_REST_Request( 'POST', '/' . $this->namespace . $wpcom_path ); | |
| 590 | + $internal_request->set_query_params( $request->get_params() ); | |
| 591 | + | |
| 592 | + return rest_do_request( $internal_request ); | |
| 593 | + } | |
| 594 | + | |
| 595 | + $response = Client::wpcom_json_api_request_as_user( | |
| 596 | + $wpcom_path, | |
| 597 | + '2', | |
| 598 | + array( | |
| 599 | + 'method' => 'POST', | |
| 600 | + ) | |
| 601 | + ); | |
| 602 | + | |
| 603 | + return json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 428 | 604 | } |
| 429 | 605 | |
| 430 | 606 | /** |
| 431 | - * Returns a download URL, dealing with Google's long file names. | |
| 607 | + * Gets a session for a service. | |
| 432 | 608 | * |
| 609 | + * @param \WP_REST_Request $request Full details about the request. | |
| 610 | + * @return array|\WP_Error|mixed | |
| 611 | + */ | |
| 612 | + public function get_session( \WP_REST_Request $request ) { | |
| 613 | + $service = $request->get_param( 'service' ); | |
| 614 | + $session_id = $request->get_param( 'session_id' ); | |
| 615 | + $wpcom_path = sprintf( '/meta/external-media/session/%s/%s', rawurlencode( $service ), rawurlencode( $session_id ) ); | |
| 616 | + | |
| 617 | + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { | |
| 618 | + $internal_request = new \WP_REST_Request( 'GET', '/' . $this->namespace . $wpcom_path ); | |
| 619 | + $internal_request->set_query_params( $request->get_params() ); | |
| 620 | + | |
| 621 | + return rest_do_request( $internal_request ); | |
| 622 | + } | |
| 623 | + | |
| 624 | + $response = Client::wpcom_json_api_request_as_user( | |
| 625 | + $wpcom_path, | |
| 626 | + '2', | |
| 627 | + array( | |
| 628 | + 'method' => 'GET', | |
| 629 | + ) | |
| 630 | + ); | |
| 631 | + | |
| 632 | + return json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 633 | + } | |
| 634 | + | |
| 635 | + /** | |
| 636 | + * Deletes a session for a service. | |
| 637 | + * | |
| 638 | + * @param \WP_REST_Request $request Full details about the request. | |
| 639 | + * @return array|\WP_Error|mixed | |
| 640 | + */ | |
| 641 | + public function delete_session( \WP_REST_Request $request ) { | |
| 642 | + $service = $request->get_param( 'service' ); | |
| 643 | + $session_id = $request->get_param( 'session_id' ); | |
| 644 | + $wpcom_path = sprintf( '/meta/external-media/session/%s/%s', rawurlencode( $service ), rawurlencode( $session_id ) ); | |
| 645 | + | |
| 646 | + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { | |
| 647 | + $internal_request = new \WP_REST_Request( 'DELETE', '/' . $this->namespace . $wpcom_path ); | |
| 648 | + $internal_request->set_query_params( $request->get_params() ); | |
| 649 | + | |
| 650 | + return rest_do_request( $internal_request ); | |
| 651 | + } | |
| 652 | + | |
| 653 | + $response = Client::wpcom_json_api_request_as_user( | |
| 654 | + $wpcom_path, | |
| 655 | + '2', | |
| 656 | + array( | |
| 657 | + 'method' => 'DELETE', | |
| 658 | + ) | |
| 659 | + ); | |
| 660 | + | |
| 661 | + return json_decode( wp_remote_retrieve_body( $response ), true ); | |
| 662 | + } | |
| 663 | + | |
| 664 | + /** | |
| 665 | + * Proxies media requests with proper authorization headers | |
| 666 | + * | |
| 667 | + * @param WP_REST_Request $request Full details about the request. | |
| 668 | + * @return WP_REST_Response|WP_Error|array Response object or WP_Error. | |
| 669 | + */ | |
| 670 | + public function proxy_media_request( $request ) { | |
| 671 | + $params = $request->get_params(); | |
| 672 | + $service = rawurlencode( $request->get_param( 'service' ) ); | |
| 673 | + $wpcom_path = sprintf( '/meta/external-media/proxy/%s', $service ); | |
| 674 | + | |
| 675 | + if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) { | |
| 676 | + $request = new \WP_REST_Request( 'POST', '/' . $this->namespace . $wpcom_path ); | |
| 677 | + $request->set_query_params( $params ); | |
| 678 | + | |
| 679 | + return rest_do_request( $request ); | |
| 680 | + | |
| 681 | + } else { | |
| 682 | + // Build query string to pass to wpcom endpoint. | |
| 683 | + $service_args = array_filter( | |
| 684 | + $params, | |
| 685 | + function ( $key ) { | |
| 686 | + return in_array( $key, array( 'url' ), true ); | |
| 687 | + }, | |
| 688 | + ARRAY_FILTER_USE_KEY | |
| 689 | + ); | |
| 690 | + | |
| 691 | + if ( ! empty( $service_args ) ) { | |
| 692 | + $wpcom_path .= '?' . http_build_query( $service_args ); | |
| 693 | + } | |
| 694 | + | |
| 695 | + $response = Client::wpcom_json_api_request_as_user( | |
| 696 | + $wpcom_path, | |
| 697 | + '2', | |
| 698 | + array( | |
| 699 | + 'method' => 'POST', | |
| 700 | + ) | |
| 701 | + ); | |
| 702 | + | |
| 703 | + $status_code = wp_remote_retrieve_response_code( $response ); | |
| 704 | + $headers = wp_remote_retrieve_headers( $response ); | |
| 705 | + $body = wp_remote_retrieve_body( $response ); | |
| 706 | + | |
| 707 | + // For non-200 responses, parse and return JSON error | |
| 708 | + if ( $status_code !== 200 ) { | |
| 709 | + $error_data = json_decode( $body, true ); | |
| 710 | + return new \WP_REST_Response( $error_data, $status_code ); | |
| 711 | + } | |
| 712 | + } | |
| 713 | + | |
| 714 | + // Return binary content directly | |
| 715 | + $valid_headers = array( | |
| 716 | + 'content-type', | |
| 717 | + 'content-length', | |
| 718 | + 'content-disposition', | |
| 719 | + ); | |
| 720 | + // Set content headers | |
| 721 | + foreach ( $valid_headers as $header ) { | |
| 722 | + if ( ! empty( $headers[ $header ] ) ) { | |
| 723 | + header( ucwords( $header, '-' ) . ': ' . $headers[ $header ] ); | |
| 724 | + } | |
| 725 | + } | |
| 726 | + | |
| 727 | + // Set cache headers | |
| 728 | + header( 'Cache-Control: no-cache, no-store, must-revalidate' ); | |
| 729 | + header( 'Pragma: no-cache' ); | |
| 730 | + header( 'Expires: 0' ); | |
| 731 | + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Media binary data | |
| 732 | + echo $body; | |
| 733 | + exit( 0 ); | |
| 734 | + } | |
| 735 | + | |
| 736 | + /** | |
| 737 | + * Downloads a remote media file into a temporary file for sideloading. | |
| 738 | + * | |
| 739 | + * The remote file is streamed into a randomly-named temporary file created by | |
| 740 | + * wp_tempnam(). The caller-supplied name is never used for the temporary file | |
| 741 | + * itself; it is only applied — and validated by WordPress — later, when the | |
| 742 | + * completed download is handed to media_handle_sideload(). This prevents a | |
| 743 | + * crafted name from controlling the physical path or extension of the file | |
| 744 | + * written to disk. | |
| 745 | + * | |
| 433 | 746 | * @param array $guid Media information. |
| 434 | - * @return string|\WP_Error | |
| 747 | + * @return string|\WP_Error Path to the downloaded temporary file, or WP_Error on failure. | |
| 435 | 748 | */ |
| 436 | 749 | public function get_download_url( $guid ) { |
| 437 | - $this->tmp_name = $guid['name']; | |
| 438 | - add_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) ); | |
| 439 | - $download_url = download_url( $guid['url'] ); | |
| 440 | - remove_filter( 'wp_unique_filename', array( $this, 'tmp_name' ) ); | |
| 750 | + require_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 441 | 751 | |
| 442 | - if ( is_wp_error( $download_url ) ) { | |
| 443 | - $download_url->add_data( array( 'status' => 400 ) ); | |
| 752 | + $tmp_name = wp_tempnam(); | |
| 753 | + if ( ! $tmp_name ) { | |
| 754 | + return new WP_Error( | |
| 755 | + 'rest_upload_error', | |
| 756 | + __( 'Could not create a temporary file.', 'jetpack' ), | |
| 757 | + array( 'status' => 500 ) | |
| 758 | + ); | |
| 444 | 759 | } |
| 445 | 760 | |
| 446 | - return $download_url; | |
| 761 | + $response = wp_safe_remote_get( | |
| 762 | + $guid['url'], | |
| 763 | + array( | |
| 764 | + 'timeout' => 300, | |
| 765 | + 'stream' => true, | |
| 766 | + 'filename' => $tmp_name, | |
| 767 | + ) | |
| 768 | + ); | |
| 769 | + | |
| 770 | + if ( is_wp_error( $response ) ) { | |
| 771 | + wp_delete_file( $tmp_name ); | |
| 772 | + $response->add_data( array( 'status' => 400 ) ); | |
| 773 | + return $response; | |
| 774 | + } | |
| 775 | + | |
| 776 | + if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { | |
| 777 | + wp_delete_file( $tmp_name ); | |
| 778 | + return new WP_Error( | |
| 779 | + 'rest_upload_error', | |
| 780 | + __( 'Could not download the media file.', 'jetpack' ), | |
| 781 | + array( 'status' => 400 ) | |
| 782 | + ); | |
| 783 | + } | |
| 784 | + | |
| 785 | + return $tmp_name; | |
| 447 | 786 | } |
| 448 | 787 | |
| 449 | 788 | /** |
| 450 | 789 | * Uploads media file and creates attachment object. |
| @@ -456,9 +795,9 @@ | ||
| 456 | 795 | * @return int|\WP_Error |
| 457 | 796 | */ |
| 458 | 797 | public function sideload_media( $file_name, $download_url, $post_id = 0 ) { |
| 459 | 798 | $file = array( |
| 460 | - 'name' => wp_basename( $file_name ), | |
| 799 | + 'name' => sanitize_file_name( wp_basename( $file_name ) ), | |
| 461 | 800 | 'tmp_name' => $download_url, |
| 462 | 801 | ); |
| 463 | 802 | |
| 464 | 803 | $id = media_handle_sideload( $file, $post_id, null ); |
| @@ -525,8 +864,24 @@ | ||
| 525 | 864 | ); |
| 526 | 865 | } |
| 527 | 866 | |
| 528 | 867 | return $response; |
| 868 | + } | |
| 869 | + | |
| 870 | + /** | |
| 871 | + * Get the wp filesystem. | |
| 872 | + * | |
| 873 | + * @return \WP_Filesystem_Base|null | |
| 874 | + */ | |
| 875 | + private function get_wp_filesystem() { | |
| 876 | + global $wp_filesystem; | |
| 877 | + | |
| 878 | + if ( ! isset( $wp_filesystem ) ) { | |
| 879 | + require_once ABSPATH . '/wp-admin/includes/file.php'; | |
| 880 | + WP_Filesystem(); | |
| 881 | + } | |
| 882 | + | |
| 883 | + return $wp_filesystem; | |
| 529 | 884 | } |
| 530 | 885 | } |
| 531 | 886 | |
| 532 | 887 | wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_External_Media' ); |