wp/wp-includes/Requests/Transport/cURL.php
changeset 18 be944660c56a
parent 7 cf61fcea0001
equal deleted inserted replaced
17:34716fd837a4 18:be944660c56a
    36 	 * @var array cURL information array, see {@see https://secure.php.net/curl_getinfo}
    36 	 * @var array cURL information array, see {@see https://secure.php.net/curl_getinfo}
    37 	 */
    37 	 */
    38 	public $info;
    38 	public $info;
    39 
    39 
    40 	/**
    40 	/**
    41 	 * Version string
    41 	 * cURL version number
    42 	 *
    42 	 *
    43 	 * @var long
    43 	 * @var int
    44 	 */
    44 	 */
    45 	public $version;
    45 	public $version;
    46 
    46 
    47 	/**
    47 	/**
    48 	 * cURL handle
    48 	 * cURL handle
    88 
    88 
    89 	/**
    89 	/**
    90 	 * Constructor
    90 	 * Constructor
    91 	 */
    91 	 */
    92 	public function __construct() {
    92 	public function __construct() {
    93 		$curl = curl_version();
    93 		$curl          = curl_version();
    94 		$this->version = $curl['version_number'];
    94 		$this->version = $curl['version_number'];
    95 		$this->handle = curl_init();
    95 		$this->handle  = curl_init();
    96 
    96 
    97 		curl_setopt($this->handle, CURLOPT_HEADER, false);
    97 		curl_setopt($this->handle, CURLOPT_HEADER, false);
    98 		curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1);
    98 		curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1);
    99 		if ($this->version >= self::CURL_7_10_5) {
    99 		if ($this->version >= self::CURL_7_10_5) {
   100 			curl_setopt($this->handle, CURLOPT_ENCODING, '');
   100 			curl_setopt($this->handle, CURLOPT_ENCODING, '');
   101 		}
   101 		}
   102 		if (defined('CURLOPT_PROTOCOLS')) {
   102 		if (defined('CURLOPT_PROTOCOLS')) {
       
   103 			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound
   103 			curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
   104 			curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
   104 		}
   105 		}
   105 		if (defined('CURLOPT_REDIR_PROTOCOLS')) {
   106 		if (defined('CURLOPT_REDIR_PROTOCOLS')) {
       
   107 			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound
   106 			curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
   108 			curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
   107 		}
   109 		}
   108 	}
   110 	}
   109 
   111 
   110 	/**
   112 	/**
   136 
   138 
   137 		if ($options['filename'] !== false) {
   139 		if ($options['filename'] !== false) {
   138 			$this->stream_handle = fopen($options['filename'], 'wb');
   140 			$this->stream_handle = fopen($options['filename'], 'wb');
   139 		}
   141 		}
   140 
   142 
   141 		$this->response_data = '';
   143 		$this->response_data       = '';
   142 		$this->response_bytes = 0;
   144 		$this->response_bytes      = 0;
   143 		$this->response_byte_limit = false;
   145 		$this->response_byte_limit = false;
   144 		if ($options['max_bytes'] !== false) {
   146 		if ($options['max_bytes'] !== false) {
   145 			$this->response_byte_limit = $options['max_bytes'];
   147 			$this->response_byte_limit = $options['max_bytes'];
   146 		}
   148 		}
   147 
   149 
   166 
   168 
   167 		if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) {
   169 		if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) {
   168 			// Reset encoding and try again
   170 			// Reset encoding and try again
   169 			curl_setopt($this->handle, CURLOPT_ENCODING, 'none');
   171 			curl_setopt($this->handle, CURLOPT_ENCODING, 'none');
   170 
   172 
   171 			$this->response_data = '';
   173 			$this->response_data  = '';
   172 			$this->response_bytes = 0;
   174 			$this->response_bytes = 0;
   173 			curl_exec($this->handle);
   175 			curl_exec($this->handle);
   174 			$response = $this->response_data;
   176 			$response = $this->response_data;
   175 		}
   177 		}
   176 
   178 
   197 			return array();
   199 			return array();
   198 		}
   200 		}
   199 
   201 
   200 		$multihandle = curl_multi_init();
   202 		$multihandle = curl_multi_init();
   201 		$subrequests = array();
   203 		$subrequests = array();
   202 		$subhandles = array();
   204 		$subhandles  = array();
   203 
   205 
   204 		$class = get_class($this);
   206 		$class = get_class($this);
   205 		foreach ($requests as $id => $request) {
   207 		foreach ($requests as $id => $request) {
   206 			$subrequests[$id] = new $class();
   208 			$subrequests[$id] = new $class();
   207 			$subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
   209 			$subhandles[$id]  = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
   208 			$request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
   210 			$request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id]));
   209 			curl_multi_add_handle($multihandle, $subhandles[$id]);
   211 			curl_multi_add_handle($multihandle, $subhandles[$id]);
   210 		}
   212 		}
   211 
   213 
   212 		$completed = 0;
   214 		$completed       = 0;
   213 		$responses = array();
   215 		$responses       = array();
       
   216 		$subrequestcount = count($subrequests);
   214 
   217 
   215 		$request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle));
   218 		$request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle));
   216 
   219 
   217 		do {
   220 		do {
   218 			$active = false;
   221 			$active = 0;
   219 
   222 
   220 			do {
   223 			do {
   221 				$status = curl_multi_exec($multihandle, $active);
   224 				$status = curl_multi_exec($multihandle, $active);
   222 			}
   225 			}
   223 			while ($status === CURLM_CALL_MULTI_PERFORM);
   226 			while ($status === CURLM_CALL_MULTI_PERFORM);
   233 			}
   236 			}
   234 
   237 
   235 			// Parse the finished requests before we start getting the new ones
   238 			// Parse the finished requests before we start getting the new ones
   236 			foreach ($to_process as $key => $done) {
   239 			foreach ($to_process as $key => $done) {
   237 				$options = $requests[$key]['options'];
   240 				$options = $requests[$key]['options'];
   238 				if (CURLE_OK !== $done['result']) {
   241 				if ($done['result'] !== CURLE_OK) {
   239 					//get error string for handle.
   242 					//get error string for handle.
   240 					$reason = curl_error($done['handle']);
   243 					$reason          = curl_error($done['handle']);
   241 					$exception = new Requests_Exception_Transport_cURL(
   244 					$exception       = new Requests_Exception_Transport_cURL(
   242 									$reason,
   245 						$reason,
   243 									Requests_Exception_Transport_cURL::EASY,
   246 						Requests_Exception_Transport_cURL::EASY,
   244 									$done['handle'],
   247 						$done['handle'],
   245 									$done['result']
   248 						$done['result']
   246 								);
   249 					);
   247 					$responses[$key] = $exception;
   250 					$responses[$key] = $exception;
   248 					$options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
   251 					$options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key]));
   249 				}
   252 				}
   250 				else {
   253 				else {
   251 					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
   254 					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);
   260 					$options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
   263 					$options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key));
   261 				}
   264 				}
   262 				$completed++;
   265 				$completed++;
   263 			}
   266 			}
   264 		}
   267 		}
   265 		while ($active || $completed < count($subrequests));
   268 		while ($active || $completed < $subrequestcount);
   266 
   269 
   267 		$request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
   270 		$request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle));
   268 
   271 
   269 		curl_multi_close($multihandle);
   272 		curl_multi_close($multihandle);
   270 
   273 
   285 
   288 
   286 		if ($options['filename'] !== false) {
   289 		if ($options['filename'] !== false) {
   287 			$this->stream_handle = fopen($options['filename'], 'wb');
   290 			$this->stream_handle = fopen($options['filename'], 'wb');
   288 		}
   291 		}
   289 
   292 
   290 		$this->response_data = '';
   293 		$this->response_data       = '';
   291 		$this->response_bytes = 0;
   294 		$this->response_bytes      = 0;
   292 		$this->response_byte_limit = false;
   295 		$this->response_byte_limit = false;
   293 		if ($options['max_bytes'] !== false) {
   296 		if ($options['max_bytes'] !== false) {
   294 			$this->response_byte_limit = $options['max_bytes'];
   297 			$this->response_byte_limit = $options['max_bytes'];
   295 		}
   298 		}
   296 		$this->hooks = $options['hooks'];
   299 		$this->hooks = $options['hooks'];
   308 	 */
   311 	 */
   309 	protected function setup_handle($url, $headers, $data, $options) {
   312 	protected function setup_handle($url, $headers, $data, $options) {
   310 		$options['hooks']->dispatch('curl.before_request', array(&$this->handle));
   313 		$options['hooks']->dispatch('curl.before_request', array(&$this->handle));
   311 
   314 
   312 		// Force closing the connection for old versions of cURL (<7.22).
   315 		// Force closing the connection for old versions of cURL (<7.22).
   313 		if ( ! isset( $headers['Connection'] ) ) {
   316 		if (!isset($headers['Connection'])) {
   314 			$headers['Connection'] = 'close';
   317 			$headers['Connection'] = 'close';
       
   318 		}
       
   319 
       
   320 		/**
       
   321 		 * Add "Expect" header.
       
   322 		 *
       
   323 		 * By default, cURL adds a "Expect: 100-Continue" to most requests. This header can
       
   324 		 * add as much as a second to the time it takes for cURL to perform a request. To
       
   325 		 * prevent this, we need to set an empty "Expect" header. To match the behaviour of
       
   326 		 * Guzzle, we'll add the empty header to requests that are smaller than 1 MB and use
       
   327 		 * HTTP/1.1.
       
   328 		 *
       
   329 		 * https://curl.se/mail/lib-2017-07/0013.html
       
   330 		 */
       
   331 		if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) {
       
   332 			$headers['Expect'] = $this->get_expect_header($data);
   315 		}
   333 		}
   316 
   334 
   317 		$headers = Requests::flatten($headers);
   335 		$headers = Requests::flatten($headers);
   318 
   336 
   319 		if (!empty($data)) {
   337 		if (!empty($data)) {
   320 			$data_format = $options['data_format'];
   338 			$data_format = $options['data_format'];
   321 
   339 
   322 			if ($data_format === 'query') {
   340 			if ($data_format === 'query') {
   323 				$url = self::format_get($url, $data);
   341 				$url  = self::format_get($url, $data);
   324 				$data = '';
   342 				$data = '';
   325 			}
   343 			}
   326 			elseif (!is_string($data)) {
   344 			elseif (!is_string($data)) {
   327 				$data = http_build_query($data, null, '&');
   345 				$data = http_build_query($data, null, '&');
   328 			}
   346 			}
   361 
   379 
   362 		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
   380 		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
   363 			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
   381 			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
   364 		}
   382 		}
   365 		else {
   383 		else {
       
   384 			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound
   366 			curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
   385 			curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
   367 		}
   386 		}
   368 
   387 
   369 		if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
   388 		if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
   370 			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
   389 			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
   371 		}
   390 		}
   372 		else {
   391 		else {
       
   392 			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound
   373 			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
   393 			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
   374 		}
   394 		}
   375 		curl_setopt($this->handle, CURLOPT_URL, $url);
   395 		curl_setopt($this->handle, CURLOPT_URL, $url);
   376 		curl_setopt($this->handle, CURLOPT_REFERER, $url);
   396 		curl_setopt($this->handle, CURLOPT_REFERER, $url);
   377 		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
   397 		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
   383 		}
   403 		}
   384 		else {
   404 		else {
   385 			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
   405 			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
   386 		}
   406 		}
   387 
   407 
   388 		if (true === $options['blocking']) {
   408 		if ($options['blocking'] === true) {
   389 			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array(&$this, 'stream_headers'));
   409 			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array($this, 'stream_headers'));
   390 			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array(&$this, 'stream_body'));
   410 			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array($this, 'stream_body'));
   391 			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
   411 			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
   392 		}
   412 		}
   393 	}
   413 	}
   394 
   414 
   395 	/**
   415 	/**
   396 	 * Process a response
   416 	 * Process a response
   397 	 *
   417 	 *
   398 	 * @param string $response Response data from the body
   418 	 * @param string $response Response data from the body
   399 	 * @param array $options Request options
   419 	 * @param array $options Request options
   400 	 * @return string HTTP response data including headers
   420 	 * @return string|false HTTP response data including headers. False if non-blocking.
       
   421 	 * @throws Requests_Exception
   401 	 */
   422 	 */
   402 	public function process_response($response, $options) {
   423 	public function process_response($response, $options) {
   403 		if ($options['blocking'] === false) {
   424 		if ($options['blocking'] === false) {
   404 			$fake_headers = '';
   425 			$fake_headers = '';
   405 			$options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
   426 			$options['hooks']->dispatch('curl.after_request', array(&$fake_headers));
   406 			return false;
   427 			return false;
   407 		}
   428 		}
   408 		if ($options['filename'] !== false) {
   429 		if ($options['filename'] !== false && $this->stream_handle) {
   409 			fclose($this->stream_handle);
   430 			fclose($this->stream_handle);
   410 			$this->headers = trim($this->headers);
   431 			$this->headers = trim($this->headers);
   411 		}
   432 		}
   412 		else {
   433 		else {
   413 			$this->headers .= $response;
   434 			$this->headers .= $response;
   437 	public function stream_headers($handle, $headers) {
   458 	public function stream_headers($handle, $headers) {
   438 		// Why do we do this? cURL will send both the final response and any
   459 		// Why do we do this? cURL will send both the final response and any
   439 		// interim responses, such as a 100 Continue. We don't need that.
   460 		// interim responses, such as a 100 Continue. We don't need that.
   440 		// (We may want to keep this somewhere just in case)
   461 		// (We may want to keep this somewhere just in case)
   441 		if ($this->done_headers) {
   462 		if ($this->done_headers) {
   442 			$this->headers = '';
   463 			$this->headers      = '';
   443 			$this->done_headers = false;
   464 			$this->done_headers = false;
   444 		}
   465 		}
   445 		$this->headers .= $headers;
   466 		$this->headers .= $headers;
   446 
   467 
   447 		if ($headers === "\r\n") {
   468 		if ($headers === "\r\n") {
   471 			}
   492 			}
   472 
   493 
   473 			if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
   494 			if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
   474 				// Limit the length
   495 				// Limit the length
   475 				$limited_length = ($this->response_byte_limit - $this->response_bytes);
   496 				$limited_length = ($this->response_byte_limit - $this->response_bytes);
   476 				$data = substr($data, 0, $limited_length);
   497 				$data           = substr($data, 0, $limited_length);
   477 			}
   498 			}
   478 		}
   499 		}
   479 
   500 
   480 		if ($this->stream_handle) {
   501 		if ($this->stream_handle) {
   481 			fwrite($this->stream_handle, $data);
   502 			fwrite($this->stream_handle, $data);
   495 	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
   516 	 * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query}
   496 	 * @return string URL with data
   517 	 * @return string URL with data
   497 	 */
   518 	 */
   498 	protected static function format_get($url, $data) {
   519 	protected static function format_get($url, $data) {
   499 		if (!empty($data)) {
   520 		if (!empty($data)) {
       
   521 			$query     = '';
   500 			$url_parts = parse_url($url);
   522 			$url_parts = parse_url($url);
   501 			if (empty($url_parts['query'])) {
   523 			if (empty($url_parts['query'])) {
   502 				$query = $url_parts['query'] = '';
   524 				$url_parts['query'] = '';
   503 			}
   525 			}
   504 			else {
   526 			else {
   505 				$query = $url_parts['query'];
   527 				$query = $url_parts['query'];
   506 			}
   528 			}
   507 
   529 
   508 			$query .= '&' . http_build_query($data, null, '&');
   530 			$query .= '&' . http_build_query($data, null, '&');
   509 			$query = trim($query, '&');
   531 			$query  = trim($query, '&');
   510 
   532 
   511 			if (empty($url_parts['query'])) {
   533 			if (empty($url_parts['query'])) {
   512 				$url .= '?' . $query;
   534 				$url .= '?' . $query;
   513 			}
   535 			}
   514 			else {
   536 			else {
   537 			}
   559 			}
   538 		}
   560 		}
   539 
   561 
   540 		return true;
   562 		return true;
   541 	}
   563 	}
       
   564 
       
   565 	/**
       
   566 	 * Get the correct "Expect" header for the given request data.
       
   567 	 *
       
   568 	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD.
       
   569 	 * @return string The "Expect" header.
       
   570 	 */
       
   571 	protected function get_expect_header($data) {
       
   572 		if (!is_array($data)) {
       
   573 			return strlen((string) $data) >= 1048576 ? '100-Continue' : '';
       
   574 		}
       
   575 
       
   576 		$bytesize = 0;
       
   577 		$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data));
       
   578 
       
   579 		foreach ($iterator as $datum) {
       
   580 			$bytesize += strlen((string) $datum);
       
   581 
       
   582 			if ($bytesize >= 1048576) {
       
   583 				return '100-Continue';
       
   584 			}
       
   585 		}
       
   586 
       
   587 		return '';
       
   588 	}
   542 }
   589 }