Completed
Push — master ( 9fb761...cb070b )
by smiley
01:28
created

CurlHandle::setHeaderOptions()   B

Complexity

Conditions 10
Paths 24

Size

Total Lines 48

Duplication

Lines 7
Ratio 14.58 %

Importance

Changes 0
Metric Value
cc 10
nc 24
nop 1
dl 7
loc 48
rs 7.2678
c 0
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_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;
0 ignored issues
show
Documentation Bug introduced by
$options is of type object<chillerlan\Settin...ingsContainerInterface>, but the property $options was declared to be of type object<chillerlan\HTTP\HTTPOptions>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof 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 given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

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