wp/wp-includes/class-wp-http-curl.php
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     1 <?php
       
     2 /**
       
     3  * HTTP API: WP_Http_Curl class
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage HTTP
       
     7  * @since 4.4.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Core class used to integrate Curl as an HTTP transport.
       
    12  *
       
    13  * HTTP request method uses Curl extension to retrieve the url.
       
    14  *
       
    15  * Requires the Curl extension to be installed.
       
    16  *
       
    17  * @since 2.7.0
       
    18  */
       
    19 class WP_Http_Curl {
       
    20 
       
    21 	/**
       
    22 	 * Temporary header storage for during requests.
       
    23 	 *
       
    24 	 * @since 3.2.0
       
    25 	 * @var string
       
    26 	 */
       
    27 	private $headers = '';
       
    28 
       
    29 	/**
       
    30 	 * Temporary body storage for during requests.
       
    31 	 *
       
    32 	 * @since 3.6.0
       
    33 	 * @var string
       
    34 	 */
       
    35 	private $body = '';
       
    36 
       
    37 	/**
       
    38 	 * The maximum amount of data to receive from the remote server.
       
    39 	 *
       
    40 	 * @since 3.6.0
       
    41 	 * @var int
       
    42 	 */
       
    43 	private $max_body_length = false;
       
    44 
       
    45 	/**
       
    46 	 * The file resource used for streaming to file.
       
    47 	 *
       
    48 	 * @since 3.6.0
       
    49 	 * @var resource
       
    50 	 */
       
    51 	private $stream_handle = false;
       
    52 
       
    53 	/**
       
    54 	 * The total bytes written in the current request.
       
    55 	 *
       
    56 	 * @since 4.1.0
       
    57 	 * @var int
       
    58 	 */
       
    59 	private $bytes_written_total = 0;
       
    60 
       
    61 	/**
       
    62 	 * Send a HTTP request to a URI using cURL extension.
       
    63 	 *
       
    64 	 * @since 2.7.0
       
    65 	 *
       
    66 	 * @param string $url The request URL.
       
    67 	 * @param string|array $args Optional. Override the defaults.
       
    68 	 * @return array|WP_Error Array containing 'headers', 'body', 'response', 'cookies', 'filename'. A WP_Error instance upon error
       
    69 	 */
       
    70 	public function request($url, $args = array()) {
       
    71 		$defaults = array(
       
    72 			'method' => 'GET', 'timeout' => 5,
       
    73 			'redirection' => 5, 'httpversion' => '1.0',
       
    74 			'blocking' => true,
       
    75 			'headers' => array(), 'body' => null, 'cookies' => array()
       
    76 		);
       
    77 
       
    78 		$r = wp_parse_args( $args, $defaults );
       
    79 
       
    80 		if ( isset( $r['headers']['User-Agent'] ) ) {
       
    81 			$r['user-agent'] = $r['headers']['User-Agent'];
       
    82 			unset( $r['headers']['User-Agent'] );
       
    83 		} elseif ( isset( $r['headers']['user-agent'] ) ) {
       
    84 			$r['user-agent'] = $r['headers']['user-agent'];
       
    85 			unset( $r['headers']['user-agent'] );
       
    86 		}
       
    87 
       
    88 		// Construct Cookie: header if any cookies are set.
       
    89 		WP_Http::buildCookieHeader( $r );
       
    90 
       
    91 		$handle = curl_init();
       
    92 
       
    93 		// cURL offers really easy proxy support.
       
    94 		$proxy = new WP_HTTP_Proxy();
       
    95 
       
    96 		if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) {
       
    97 
       
    98 			curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP );
       
    99 			curl_setopt( $handle, CURLOPT_PROXY, $proxy->host() );
       
   100 			curl_setopt( $handle, CURLOPT_PROXYPORT, $proxy->port() );
       
   101 
       
   102 			if ( $proxy->use_authentication() ) {
       
   103 				curl_setopt( $handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY );
       
   104 				curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy->authentication() );
       
   105 			}
       
   106 		}
       
   107 
       
   108 		$is_local = isset($r['local']) && $r['local'];
       
   109 		$ssl_verify = isset($r['sslverify']) && $r['sslverify'];
       
   110 		if ( $is_local ) {
       
   111 			/** This filter is documented in wp-includes/class-wp-http-streams.php */
       
   112 			$ssl_verify = apply_filters( 'https_local_ssl_verify', $ssl_verify );
       
   113 		} elseif ( ! $is_local ) {
       
   114 			/** This filter is documented in wp-includes/class-wp-http-streams.php */
       
   115 			$ssl_verify = apply_filters( 'https_ssl_verify', $ssl_verify );
       
   116 		}
       
   117 
       
   118 		/*
       
   119 		 * CURLOPT_TIMEOUT and CURLOPT_CONNECTTIMEOUT expect integers. Have to use ceil since.
       
   120 		 * a value of 0 will allow an unlimited timeout.
       
   121 		 */
       
   122 		$timeout = (int) ceil( $r['timeout'] );
       
   123 		curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, $timeout );
       
   124 		curl_setopt( $handle, CURLOPT_TIMEOUT, $timeout );
       
   125 
       
   126 		curl_setopt( $handle, CURLOPT_URL, $url);
       
   127 		curl_setopt( $handle, CURLOPT_RETURNTRANSFER, true );
       
   128 		curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, ( $ssl_verify === true ) ? 2 : false );
       
   129 		curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, $ssl_verify );
       
   130 
       
   131 		if ( $ssl_verify ) {
       
   132 			curl_setopt( $handle, CURLOPT_CAINFO, $r['sslcertificates'] );
       
   133 		}
       
   134 
       
   135 		curl_setopt( $handle, CURLOPT_USERAGENT, $r['user-agent'] );
       
   136 
       
   137 		/*
       
   138 		 * The option doesn't work with safe mode or when open_basedir is set, and there's
       
   139 		 * a bug #17490 with redirected POST requests, so handle redirections outside Curl.
       
   140 		 */
       
   141 		curl_setopt( $handle, CURLOPT_FOLLOWLOCATION, false );
       
   142 		if ( defined( 'CURLOPT_PROTOCOLS' ) ) // PHP 5.2.10 / cURL 7.19.4
       
   143 			curl_setopt( $handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS );
       
   144 
       
   145 		switch ( $r['method'] ) {
       
   146 			case 'HEAD':
       
   147 				curl_setopt( $handle, CURLOPT_NOBODY, true );
       
   148 				break;
       
   149 			case 'POST':
       
   150 				curl_setopt( $handle, CURLOPT_POST, true );
       
   151 				curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] );
       
   152 				break;
       
   153 			case 'PUT':
       
   154 				curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, 'PUT' );
       
   155 				curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] );
       
   156 				break;
       
   157 			default:
       
   158 				curl_setopt( $handle, CURLOPT_CUSTOMREQUEST, $r['method'] );
       
   159 				if ( ! is_null( $r['body'] ) )
       
   160 					curl_setopt( $handle, CURLOPT_POSTFIELDS, $r['body'] );
       
   161 				break;
       
   162 		}
       
   163 
       
   164 		if ( true === $r['blocking'] ) {
       
   165 			curl_setopt( $handle, CURLOPT_HEADERFUNCTION, array( $this, 'stream_headers' ) );
       
   166 			curl_setopt( $handle, CURLOPT_WRITEFUNCTION, array( $this, 'stream_body' ) );
       
   167 		}
       
   168 
       
   169 		curl_setopt( $handle, CURLOPT_HEADER, false );
       
   170 
       
   171 		if ( isset( $r['limit_response_size'] ) )
       
   172 			$this->max_body_length = intval( $r['limit_response_size'] );
       
   173 		else
       
   174 			$this->max_body_length = false;
       
   175 
       
   176 		// If streaming to a file open a file handle, and setup our curl streaming handler.
       
   177 		if ( $r['stream'] ) {
       
   178 			if ( ! WP_DEBUG )
       
   179 				$this->stream_handle = @fopen( $r['filename'], 'w+' );
       
   180 			else
       
   181 				$this->stream_handle = fopen( $r['filename'], 'w+' );
       
   182 			if ( ! $this->stream_handle ) {
       
   183 				return new WP_Error( 'http_request_failed', sprintf(
       
   184 					/* translators: 1: fopen() 2: file name */
       
   185 					__( 'Could not open handle for %1$s to %2$s.' ),
       
   186 					'fopen()',
       
   187 					$r['filename']
       
   188 				) );
       
   189 			}
       
   190 		} else {
       
   191 			$this->stream_handle = false;
       
   192 		}
       
   193 
       
   194 		if ( !empty( $r['headers'] ) ) {
       
   195 			// cURL expects full header strings in each element.
       
   196 			$headers = array();
       
   197 			foreach ( $r['headers'] as $name => $value ) {
       
   198 				$headers[] = "{$name}: $value";
       
   199 			}
       
   200 			curl_setopt( $handle, CURLOPT_HTTPHEADER, $headers );
       
   201 		}
       
   202 
       
   203 		if ( $r['httpversion'] == '1.0' )
       
   204 			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0 );
       
   205 		else
       
   206 			curl_setopt( $handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
       
   207 
       
   208 		/**
       
   209 		 * Fires before the cURL request is executed.
       
   210 		 *
       
   211 		 * Cookies are not currently handled by the HTTP API. This action allows
       
   212 		 * plugins to handle cookies themselves.
       
   213 		 *
       
   214 		 * @since 2.8.0
       
   215 		 *
       
   216 		 * @param resource $handle  The cURL handle returned by curl_init() (passed by reference).
       
   217 		 * @param array    $r       The HTTP request arguments.
       
   218 		 * @param string   $url     The request URL.
       
   219 		 */
       
   220 		do_action_ref_array( 'http_api_curl', array( &$handle, $r, $url ) );
       
   221 
       
   222 		// We don't need to return the body, so don't. Just execute request and return.
       
   223 		if ( ! $r['blocking'] ) {
       
   224 			curl_exec( $handle );
       
   225 
       
   226 			if ( $curl_error = curl_error( $handle ) ) {
       
   227 				curl_close( $handle );
       
   228 				return new WP_Error( 'http_request_failed', $curl_error );
       
   229 			}
       
   230 			if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ) ) ) {
       
   231 				curl_close( $handle );
       
   232 				return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) );
       
   233 			}
       
   234 
       
   235 			curl_close( $handle );
       
   236 			return array( 'headers' => array(), 'body' => '', 'response' => array('code' => false, 'message' => false), 'cookies' => array() );
       
   237 		}
       
   238 
       
   239 		curl_exec( $handle );
       
   240 		$theHeaders = WP_Http::processHeaders( $this->headers, $url );
       
   241 		$theBody = $this->body;
       
   242 		$bytes_written_total = $this->bytes_written_total;
       
   243 
       
   244 		$this->headers = '';
       
   245 		$this->body = '';
       
   246 		$this->bytes_written_total = 0;
       
   247 
       
   248 		$curl_error = curl_errno( $handle );
       
   249 
       
   250 		// If an error occurred, or, no response.
       
   251 		if ( $curl_error || ( 0 == strlen( $theBody ) && empty( $theHeaders['headers'] ) ) ) {
       
   252 			if ( CURLE_WRITE_ERROR /* 23 */ == $curl_error ) {
       
   253 				if ( ! $this->max_body_length || $this->max_body_length != $bytes_written_total ) {
       
   254 					if ( $r['stream'] ) {
       
   255 						curl_close( $handle );
       
   256 						fclose( $this->stream_handle );
       
   257 						return new WP_Error( 'http_request_failed', __( 'Failed to write request to temporary file.' ) );
       
   258 					} else {
       
   259 						curl_close( $handle );
       
   260 						return new WP_Error( 'http_request_failed', curl_error( $handle ) );
       
   261 					}
       
   262 				}
       
   263 			} else {
       
   264 				if ( $curl_error = curl_error( $handle ) ) {
       
   265 					curl_close( $handle );
       
   266 					return new WP_Error( 'http_request_failed', $curl_error );
       
   267 				}
       
   268 			}
       
   269 			if ( in_array( curl_getinfo( $handle, CURLINFO_HTTP_CODE ), array( 301, 302 ) ) ) {
       
   270 				curl_close( $handle );
       
   271 				return new WP_Error( 'http_request_failed', __( 'Too many redirects.' ) );
       
   272 			}
       
   273 		}
       
   274 
       
   275 		curl_close( $handle );
       
   276 
       
   277 		if ( $r['stream'] )
       
   278 			fclose( $this->stream_handle );
       
   279 
       
   280 		$response = array(
       
   281 			'headers' => $theHeaders['headers'],
       
   282 			'body' => null,
       
   283 			'response' => $theHeaders['response'],
       
   284 			'cookies' => $theHeaders['cookies'],
       
   285 			'filename' => $r['filename']
       
   286 		);
       
   287 
       
   288 		// Handle redirects.
       
   289 		if ( false !== ( $redirect_response = WP_HTTP::handle_redirects( $url, $r, $response ) ) )
       
   290 			return $redirect_response;
       
   291 
       
   292 		if ( true === $r['decompress'] && true === WP_Http_Encoding::should_decode($theHeaders['headers']) )
       
   293 			$theBody = WP_Http_Encoding::decompress( $theBody );
       
   294 
       
   295 		$response['body'] = $theBody;
       
   296 
       
   297 		return $response;
       
   298 	}
       
   299 
       
   300 	/**
       
   301 	 * Grabs the headers of the cURL request.
       
   302 	 *
       
   303 	 * Each header is sent individually to this callback, so we append to the `$header` property
       
   304 	 * for temporary storage
       
   305 	 *
       
   306 	 * @since 3.2.0
       
   307 	 *
       
   308 	 * @param resource $handle  cURL handle.
       
   309 	 * @param string   $headers cURL request headers.
       
   310 	 * @return int Length of the request headers.
       
   311 	 */
       
   312 	private function stream_headers( $handle, $headers ) {
       
   313 		$this->headers .= $headers;
       
   314 		return strlen( $headers );
       
   315 	}
       
   316 
       
   317 	/**
       
   318 	 * Grabs the body of the cURL request.
       
   319 	 *
       
   320 	 * The contents of the document are passed in chunks, so we append to the `$body`
       
   321 	 * property for temporary storage. Returning a length shorter than the length of
       
   322 	 * `$data` passed in will cause cURL to abort the request with `CURLE_WRITE_ERROR`.
       
   323 	 *
       
   324 	 * @since 3.6.0
       
   325 	 *
       
   326 	 * @param resource $handle  cURL handle.
       
   327 	 * @param string   $data    cURL request body.
       
   328 	 * @return int Total bytes of data written.
       
   329 	 */
       
   330 	private function stream_body( $handle, $data ) {
       
   331 		$data_length = strlen( $data );
       
   332 
       
   333 		if ( $this->max_body_length && ( $this->bytes_written_total + $data_length ) > $this->max_body_length ) {
       
   334 			$data_length = ( $this->max_body_length - $this->bytes_written_total );
       
   335 			$data = substr( $data, 0, $data_length );
       
   336 		}
       
   337 
       
   338 		if ( $this->stream_handle ) {
       
   339 			$bytes_written = fwrite( $this->stream_handle, $data );
       
   340 		} else {
       
   341 			$this->body .= $data;
       
   342 			$bytes_written = $data_length;
       
   343 		}
       
   344 
       
   345 		$this->bytes_written_total += $bytes_written;
       
   346 
       
   347 		// Upon event of this function returning less than strlen( $data ) curl will error with CURLE_WRITE_ERROR.
       
   348 		return $bytes_written;
       
   349 	}
       
   350 
       
   351 	/**
       
   352 	 * Determines whether this class can be used for retrieving a URL.
       
   353 	 *
       
   354 	 * @static
       
   355 	 * @since 2.7.0
       
   356 	 *
       
   357 	 * @param array $args Optional. Array of request arguments. Default empty array.
       
   358 	 * @return bool False means this class can not be used, true means it can.
       
   359 	 */
       
   360 	public static function test( $args = array() ) {
       
   361 		if ( ! function_exists( 'curl_init' ) || ! function_exists( 'curl_exec' ) )
       
   362 			return false;
       
   363 
       
   364 		$is_ssl = isset( $args['ssl'] ) && $args['ssl'];
       
   365 
       
   366 		if ( $is_ssl ) {
       
   367 			$curl_version = curl_version();
       
   368 			// Check whether this cURL version support SSL requests.
       
   369 			if ( ! (CURL_VERSION_SSL & $curl_version['features']) )
       
   370 				return false;
       
   371 		}
       
   372 
       
   373 		/**
       
   374 		 * Filters whether cURL can be used as a transport for retrieving a URL.
       
   375 		 *
       
   376 		 * @since 2.7.0
       
   377 		 *
       
   378 		 * @param bool  $use_class Whether the class can be used. Default true.
       
   379 		 * @param array $args      An array of request arguments.
       
   380 		 */
       
   381 		return apply_filters( 'use_curl_transport', true, $args );
       
   382 	}
       
   383 }