class-wp-http-curl.php 11 KB

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