PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 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 All 508 releases
← 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.