Completed
Push — master ( 3bcdeb...c90bfd )
by smiley
01:29
created

CurlHandle::reset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class CurlHandle
4
 *
5
 * @filesource   CurlHandle.php
6
 * @created      30.08.2018
7
 * @package      chillerlan\HTTP\CurlUtils
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP\CurlUtils;
14
15
use chillerlan\Settings\SettingsContainerInterface;
16
use Psr\Http\Message\{RequestInterface, ResponseInterface};
17
18
use function array_key_exists, count, curl_close, curl_init, curl_reset, curl_setopt_array,
19
	explode, in_array, is_resource, strlen, strtolower, strtoupper, substr, trim;
20
21
use const CURLOPT_CAINFO, CURLOPT_CONNECTTIMEOUT, CURLOPT_CUSTOMREQUEST, CURLOPT_FOLLOWLOCATION, CURLOPT_HEADER,
22
	CURLOPT_HEADERFUNCTION, CURLOPT_HTTP_VERSION, CURLOPT_HTTPHEADER, CURLOPT_INFILESIZE, CURLOPT_NOBODY,
23
	CURLOPT_POSTFIELDS, CURLOPT_PROGRESSFUNCTION, CURLOPT_PROTOCOLS, CURLOPT_READFUNCTION, CURLOPT_RETURNTRANSFER,
24
	CURLOPT_SSL_VERIFYHOST, CURLOPT_SSL_VERIFYPEER, CURLOPT_TIMEOUT, CURLOPT_UPLOAD, CURLOPT_URL, CURLOPT_USERAGENT,
25
	CURLOPT_USERPWD, CURLOPT_WRITEFUNCTION, CURLPROTO_HTTP, CURLPROTO_HTTPS, CURL_HTTP_VERSION_2TLS;
26
27
class CurlHandle implements CurlHandleInterface{
28
29
	/**
30
	 * The cURL handle
31
	 *
32
	 * @var resource
33
	 */
34
	public $curl;
35
36
	/**
37
	 * a handle ID (counter), used in CurlMultiClient
38
	 *
39
	 * @var int
40
	 */
41
	public $id;
42
43
	/**
44
	 * a retry counter, used in CurlMultiClient
45
	 *
46
	 * @var int
47
	 */
48
	public $retries;
49
50
	/**
51
	 * @var \Psr\Http\Message\RequestInterface
52
	 */
53
	public $request;
54
55
	/**
56
	 * @var \Psr\Http\Message\ResponseInterface
57
	 */
58
	public $response;
59
60
	/**
61
	 * @var \chillerlan\HTTP\HTTPOptions
62
	 */
63
	protected $options;
64
65
	/**
66
	 * CurlHandle constructor.
67
	 *
68
	 * @param \Psr\Http\Message\RequestInterface              $request
69
	 * @param \Psr\Http\Message\ResponseInterface             $response
70
	 * @param \chillerlan\Settings\SettingsContainerInterface $options
71
	 */
72
	public function __construct(RequestInterface $request, ResponseInterface $response, SettingsContainerInterface $options){
73
		$this->request  = $request;
74
		$this->response = $response;
75
		$this->options  = $options;
76
		$this->curl     = curl_init();
77
	}
78
79
	/**
80
	 * close an existing cURL handle on exit
81
	 */
82
	public function __destruct(){
83
		$this->close();
84
	}
85
86
	/**
87
	 * @return \chillerlan\HTTP\CurlUtils\CurlHandleInterface
88
	 */
89
	public function close():CurlHandleInterface{
90
91
		if(is_resource($this->curl)){
92
			curl_close($this->curl);
93
		}
94
95
		return $this;
96
	}
97
98
	/**
99
	 * @return array
100
	 */
101
	protected function initCurlOptions():array{
102
		return [
103
			CURLOPT_HEADER         => false,
104
			CURLOPT_RETURNTRANSFER => true,
105
			CURLOPT_FOLLOWLOCATION => false,
106
			CURLOPT_URL            => (string)$this->request->getUri()->withFragment(''),
107
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_2TLS,
108
			CURLOPT_USERAGENT      => $this->options->user_agent,
109
			CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
110
			CURLOPT_SSL_VERIFYPEER => true,
111
			CURLOPT_SSL_VERIFYHOST => 2,
112
			CURLOPT_CAINFO         => $this->options->ca_info,
113
			CURLOPT_TIMEOUT        => $this->options->timeout,
114
			CURLOPT_CONNECTTIMEOUT => 30,
115
			CURLOPT_WRITEFUNCTION  => [$this, 'writefunction'],
116
			CURLOPT_HEADERFUNCTION => [$this, 'headerfunction'],
117
		];
118
	}
119
120
	/**
121
	 * @param array $options
122
	 *
123
	 * @return array
124
	 */
125
	protected function setBodyOptions(array $options):array{
126
		$body     = $this->request->getBody();
127
		$bodySize = $body->getSize();
128
129
		if($bodySize === 0){
130
			return $options;
131
		}
132
133
		if($body->isSeekable()){
134
			$body->rewind();
135
		}
136
137
		// Message has non empty body.
138
		if($bodySize === null || $bodySize > 1 << 20){
139
			// Avoid full loading large or unknown size body into memory
140
			$options[CURLOPT_UPLOAD] = true;
141
142
			if($bodySize !== null){
143
				$options[CURLOPT_INFILESIZE] = $bodySize;
144
			}
145
146
			$options[CURLOPT_READFUNCTION] = [$this, 'readfunction'];
147
		}
148
		// Small body can be loaded into memory
149
		else{
150
			$options[CURLOPT_POSTFIELDS] = (string)$body;
151
		}
152
153
		return $options;
154
	}
155
156
	/**
157
	 * @param array $options
158
	 *
159
	 * @return array
160
	 */
161
	protected function setHeaderOptions(array $options):array{
162
		$headers = [];
163
164
		foreach($this->request->getHeaders() as $name => $values){
165
			$header = strtolower($name);
166
167
			// curl-client does not support "Expect-Continue", so dropping "expect" headers
168
			if($header === 'expect'){
169
				continue;
170
			}
171
172
			if($header === 'content-length'){
173
174
				// Small body content length can be calculated here.
175
				if(array_key_exists(CURLOPT_POSTFIELDS, $options)){
176
					$values = [strlen($options[CURLOPT_POSTFIELDS])];
177
				}
178
				// Else if there is no body, forcing "Content-length" to 0
179
				elseif(!array_key_exists(CURLOPT_READFUNCTION, $options)){
180
					$values = ['0'];
181
				}
182
183
			}
184
185 View Code Duplication
			foreach($values as $value){
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
186
				$value = (string)$value;
187
188
				// cURL requires a special format for empty headers.
189
				// See https://github.com/guzzle/guzzle/issues/1882 for more details.
190
				$headers[] = $value === '' ? $name.';' : $name.': '.$value;
191
			}
192
193
		}
194
195
		$options[CURLOPT_HTTPHEADER] = $headers;
196
197
		// If the Expect header is not present, prevent curl from adding it
198
		if(!$this->request->hasHeader('Expect')){
199
			$options[CURLOPT_HTTPHEADER][] = 'Expect:';
200
		}
201
202
		// cURL sometimes adds a content-type by default. Prevent this.
203
		if(!$this->request->hasHeader('Content-Type')){
204
			$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
205
		}
206
207
		return $options;
208
	}
209
210
	/**
211
	 * @return \chillerlan\HTTP\CurlUtils\CurlHandleInterface
212
	 */
213
	public function init():CurlHandleInterface{
214
		$options  = $this->initCurlOptions();
215
		$userinfo = $this->request->getUri()->getUserInfo();
216
		$method   = $this->request->getMethod();
217
218
		if(!empty($userinfo)){
219
			$options[CURLOPT_USERPWD] = $userinfo;
220
		}
221
222
		/*
223
		 * Some HTTP methods cannot have payload:
224
		 *
225
		 * - GET   — cURL will automatically change the method to PUT or POST
226
		 *           if we set CURLOPT_UPLOAD or CURLOPT_POSTFIELDS.
227
		 * - HEAD  — cURL treats HEAD as a GET request with same restrictions.
228
		 * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
229
		 */
230
231
		if(in_array($method, ['DELETE', 'PATCH', 'POST', 'PUT'], true)){
232
			$options = $this->setBodyOptions($options);
233
		}
234
235
		// This will set HTTP method to "HEAD".
236
		if($method === 'HEAD'){
237
			$options[CURLOPT_NOBODY] = true;
238
		}
239
240
		// GET is a default method. Other methods should be specified explicitly.
241
		if($method !== 'GET'){
242
			$options[CURLOPT_CUSTOMREQUEST] = $method;
243
		}
244
245
		// overwrite the default values with $curl_options
246
		foreach($this->options->curl_options as $k => $v){
247
			// skip some options that are only set automatically
248
			if(in_array($k, [CURLOPT_HTTPHEADER, CURLOPT_CUSTOMREQUEST, CURLOPT_NOBODY], true)){
249
				continue;
250
			}
251
252
			$options[$k] = $v;
253
		}
254
255
		$options = $this->setHeaderOptions($options);
256
257
		curl_setopt_array($this->curl, $options);
258
259
		return $this;
260
	}
261
262
	/**
263
	 * @internal
264
	 *
265
	 * @param resource $curl
266
	 * @param resource $stream
267
	 * @param int      $length
268
	 *
269
	 * @return string
270
	 */
271
	public function readfunction($curl, $stream, int $length):string{
272
		return $this->request->getBody()->read($length);
273
	}
274
275
	/**
276
	 * @internal
277
	 *
278
	 * @param resource $curl
279
	 * @param string   $data
280
	 *
281
	 * @return int
282
	 */
283
	public function writefunction($curl, string $data):int{
284
		return $this->response->getBody()->write($data);
285
	}
286
287
	/**
288
	 * @internal
289
	 *
290
	 * @param resource $curl
291
	 * @param string   $line
292
	 *
293
	 * @return int
294
	 */
295
	public function headerfunction($curl, string $line):int{
296
		$str    = trim($line);
297
		$header = explode(':', $str, 2);
298
299
		if(count($header) === 2){
300
			$this->response = $this->response
301
				->withAddedHeader(trim($header[0]), trim($header[1]));
302
		}
303
		elseif(substr(strtoupper($str), 0, 5) === 'HTTP/'){
304
			$status = explode(' ', $str, 3);
305
			$reason = count($status) > 2 ? trim($status[2]) : '';
306
307
			$this->response = $this->response
308
				->withStatus((int)$status[1], $reason)
309
				->withProtocolVersion(substr($status[0], 5))
310
			;
311
		}
312
313
		return strlen($line);
314
	}
315
316
}
317