Completed
Push — master ( 2baebf...b22d69 )
by smiley
01:43
created

CurlHandle::close()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 7
rs 10
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
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP\Psr18;
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 $curl;
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
	protected $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->curl     = curl_init();
54
55
		curl_setopt_array($this->curl, $this->options->curl_options);
56
	}
57
58
	/**
59
	 * close an existing cURL handle on exit
60
	 */
61
	public function __destruct(){
62
		$this->close();
63
	}
64
65
	/**
66
	 * @return void
67
	 */
68
	public function close():void{
69
70
		if(is_resource($this->curl)){
71
			curl_close($this->curl);
72
		}
73
74
	}
75
76
	/**
77
	 * @return void
78
	 */
79
	public function reset():void{
80
81
		if(is_resource($this->curl)){
82
83
			curl_setopt_array($this->curl, [
84
				CURLOPT_HEADERFUNCTION   => null,
85
				CURLOPT_READFUNCTION     => null,
86
				CURLOPT_WRITEFUNCTION    => null,
87
				CURLOPT_PROGRESSFUNCTION => null,
88
			]);
89
90
			curl_reset($this->curl);
91
		}
92
93
	}
94
95
	/**
96
	 * @return array
97
	 */
98
	protected function initCurlOptions():array{
99
100
		$v = [
101
			'1.0' => CURL_HTTP_VERSION_1_0,
102
			'1.1' => CURL_HTTP_VERSION_1_1,
103
			'2.0' => CURL_HTTP_VERSION_2_0,
104
		];
105
106
		return [
107
			CURLOPT_HEADER         => false,
108
			CURLOPT_RETURNTRANSFER => false,
109
			CURLOPT_FOLLOWLOCATION => false,
110
			CURLOPT_URL            => (string)$this->request->getUri()->withFragment(''),
111
			CURLOPT_HTTP_VERSION   => $v[$this->request->getProtocolVersion()] ?? CURL_HTTP_VERSION_NONE,
112
			CURLOPT_USERAGENT      => $this->options->user_agent,
113
			CURLOPT_PROTOCOLS      => CURLPROTO_HTTP | CURLPROTO_HTTPS,
114
			CURLOPT_SSL_VERIFYPEER => true,
115
			CURLOPT_SSL_VERIFYHOST => 2,
116
			CURLOPT_CAINFO         => $this->options->ca_info,
117
			CURLOPT_TIMEOUT        => 10,
118
			CURLOPT_CONNECTTIMEOUT => 30,
119
			CURLOPT_WRITEFUNCTION  => [$this, 'writefunction'],
120
			CURLOPT_HEADERFUNCTION => [$this, 'headerfunction'],
121
		];
122
	}
123
124
	/**
125
	 * @param array $options
126
	 *
127
	 * @return void
128
	 */
129
	protected function setBodyOptions(array &$options):void{
130
		$body     = $this->request->getBody();
131
		$bodySize = $body->getSize();
132
133
		if($bodySize === 0){
134
			return;
135
		}
136
137
		if($body->isSeekable()){
138
			$body->rewind();
139
		}
140
141
		// Message has non empty body.
142
		if($bodySize === null || $bodySize > 1 << 20){
143
			// Avoid full loading large or unknown size body into memory
144
			$options[CURLOPT_UPLOAD] = true;
145
146
			if($bodySize !== null){
147
				$options[CURLOPT_INFILESIZE] = $bodySize;
148
			}
149
150
			$options[CURLOPT_READFUNCTION] = [$this, 'readfunction'];
151
		}
152
		// Small body can be loaded into memory
153
		else{
154
			$options[CURLOPT_POSTFIELDS] = (string)$body;
155
		}
156
157
	}
158
159
	/**
160
	 * @param array $options
161
	 *
162
	 * @return array
163
	 */
164
	protected function initCurlHeaders(array $options):array{
165
		$headers = [];
166
167
		foreach($this->request->getHeaders() as $name => $values){
168
			$header = strtolower($name);
169
170
			// curl-client does not support "Expect-Continue", so dropping "expect" headers
171
			if($header === 'expect'){
172
				continue;
173
			}
174
175
			if($header === 'content-length'){
176
177
				// Small body content length can be calculated here.
178
				if(array_key_exists(CURLOPT_POSTFIELDS, $options)){
179
					$values = [strlen($options[CURLOPT_POSTFIELDS])];
180
				}
181
				// Else if there is no body, forcing "Content-length" to 0
182
				elseif(!array_key_exists(CURLOPT_READFUNCTION, $options)){
183
					$values = ['0'];
184
				}
185
186
			}
187
188 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...
189
				$value = (string)$value;
190
191
				// cURL requires a special format for empty headers.
192
				// See https://github.com/guzzle/guzzle/issues/1882 for more details.
193
				$headers[] = $value === '' ? $name.';' : $name.': '.$value;
194
			}
195
196
		}
197
198
		return $headers;
199
	}
200
201
	/**
202
	 * @return resource cURL handle
203
	 */
204
	public function init(){
205
		$options = $this->initCurlOptions();
206
207
		$userinfo = $this->request->getUri()->getUserInfo();
208
209
		if(!empty($userinfo)){
210
			$options[CURLOPT_USERPWD] = $userinfo;
211
		}
212
213
		/*
214
		 * Some HTTP methods cannot have payload:
215
		 *
216
		 * - GET   — cURL will automatically change method to PUT or POST
217
		 *           if we set CURLOPT_UPLOAD or CURLOPT_POSTFIELDS.
218
		 * - HEAD  — cURL treats HEAD as GET request with a same restrictions.
219
		 * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
220
		 */
221
		$method = $this->request->getMethod();
222
223
		if(in_array($method, ['DELETE', 'PATCH', 'POST', 'PUT'], true)){
224
			$this->setBodyOptions($options);
225
		}
226
227
		// This will set HTTP method to "HEAD".
228
		if($method === 'HEAD'){
229
			$options[CURLOPT_NOBODY] = true;
230
		}
231
232
		// GET is a default method. Other methods should be specified explicitly.
233
		if($method !== 'GET'){
234
			$options[CURLOPT_CUSTOMREQUEST] = $method;
235
		}
236
237
		$options[CURLOPT_HTTPHEADER] = $this->initCurlHeaders($options);
238
239
		// If the Expect header is not present, prevent curl from adding it
240
		if(!$this->request->hasHeader('Expect')){
241
			$options[CURLOPT_HTTPHEADER][] = 'Expect:';
242
		}
243
244
		// cURL sometimes adds a content-type by default. Prevent this.
245
		if(!$this->request->hasHeader('Content-Type')){
246
			$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
247
		}
248
249
		// overwrite the default values with $curl_options
250
		// @todo: callback/middleware for the cURL options array?
251
		curl_setopt_array($this->curl, $options + $this->options->curl_options);
252
253
		return $this->curl;
254
	}
255
256
	/**
257
	 * @param resource $curl
258
	 * @param resource $stream
259
	 * @param int      $length
260
	 *
261
	 * @return string
262
	 */
263
	public function readfunction($curl, $stream, int $length):string{
264
		return $this->request->getBody()->read($length);
265
	}
266
267
	/**
268
	 * @param resource $curl
269
	 * @param string   $data
270
	 *
271
	 * @return int
272
	 */
273
	public function writefunction($curl, string $data):int{
274
		return $this->response->getBody()->write($data);
275
	}
276
277
	/**
278
	 * @param resource $curl
279
	 * @param string   $line
280
	 *
281
	 * @return int
282
	 */
283
284
	public function headerfunction($curl, string $line):int{
285
		$str    = trim($line);
286
		$header = explode(':', $str, 2);
287
288
		if(count($header) === 2){
289
			$this->response = $this->response
290
				->withAddedHeader(trim($header[0]), trim($header[1]));
291
		}
292
		elseif(substr(strtoupper($str), 0, 5) === 'HTTP/'){
293
			$status = explode(' ', $str, 3);
294
			$reason = count($status) > 2 ? trim($status[2]) : '';
295
296
			$this->response = $this->response
297
				->withStatus((int)$status[1], $reason)
298
				->withProtocolVersion(substr($status[0], 5));
299
		}
300
301
		return strlen($line);
302
	}
303
304
}
305