← All changes
|
_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-external-media.php
+18
-130
16.3
→
16.3-beta
View file →
| @@ -7,9 +7,8 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | |
| 9 | 9 | use Automattic\Jetpack\Connection\Client; |
| 10 | 10 | use Automattic\Jetpack\Connection\Manager; |
| 11 | -use Automattic\Jetpack\IP\Utils; | |
| 12 | 11 | |
| 13 | 12 | if ( ! defined( 'ABSPATH' ) ) { |
| 14 | 13 | exit( 0 ); |
| 15 | 14 | } |
| @@ -21,25 +20,8 @@ | ||
| 21 | 20 | */ |
| 22 | 21 | class WPCOM_REST_API_V2_Endpoint_External_Media extends WP_REST_Controller { |
| 23 | 22 | |
| 24 | 23 | /** |
| 25 | - * Maximum number of redirect hops to follow when downloading a media file. | |
| 26 | - * | |
| 27 | - * Matches WordPress's default `redirection` limit, so media URLs that redirect | |
| 28 | - * to a CDN keep resolving exactly as before. | |
| 29 | - * | |
| 30 | - * @var int | |
| 31 | - */ | |
| 32 | - const MAX_REDIRECTS = 5; | |
| 33 | - | |
| 34 | - /** | |
| 35 | - * Seconds a media download may take, redirects included. | |
| 36 | - * | |
| 37 | - * @var int | |
| 38 | - */ | |
| 39 | - const DOWNLOAD_TIMEOUT = 300; | |
| 40 | - | |
| 41 | - /** | |
| 42 | 24 | * Media argument schema for /copy endpoint. |
| 43 | 25 | * |
| 44 | 26 | * @var array |
| 45 | 27 | */ |
| @@ -753,11 +735,8 @@ | ||
| 753 | 735 | |
| 754 | 736 | /** |
| 755 | 737 | * Downloads a remote media file into a temporary file for sideloading. |
| 756 | 738 | * |
| 757 | - * The URL is checked against Utils::url_is_public() before the fetch and again | |
| 758 | - * on every redirect hop, the same rule the resolve-redirect endpoint applies. | |
| 759 | - * | |
| 760 | 739 | * The remote file is streamed into a randomly-named temporary file created by |
| 761 | 740 | * wp_tempnam(). The caller-supplied name is never used for the temporary file |
| 762 | 741 | * itself; it is only applied — and validated by WordPress — later, when the |
| 763 | 742 | * completed download is handed to media_handle_sideload(). This prevents a |
| @@ -769,14 +748,8 @@ | ||
| 769 | 748 | */ |
| 770 | 749 | public function get_download_url( $guid ) { |
| 771 | 750 | require_once ABSPATH . 'wp-admin/includes/file.php'; |
| 772 | 751 | |
| 773 | - $url = isset( $guid['url'] ) && is_string( $guid['url'] ) ? $guid['url'] : ''; | |
| 774 | - | |
| 775 | - if ( ! $this->url_is_public( $url ) ) { | |
| 776 | - return $this->download_failed_error(); | |
| 777 | - } | |
| 778 | - | |
| 779 | 752 | $tmp_name = wp_tempnam(); |
| 780 | 753 | if ( ! $tmp_name ) { |
| 781 | 754 | return new WP_Error( |
| 782 | 755 | 'rest_upload_error', |
| @@ -784,118 +757,33 @@ | ||
| 784 | 757 | array( 'status' => 500 ) |
| 785 | 758 | ); |
| 786 | 759 | } |
| 787 | 760 | |
| 788 | - $result = $this->stream_to_temp_file( $url, $tmp_name ); | |
| 761 | + $response = wp_safe_remote_get( | |
| 762 | + $guid['url'], | |
| 763 | + array( | |
| 764 | + 'timeout' => 300, | |
| 765 | + 'stream' => true, | |
| 766 | + 'filename' => $tmp_name, | |
| 767 | + ) | |
| 768 | + ); | |
| 789 | 769 | |
| 790 | - if ( is_wp_error( $result ) ) { | |
| 770 | + if ( is_wp_error( $response ) ) { | |
| 791 | 771 | wp_delete_file( $tmp_name ); |
| 772 | + $response->add_data( array( 'status' => 400 ) ); | |
| 773 | + return $response; | |
| 792 | 774 | } |
| 793 | 775 | |
| 794 | - return $result; | |
| 795 | - } | |
| 796 | - | |
| 797 | - /** | |
| 798 | - * Streams an already-validated URL into a temporary file, following redirects. | |
| 799 | - * | |
| 800 | - * Each hop is fetched with `redirection => 0` and re-checked with | |
| 801 | - * Utils::url_is_public() before the next request. The caller owns $tmp_name and | |
| 802 | - * deletes it when this returns an error. | |
| 803 | - * | |
| 804 | - * @param string $url Validated URL to download. | |
| 805 | - * @param string $tmp_name Path of the temporary file to stream into. | |
| 806 | - * @return string|\WP_Error $tmp_name on success, WP_Error on failure. | |
| 807 | - */ | |
| 808 | - private function stream_to_temp_file( $url, $tmp_name ) { | |
| 809 | - // One budget for the whole chain: WordPress used to apply the timeout across | |
| 810 | - // the redirects it followed itself, and following them here must not multiply | |
| 811 | - // how long a single import can hold a request open. | |
| 812 | - $deadline = microtime( true ) + self::DOWNLOAD_TIMEOUT; | |
| 813 | - | |
| 814 | - for ( $hop = 0; $hop <= self::MAX_REDIRECTS; $hop++ ) { | |
| 815 | - $remaining = (int) ceil( $deadline - microtime( true ) ); | |
| 816 | - if ( $remaining < 1 ) { | |
| 817 | - return $this->download_failed_error(); | |
| 818 | - } | |
| 819 | - | |
| 820 | - $response = wp_safe_remote_get( | |
| 821 | - $url, | |
| 822 | - array( | |
| 823 | - 'timeout' => $remaining, | |
| 824 | - 'stream' => true, | |
| 825 | - 'filename' => $tmp_name, | |
| 826 | - // Do not let WordPress follow redirects for us; we validate each hop first. | |
| 827 | - 'redirection' => 0, | |
| 828 | - ) | |
| 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 ) | |
| 829 | 782 | ); |
| 830 | - | |
| 831 | - if ( is_wp_error( $response ) ) { | |
| 832 | - return $this->download_failed_error(); | |
| 833 | - } | |
| 834 | - | |
| 835 | - $status = (int) wp_remote_retrieve_response_code( $response ); | |
| 836 | - | |
| 837 | - if ( $status < 300 || $status >= 400 ) { | |
| 838 | - return 200 === $status ? $tmp_name : $this->download_failed_error(); | |
| 839 | - } | |
| 840 | - | |
| 841 | - // Budget exhausted: stop before validating a destination we will never fetch. | |
| 842 | - if ( self::MAX_REDIRECTS === $hop ) { | |
| 843 | - break; | |
| 844 | - } | |
| 845 | - | |
| 846 | - $location = wp_remote_retrieve_header( $response, 'location' ); | |
| 847 | - | |
| 848 | - // Multiple Location headers: follow the last, as core does. | |
| 849 | - if ( is_array( $location ) ) { | |
| 850 | - $location = end( $location ); | |
| 851 | - } | |
| 852 | - | |
| 853 | - // Location may be relative; resolve it against the current URL. | |
| 854 | - $next_url = is_string( $location ) && '' !== $location | |
| 855 | - ? WP_Http::make_absolute_url( $location, $url ) | |
| 856 | - : ''; | |
| 857 | - | |
| 858 | - if ( ! is_string( $next_url ) || ! $this->url_is_public( $next_url ) ) { | |
| 859 | - return $this->download_failed_error(); | |
| 860 | - } | |
| 861 | - | |
| 862 | - $url = $next_url; | |
| 863 | 783 | } |
| 864 | 784 | |
| 865 | - return $this->download_failed_error(); | |
| 866 | - } | |
| 867 | - | |
| 868 | - /** | |
| 869 | - * Checks whether a URL is a public destination for a media download. | |
| 870 | - * | |
| 871 | - * An older jetpack-ip without url_is_public() may win the autoloader; that case | |
| 872 | - * falls back to core's check, the same one wp_safe_remote_get() applies. | |
| 873 | - * | |
| 874 | - * @param string $url URL to check. | |
| 875 | - * @return bool | |
| 876 | - */ | |
| 877 | - private function url_is_public( $url ) { | |
| 878 | - if ( method_exists( Utils::class, 'url_is_public' ) ) { | |
| 879 | - return Utils::url_is_public( $url ); | |
| 880 | - } | |
| 881 | - | |
| 882 | - return (bool) wp_http_validate_url( $url ); | |
| 883 | - } | |
| 884 | - | |
| 885 | - /** | |
| 886 | - * Builds the WP_Error returned when a media file cannot be downloaded. | |
| 887 | - * | |
| 888 | - * Every failed download shares this one generic error. | |
| 889 | - * | |
| 890 | - * @return WP_Error | |
| 891 | - */ | |
| 892 | - private function download_failed_error() { | |
| 893 | - return new WP_Error( | |
| 894 | - 'rest_upload_error', | |
| 895 | - __( 'Could not download the media file.', 'jetpack' ), | |
| 896 | - array( 'status' => 400 ) | |
| 897 | - ); | |
| 785 | + return $tmp_name; | |
| 898 | 786 | } |
| 899 | 787 | |
| 900 | 788 | /** |
| 901 | 789 | * Uploads media file and creates attachment object. |