class-wp-http-curl.php 12 KB

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