Completed
Push — master ( 78d4e7...979ecb )
by smiley
02:57
created

CurlHandle::init()   F

Complexity

Conditions 23
Paths 8064

Size

Total Lines 130

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 23
nc 8064
nop 0
dl 0
loc 130
rs 0
c 0
b 0
f 0

How to fix   Long Method    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
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP;
14
15
use chillerlan\Settings\SettingsContainerInterface;
16
use Psr\Http\Message\{RequestInterface, ResponseInterface};
17
18
class CurlHandle{
19
20
	/**
21
	 * The cURL handle
22
	 *
23
	 * @var resource
24
	 */
25
	public $ch;
26
27
	/**
28
	 * @var \Psr\Http\Message\RequestInterface
29
	 */
30
	public $request;
31
32
	/**
33
	 * @var \Psr\Http\Message\ResponseInterface
34
	 */
35
	public $response;
36
37
	/**
38
	 * @var \chillerlan\HTTP\HTTPOptions
39
	 */
40
	public $options;
41
42
	/**
43
	 * CurlHandle constructor.
44
	 *
45
	 * @param \Psr\Http\Message\RequestInterface              $request
46
	 * @param \Psr\Http\Message\ResponseInterface             $response
47
	 * @param \chillerlan\Settings\SettingsContainerInterface $options
48
	 */
49
	public function __construct(RequestInterface $request, ResponseInterface $response, SettingsContainerInterface $options){
50
		$this->request  = $request;
51
		$this->response = $response;
52
		$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...
53
		$this->ch       = curl_init();
54
55
		if(is_array($this->options->curl_options)){
56
			curl_setopt_array($this->ch, $this->options->curl_options);
57
		}
58
	}
59
60
	/**
61
	 * close an existing cURL handle on exit
62
	 */
63
	public function __destruct(){
64
		$this->close();
65
	}
66
67
	/**
68
	 * @return void
69
	 */
70
	public function close():void{
71
72
		if(is_resource($this->ch)){
73
			curl_close($this->ch);
74
		}
75
76
	}
77
78
	/**
79
	 * @return void
80
	 */
81
	public function reset():void{
82
83
		if(is_resource($this->ch)){
84
85
			curl_setopt_array($this->ch, [
86
				CURLOPT_HEADERFUNCTION   => null,
87
				CURLOPT_READFUNCTION     => null,
88
				CURLOPT_WRITEFUNCTION    => null,
89
				CURLOPT_PROGRESSFUNCTION => null,
90
			]);
91
92
			curl_reset($this->ch);
93
		}
94
95
	}
96
97
	/**
98
	 * @return \Psr\Http\Message\ResponseInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be resource?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
99
	 */
100
	public function init(){
101
102
		$v = $this->request->getProtocolVersion();
103
104
		switch($v){
105
			case '1.0': $v = CURL_HTTP_VERSION_1_0; break;
106
			case '1.1': $v = CURL_HTTP_VERSION_1_1; break;
107
			case '2.0': $v = CURL_HTTP_VERSION_2_0; break;
108
			default:    $v = CURL_HTTP_VERSION_NONE;
109
		}
110
111
		$options = [
112
			CURLOPT_HEADER         => false,
113
			CURLOPT_RETURNTRANSFER => false,
114
			CURLOPT_FOLLOWLOCATION => false,
115
			CURLOPT_URL            => (string)$this->request->getUri()->withFragment(''),
116
			CURLOPT_HTTP_VERSION   => $v,
117
			CURLOPT_USERAGENT      => $this->options->user_agent,
118
			CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
119
			CURLOPT_SSL_VERIFYPEER => true,
120
			CURLOPT_SSL_VERIFYHOST => 2,
121
			CURLOPT_CAINFO         => is_file($this->options->ca_info) ? $this->options->ca_info : null,
122
			CURLOPT_TIMEOUT        => (int)$this->options->timeout,
123
			CURLOPT_CONNECTTIMEOUT => 30,
124
			CURLOPT_WRITEFUNCTION  => [$this, 'writefunction'],
125
			CURLOPT_HEADERFUNCTION => [$this, 'headerfunction'],
126
		];
127
128
		$userinfo = $this->request->getUri()->getUserInfo();
129
130
		if(!empty($userinfo)){
131
			$options[CURLOPT_USERPWD] = $userinfo;
132
		}
133
134
		/*
135
		 * Some HTTP methods cannot have payload:
136
		 *
137
		 * - GET   — cURL will automatically change method to PUT or POST
138
		 *           if we set CURLOPT_UPLOAD or CURLOPT_POSTFIELDS.
139
		 * - HEAD  — cURL treats HEAD as GET request with a same restrictions.
140
		 * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
141
		 */
142
		$method   = $this->request->getMethod();
143
		$body     = $this->request->getBody();
144
		$bodySize = $body->getSize();
145
146
		if(in_array($method, ['DELETE', 'PATCH', 'POST', 'PUT'], true) && $bodySize !== 0){
147
148
			if($body->isSeekable()){
149
				$body->rewind();
150
			}
151
152
			// Message has non empty body.
153
			if($bodySize === null || $bodySize > 1024 * 1024){
154
				// Avoid full loading large or unknown size body into memory
155
				$options[CURLOPT_UPLOAD] = true;
156
157
				if($bodySize !== null){
158
					$options[CURLOPT_INFILESIZE] = $bodySize;
159
				}
160
161
				$options[CURLOPT_READFUNCTION] = [$this, 'readfunction'];
162
			}
163
			// Small body can be loaded into memory
164
			else{
165
				$options[CURLOPT_POSTFIELDS] = (string)$body;
166
			}
167
168
		}
169
170
		// This will set HTTP method to "HEAD".
171
		if($method === 'HEAD'){
172
			$options[CURLOPT_NOBODY] = true;
173
		}
174
		// GET is a default method. Other methods should be specified explicitly.
175
		elseif($method !== 'GET'){
176
			$options[CURLOPT_CUSTOMREQUEST] = $method;
177
		}
178
179
		$curlHeaders = [];
180
181
		foreach($this->request->getHeaders() as $name => $values){
182
			$header = strtolower($name);
183
184
			// curl-client does not support "Expect-Continue", so dropping "expect" headers
185
			if($header === 'expect'){
186
				continue;
187
			}
188
189
			if($header === 'content-length'){
190
191
				// Small body content length can be calculated here.
192
				if(array_key_exists(CURLOPT_POSTFIELDS, $options)){
193
					$values = [strlen($options[CURLOPT_POSTFIELDS])];
194
				}
195
				// Else if there is no body, forcing "Content-length" to 0
196
				elseif(!array_key_exists(CURLOPT_READFUNCTION, $options)){
197
					$values = ['0'];
198
				}
199
200
			}
201
202
			foreach($values as $value){
203
				$value = (string)$value;
204
205
				// cURL requires a special format for empty headers.
206
				// See https://github.com/guzzle/guzzle/issues/1882 for more details.
207
				$curlHeaders[] = $value === ''
208
					? $name.';'
209
					: $name.': '.$value;
210
			}
211
212
		}
213
214
		$options[CURLOPT_HTTPHEADER] = $curlHeaders;
215
216
		// If the Expect header is not present, prevent curl from adding it
217
		if (!$this->request->hasHeader('Expect')) {
218
			$options[CURLOPT_HTTPHEADER][] = 'Expect:';
219
		}
220
221
		// cURL sometimes adds a content-type by default. Prevent this.
222
		if (!$this->request->hasHeader('Content-Type')) {
223
			$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
224
		}
225
226
		curl_setopt_array($this->ch, $this->options->curl_options + $options);
227
228
		return $this->ch;
229
	}
230
231
	/**
232
	 * @param resource $curl
233
	 * @param resource $stream
234
	 * @param int      $length
235
	 *
236
	 * @return string
237
	 */
238
	public function readfunction($curl, $stream, int $length):string{
239
		return $this->request->getBody()->read($length);
240
	}
241
242
	/**
243
	 * @param resource $curl
244
	 * @param string   $data
245
	 *
246
	 * @return int
247
	 */
248
	public function writefunction($curl, string $data):int{
249
		return $this->response->getBody()->write($data);
250
	}
251
252
	/**
253
	 * @param resource $curl
254
	 * @param string   $line
255
	 *
256
	 * @return int
257
	 */
258
259
	public function headerfunction($curl, string $line):int{
260
		$str    = trim($line);
261
		$header = explode(':', $str, 2);
262
263
		if(count($header) === 2){
264
			$this->response = $this->response
265
				->withAddedHeader(trim($header[0]), trim($header[1]));
266
		}
267
		elseif(substr(strtoupper($str), 0, 5) === 'HTTP/'){
268
			$status = explode(' ', $str, 3);
269
			$reason = count($status) > 2 ? trim($status[2]) : '';
270
271
			$this->response = $this->response
272
				->withStatus((int)$status[1], $reason)
273
				->withProtocolVersion(substr($status[0], 5));
274
		}
275
276
		return strlen($line);
277
	}
278
279
}
280