Passed
Push — main ( f2efe3...53dc0a )
by smiley
10:15
created

CurlHandle::setHeaderOptions()   B

Complexity

Conditions 10
Paths 56

Size

Total Lines 47
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 19
nc 56
nop 1
dl 0
loc 47
rs 7.6666
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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_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_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
	CURLE_COULDNT_CONNECT, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_RESOLVE_PROXY,
27
	CURLE_GOT_NOTHING, CURLE_OPERATION_TIMEOUTED, CURLE_SSL_CONNECT_ERROR;
28
29
class CurlHandle{
30
31
	const CURL_NETWORK_ERRORS = [
32
		CURLE_COULDNT_RESOLVE_PROXY,
33
		CURLE_COULDNT_RESOLVE_HOST,
34
		CURLE_COULDNT_CONNECT,
35
		CURLE_OPERATION_TIMEOUTED,
36
		CURLE_SSL_CONNECT_ERROR,
37
		CURLE_GOT_NOTHING,
38
	];
39
40
	// https://www.php.net/manual/function.curl-getinfo.php#111678
41
	// https://www.openssl.org/docs/manmaster/man1/verify.html#VERIFY_OPERATION
42
	// https://github.com/openssl/openssl/blob/91cb81d40a8102c3d8667629661be8d6937db82b/include/openssl/x509_vfy.h#L99-L189
43
	const CURLINFO_SSL_VERIFYRESULT = [
44
		0  => 'ok the operation was successful.',
45
		2  => 'unable to get issuer certificate',
46
		3  => 'unable to get certificate CRL',
47
		4  => 'unable to decrypt certificate\'s signature',
48
		5  => 'unable to decrypt CRL\'s signature',
49
		6  => 'unable to decode issuer public key',
50
		7  => 'certificate signature failure',
51
		8  => 'CRL signature failure',
52
		9  => 'certificate is not yet valid',
53
		10 => 'certificate has expired',
54
		11 => 'CRL is not yet valid',
55
		12 => 'CRL has expired',
56
		13 => 'format error in certificate\'s notBefore field',
57
		14 => 'format error in certificate\'s notAfter field',
58
		15 => 'format error in CRL\'s lastUpdate field',
59
		16 => 'format error in CRL\'s nextUpdate field',
60
		17 => 'out of memory',
61
		18 => 'self signed certificate',
62
		19 => 'self signed certificate in certificate chain',
63
		20 => 'unable to get local issuer certificate',
64
		21 => 'unable to verify the first certificate',
65
		22 => 'certificate chain too long',
66
		23 => 'certificate revoked',
67
		24 => 'invalid CA certificate',
68
		25 => 'path length constraint exceeded',
69
		26 => 'unsupported certificate purpose',
70
		27 => 'certificate not trusted',
71
		28 => 'certificate rejected',
72
		29 => 'subject issuer mismatch',
73
		30 => 'authority and subject key identifier mismatch',
74
		31 => 'authority and issuer serial number mismatch',
75
		32 => 'key usage does not include certificate signing',
76
		50 => 'application verification failure',
77
	];
78
79
	/**
80
	 * The cURL handle
81
	 *
82
	 * @var resource|null
83
	 */
84
	protected $curl;
85
86
	/**
87
	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\HTTP\HTTPOptions
88
	 */
89
	protected SettingsContainerInterface $options;
90
91
	protected RequestInterface $request;
92
93
	protected ResponseInterface $response;
94
95
	/**
96
	 * CurlHandle constructor.
97
	 */
98
	public function __construct(RequestInterface $request, ResponseInterface $response, SettingsContainerInterface $options){
99
		$this->request  = $request;
100
		$this->response = $response;
101
		$this->options  = $options;
102
		$this->curl     = curl_init();
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_init() can also be of type CurlHandle. However, the property $curl is declared as type null|resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
103
	}
104
105
	/**
106
	 * close an existing cURL handle on exit
107
	 */
108
	public function __destruct(){
109
		$this->close();
110
	}
111
112
	/**
113
	 * @return \chillerlan\HTTP\CurlUtils\CurlHandle
114
	 */
115
	public function close():CurlHandle{
116
117
		if(is_resource($this->curl)){
118
			curl_close($this->curl);
119
		}
120
121
		return $this;
122
	}
123
124
	/**
125
	 * @return resource|null
126
	 */
127
	public function getCurlResource(){
128
		return $this->curl;
129
	}
130
131
	/**
132
	 *
133
	 */
134
	public function getRequest():RequestInterface{
135
		return $this->request;
136
	}
137
138
	/**
139
	 *
140
	 */
141
	public function getResponse():ResponseInterface{
142
		return $this->response;
143
	}
144
145
	/**
146
	 * @return array
147
	 */
148
	protected function initCurlOptions():array{
149
		return [
150
			CURLOPT_HEADER         => false,
151
			CURLOPT_RETURNTRANSFER => true,
152
			CURLOPT_FOLLOWLOCATION => false,
153
			CURLOPT_URL            => (string)$this->request->getUri()->withFragment(''),
154
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_2TLS,
155
			CURLOPT_USERAGENT      => $this->options->user_agent,
156
			CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
157
			CURLOPT_SSL_VERIFYPEER => true,
158
			CURLOPT_SSL_VERIFYHOST => 2,
159
			CURLOPT_CAINFO         => $this->options->ca_info,
160
			CURLOPT_TIMEOUT        => $this->options->timeout,
161
			CURLOPT_CONNECTTIMEOUT => 30,
162
			CURLOPT_WRITEFUNCTION  => [$this, 'writefunction'],
163
			CURLOPT_HEADERFUNCTION => [$this, 'headerfunction'],
164
		];
165
	}
166
167
	/**
168
	 * @param array $options
169
	 *
170
	 * @return array
171
	 */
172
	protected function setBodyOptions(array $options):array{
173
		$body     = $this->request->getBody();
174
		$bodySize = $body->getSize();
175
176
		if($bodySize === 0){
177
			return $options;
178
		}
179
180
		if($body->isSeekable()){
181
			$body->rewind();
182
		}
183
184
		// Message has non empty body.
185
		if($bodySize === null || $bodySize > 1 << 20){
186
			// Avoid full loading large or unknown size body into memory
187
			$options[CURLOPT_UPLOAD] = true;
188
189
			if($bodySize !== null){
190
				$options[CURLOPT_INFILESIZE] = $bodySize;
191
			}
192
193
			$options[CURLOPT_READFUNCTION] = [$this, 'readfunction'];
194
		}
195
		// Small body can be loaded into memory
196
		else{
197
			$options[CURLOPT_POSTFIELDS] = (string)$body;
198
		}
199
200
		return $options;
201
	}
202
203
	/**
204
	 * @param array $options
205
	 *
206
	 * @return array
207
	 */
208
	protected function setHeaderOptions(array $options):array{
209
		$headers = [];
210
211
		foreach($this->request->getHeaders() as $name => $values){
212
			$header = strtolower($name);
213
214
			// curl-client does not support "Expect-Continue", so dropping "expect" headers
215
			if($header === 'expect'){
216
				continue;
217
			}
218
219
			if($header === 'content-length'){
220
221
				// Small body content length can be calculated here.
222
				if(array_key_exists(CURLOPT_POSTFIELDS, $options)){
223
					$values = [strlen($options[CURLOPT_POSTFIELDS])];
224
				}
225
				// Else if there is no body, forcing "Content-length" to 0
226
				elseif(!array_key_exists(CURLOPT_READFUNCTION, $options)){
227
					$values = ['0'];
228
				}
229
230
			}
231
232
			foreach($values as $value){
233
				$value = (string)$value;
234
235
				// cURL requires a special format for empty headers.
236
				// See https://github.com/guzzle/guzzle/issues/1882 for more details.
237
				$headers[] = $value === '' ? $name.';' : $name.': '.$value;
238
			}
239
240
		}
241
242
		$options[CURLOPT_HTTPHEADER] = $headers;
243
244
		// If the Expect header is not present, prevent curl from adding it
245
		if(!$this->request->hasHeader('Expect')){
246
			$options[CURLOPT_HTTPHEADER][] = 'Expect:';
247
		}
248
249
		// cURL sometimes adds a content-type by default. Prevent this.
250
		if(!$this->request->hasHeader('Content-Type')){
251
			$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
252
		}
253
254
		return $options;
255
	}
256
257
	/**
258
	 * @return \chillerlan\HTTP\CurlUtils\CurlHandle
259
	 */
260
	public function init():CurlHandle{
261
		$options  = $this->initCurlOptions();
262
		$userinfo = $this->request->getUri()->getUserInfo();
263
		$method   = $this->request->getMethod();
264
265
		if(!empty($userinfo)){
266
			$options[CURLOPT_USERPWD] = $userinfo;
267
		}
268
269
		/*
270
		 * Some HTTP methods cannot have payload:
271
		 *
272
		 * - GET   — cURL will automatically change the method to PUT or POST
273
		 *           if we set CURLOPT_UPLOAD or CURLOPT_POSTFIELDS.
274
		 * - HEAD  — cURL treats HEAD as a GET request with same restrictions.
275
		 * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
276
		 */
277
278
		if(in_array($method, ['DELETE', 'PATCH', 'POST', 'PUT'], true)){
279
			$options = $this->setBodyOptions($options);
280
		}
281
282
		// This will set HTTP method to "HEAD".
283
		if($method === 'HEAD'){
284
			$options[CURLOPT_NOBODY] = true;
285
		}
286
287
		// GET is a default method. Other methods should be specified explicitly.
288
		if($method !== 'GET'){
289
			$options[CURLOPT_CUSTOMREQUEST] = $method;
290
		}
291
292
		// overwrite the default values with $curl_options
293
		foreach($this->options->curl_options as $k => $v){
294
			// skip some options that are only set automatically
295
			if(in_array($k, [CURLOPT_HTTPHEADER, CURLOPT_CUSTOMREQUEST, CURLOPT_NOBODY], true)){
296
				continue;
297
			}
298
299
			$options[$k] = $v;
300
		}
301
302
		$options = $this->setHeaderOptions($options);
303
304
		curl_setopt_array($this->curl, $options);
305
306
		return $this;
307
	}
308
309
	/**
310
	 * @internal
311
	 *
312
	 * @param resource $curl
313
	 * @param resource $stream
314
	 * @param int      $length
315
	 *
316
	 * @return string
317
	 */
318
	public function readfunction($curl, $stream, int $length):string{
319
		return $this->request->getBody()->read($length);
320
	}
321
322
	/**
323
	 * @internal
324
	 *
325
	 * @param resource $curl
326
	 * @param string   $data
327
	 *
328
	 * @return int
329
	 */
330
	public function writefunction($curl, string $data):int{
331
		return $this->response->getBody()->write($data);
332
	}
333
334
	/**
335
	 * @internal
336
	 *
337
	 * @param resource $curl
338
	 * @param string   $line
339
	 *
340
	 * @return int
341
	 */
342
	public function headerfunction($curl, string $line):int{
343
		$str    = trim($line);
344
		$header = explode(':', $str, 2);
345
346
		if(count($header) === 2){
347
			$this->response = $this->response
348
				->withAddedHeader(trim($header[0]), trim($header[1]));
349
		}
350
		elseif(substr(strtoupper($str), 0, 5) === 'HTTP/'){
351
			$status = explode(' ', $str, 3);
352
			$reason = count($status) > 2 ? trim($status[2]) : '';
353
354
			$this->response = $this->response
355
				->withStatus((int)$status[1], $reason)
356
				->withProtocolVersion(substr($status[0], 5))
357
			;
358
		}
359
360
		return strlen($line);
361
	}
362
363
}
364