class-wp-http-curl.php 12 KB

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