Passed
Push — gh-pages ( 22b0fe...eb2d91 )
by
unknown
12:27 queued 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
 * @created      30.08.2018
6
 * @author       smiley <[email protected]>
7
 * @copyright    2018 smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\HTTP\CurlUtils;
12
13
use chillerlan\Settings\SettingsContainerInterface;
14
use Psr\Http\Message\{RequestInterface, ResponseInterface};
15
16
use function array_key_exists, count, curl_close, curl_init, curl_setopt_array,
17
	explode, in_array, is_resource, strlen, strtolower, strtoupper, substr, trim;
18
19
use const CURLOPT_CAINFO, CURLOPT_CONNECTTIMEOUT, CURLOPT_CUSTOMREQUEST, CURLOPT_FOLLOWLOCATION, CURLOPT_HEADER,
20
	CURLOPT_HEADERFUNCTION, CURLOPT_HTTP_VERSION, CURLOPT_HTTPHEADER, CURLOPT_INFILESIZE, CURLOPT_NOBODY,
21
	CURLOPT_POSTFIELDS, CURLOPT_PROTOCOLS, CURLOPT_READFUNCTION, CURLOPT_RETURNTRANSFER,
22
	CURLOPT_SSL_VERIFYHOST, CURLOPT_SSL_VERIFYPEER, CURLOPT_TIMEOUT, CURLOPT_UPLOAD, CURLOPT_URL, CURLOPT_USERAGENT,
23
	CURLOPT_USERPWD, CURLOPT_WRITEFUNCTION, CURLPROTO_HTTP, CURLPROTO_HTTPS, CURL_HTTP_VERSION_2TLS,
24
	CURLE_COULDNT_CONNECT, CURLE_COULDNT_RESOLVE_HOST, CURLE_COULDNT_RESOLVE_PROXY,
25
	CURLE_GOT_NOTHING, CURLE_OPERATION_TIMEOUTED, CURLE_SSL_CONNECT_ERROR;
26
27
class CurlHandle{
28
29
	const CURL_NETWORK_ERRORS = [
30
		CURLE_COULDNT_RESOLVE_PROXY,
31
		CURLE_COULDNT_RESOLVE_HOST,
32
		CURLE_COULDNT_CONNECT,
33
		CURLE_OPERATION_TIMEOUTED,
34
		CURLE_SSL_CONNECT_ERROR,
35
		CURLE_GOT_NOTHING,
36
	];
37
38
	// https://www.php.net/manual/function.curl-getinfo.php#111678
39
	// https://www.openssl.org/docs/manmaster/man1/verify.html#VERIFY_OPERATION
40
	// https://github.com/openssl/openssl/blob/91cb81d40a8102c3d8667629661be8d6937db82b/include/openssl/x509_vfy.h#L99-L189
41
	const CURLINFO_SSL_VERIFYRESULT = [
42
		0  => 'ok the operation was successful.',
43
		2  => 'unable to get issuer certificate',
44
		3  => 'unable to get certificate CRL',
45
		4  => 'unable to decrypt certificate\'s signature',
46
		5  => 'unable to decrypt CRL\'s signature',
47
		6  => 'unable to decode issuer public key',
48
		7  => 'certificate signature failure',
49
		8  => 'CRL signature failure',
50
		9  => 'certificate is not yet valid',
51
		10 => 'certificate has expired',
52
		11 => 'CRL is not yet valid',
53
		12 => 'CRL has expired',
54
		13 => 'format error in certificate\'s notBefore field',
55
		14 => 'format error in certificate\'s notAfter field',
56
		15 => 'format error in CRL\'s lastUpdate field',
57
		16 => 'format error in CRL\'s nextUpdate field',
58
		17 => 'out of memory',
59
		18 => 'self signed certificate',
60
		19 => 'self signed certificate in certificate chain',
61
		20 => 'unable to get local issuer certificate',
62
		21 => 'unable to verify the first certificate',
63
		22 => 'certificate chain too long',
64
		23 => 'certificate revoked',
65
		24 => 'invalid CA certificate',
66
		25 => 'path length constraint exceeded',
67
		26 => 'unsupported certificate purpose',
68
		27 => 'certificate not trusted',
69
		28 => 'certificate rejected',
70
		29 => 'subject issuer mismatch',
71
		30 => 'authority and subject key identifier mismatch',
72
		31 => 'authority and issuer serial number mismatch',
73
		32 => 'key usage does not include certificate signing',
74
		50 => 'application verification failure',
75
	];
76
77
	/**
78
	 * The cURL handle
79
	 *
80
	 * @var resource|null
81
	 */
82
	protected $curl;
83
84
	/**
85
	 * @var \chillerlan\Settings\SettingsContainerInterface|\chillerlan\HTTP\HTTPOptions
86
	 */
87
	protected SettingsContainerInterface $options;
88
89
	protected RequestInterface $request;
90
91
	protected ResponseInterface $response;
92
93
	/**
94
	 * CurlHandle constructor.
95
	 */
96
	public function __construct(RequestInterface $request, ResponseInterface $response, SettingsContainerInterface $options){
97
		$this->request  = $request;
98
		$this->response = $response;
99
		$this->options  = $options;
100
		$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...
101
	}
102
103
	/**
104
	 * close an existing cURL handle on exit
105
	 */
106
	public function __destruct(){
107
		$this->close();
108
	}
109
110
	/**
111
	 * @return \chillerlan\HTTP\CurlUtils\CurlHandle
112
	 */
113
	public function close():CurlHandle{
114
115
		if(is_resource($this->curl)){
116
			curl_close($this->curl);
117
		}
118
119
		return $this;
120
	}
121
122
	/**
123
	 * @return resource|null
124
	 */
125
	public function getCurlResource(){
126
		return $this->curl;
127
	}
128
129
	/**
130
	 *
131
	 */
132
	public function getRequest():RequestInterface{
133
		return $this->request;
134
	}
135
136
	/**
137
	 *
138
	 */
139
	public function getResponse():ResponseInterface{
140
		return $this->response;
141
	}
142
143
	/**
144
	 * @return array
145
	 */
146
	protected function initCurlOptions():array{
147
		return [
148
			CURLOPT_HEADER         => false,
149
			CURLOPT_RETURNTRANSFER => true,
150
			CURLOPT_FOLLOWLOCATION => false,
151
			CURLOPT_URL            => (string)$this->request->getUri()->withFragment(''),
152
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_2TLS,
153
			CURLOPT_USERAGENT      => $this->options->user_agent,
154
			CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
155
			CURLOPT_SSL_VERIFYPEER => true,
156
			CURLOPT_SSL_VERIFYHOST => 2,
157
			CURLOPT_CAINFO         => $this->options->ca_info,
158
			CURLOPT_TIMEOUT        => $this->options->timeout,
159
			CURLOPT_CONNECTTIMEOUT => 30,
160
			CURLOPT_WRITEFUNCTION  => [$this, 'writefunction'],
161
			CURLOPT_HEADERFUNCTION => [$this, 'headerfunction'],
162
		];
163
	}
164
165
	/**
166
	 * @param array $options
167
	 *
168
	 * @return array
169
	 */
170
	protected function setBodyOptions(array $options):array{
171
		$body     = $this->request->getBody();
172
		$bodySize = $body->getSize();
173
174
		if($bodySize === 0){
175
			return $options;
176
		}
177
178
		if($body->isSeekable()){
179
			$body->rewind();
180
		}
181
182
		// Message has non empty body.
183
		if($bodySize === null || $bodySize > 1 << 20){
184
			// Avoid full loading large or unknown size body into memory
185
			$options[CURLOPT_UPLOAD] = true;
186
187
			if($bodySize !== null){
188
				$options[CURLOPT_INFILESIZE] = $bodySize;
189
			}
190
191
			$options[CURLOPT_READFUNCTION] = [$this, 'readfunction'];
192
		}
193
		// Small body can be loaded into memory
194
		else{
195
			$options[CURLOPT_POSTFIELDS] = (string)$body;
196
		}
197
198
		return $options;
199
	}
200
201
	/**
202
	 * @param array $options
203
	 *
204
	 * @return array
205
	 */
206
	protected function setHeaderOptions(array $options):array{
207
		$headers = [];
208
209
		foreach($this->request->getHeaders() as $name => $values){
210
			$header = strtolower($name);
211
212
			// curl-client does not support "Expect-Continue", so dropping "expect" headers
213
			if($header === 'expect'){
214
				continue;
215
			}
216
217
			if($header === 'content-length'){
218
219
				// Small body content length can be calculated here.
220
				if(array_key_exists(CURLOPT_POSTFIELDS, $options)){
221
					$values = [strlen($options[CURLOPT_POSTFIELDS])];
222
				}
223
				// Else if there is no body, forcing "Content-length" to 0
224
				elseif(!array_key_exists(CURLOPT_READFUNCTION, $options)){
225
					$values = ['0'];
226
				}
227
228
			}
229
230
			foreach($values as $value){
231
				$value = (string)$value;
232
233
				// cURL requires a special format for empty headers.
234
				// See https://github.com/guzzle/guzzle/issues/1882 for more details.
235
				$headers[] = $value === '' ? $name.';' : $name.': '.$value;
236
			}
237
238
		}
239
240
		$options[CURLOPT_HTTPHEADER] = $headers;
241
242
		// If the Expect header is not present, prevent curl from adding it
243
		if(!$this->request->hasHeader('Expect')){
244
			$options[CURLOPT_HTTPHEADER][] = 'Expect:';
245
		}
246
247
		// cURL sometimes adds a content-type by default. Prevent this.
248
		if(!$this->request->hasHeader('Content-Type')){
249
			$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
250
		}
251
252
		return $options;
253
	}
254
255
	/**
256
	 * @return \chillerlan\HTTP\CurlUtils\CurlHandle
257
	 */
258
	public function init():CurlHandle{
259
		$options  = $this->initCurlOptions();
260
		$userinfo = $this->request->getUri()->getUserInfo();
261
		$method   = $this->request->getMethod();
262
263
		if(!empty($userinfo)){
264
			$options[CURLOPT_USERPWD] = $userinfo;
265
		}
266
267
		/*
268
		 * Some HTTP methods cannot have payload:
269
		 *
270
		 * - GET   — cURL will automatically change the method to PUT or POST
271
		 *           if we set CURLOPT_UPLOAD or CURLOPT_POSTFIELDS.
272
		 * - HEAD  — cURL treats HEAD as a GET request with same restrictions.
273
		 * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
274
		 */
275
276
		if(in_array($method, ['DELETE', 'PATCH', 'POST', 'PUT'], true)){
277
			$options = $this->setBodyOptions($options);
278
		}
279
280
		// This will set HTTP method to "HEAD".
281
		if($method === 'HEAD'){
282
			$options[CURLOPT_NOBODY] = true;
283
		}
284
285
		// GET is a default method. Other methods should be specified explicitly.
286
		if($method !== 'GET'){
287
			$options[CURLOPT_CUSTOMREQUEST] = $method;
288
		}
289
290
		// overwrite the default values with $curl_options
291
		foreach($this->options->curl_options as $k => $v){
292
			// skip some options that are only set automatically
293
			if(in_array($k, [CURLOPT_HTTPHEADER, CURLOPT_CUSTOMREQUEST, CURLOPT_NOBODY], true)){
294
				continue;
295
			}
296
297
			$options[$k] = $v;
298
		}
299
300
		$options = $this->setHeaderOptions($options);
301
302
		curl_setopt_array($this->curl, $options);
303
304
		return $this;
305
	}
306
307
	/**
308
	 * @internal
309
	 *
310
	 * @param resource $curl
311
	 * @param resource $stream
312
	 * @param int      $length
313
	 *
314
	 * @return string
315
	 */
316
	public function readfunction($curl, $stream, int $length):string{
317
		return $this->request->getBody()->read($length);
318
	}
319
320
	/**
321
	 * @internal
322
	 *
323
	 * @param resource $curl
324
	 * @param string   $data
325
	 *
326
	 * @return int
327
	 */
328
	public function writefunction($curl, string $data):int{
329
		return $this->response->getBody()->write($data);
330
	}
331
332
	/**
333
	 * @internal
334
	 *
335
	 * @param resource $curl
336
	 * @param string   $line
337
	 *
338
	 * @return int
339
	 */
340
	public function headerfunction($curl, string $line):int{
341
		$str    = trim($line);
342
		$header = explode(':', $str, 2);
343
344
		if(count($header) === 2){
345
			$this->response = $this->response
346
				->withAddedHeader(trim($header[0]), trim($header[1]));
347
		}
348
		elseif(substr(strtoupper($str), 0, 5) === 'HTTP/'){
349
			$status = explode(' ', $str, 3);
350
			$reason = count($status) > 2 ? trim($status[2]) : '';
351
352
			$this->response = $this->response
353
				->withStatus((int)$status[1], $reason)
354
				->withProtocolVersion(substr($status[0], 5))
355
			;
356
		}
357
358
		return strlen($line);
359
	}
360
361
}
362