Completed
Push — master ( 3c4fae...40d490 )
by smiley
01:25
created

CurlHandle::initCurlOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 18
rs 9.6666
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\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 void
0 ignored issues
show
Documentation introduced by
Should the return type not be CurlHandle?

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...
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 void
0 ignored issues
show
Documentation introduced by
Should the return type not be CurlHandle?

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...
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 => false,
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 void
144
	 */
145
	protected function setBodyOptions(array &$options):void{
146
		$body     = $this->request->getBody();
147
		$bodySize = $body->getSize();
148
149
		if($bodySize === 0){
150
			return;
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
	}
174
175
	/**
176
	 * @param array $options
177
	 *
178
	 * @return array
179
	 */
180
	protected function initCurlHeaders(array $options):array{
181
		$headers = [];
182
183
		foreach($this->request->getHeaders() as $name => $values){
184
			$header = strtolower($name);
185
186
			// curl-client does not support "Expect-Continue", so dropping "expect" headers
187
			if($header === 'expect'){
188
				continue;
189
			}
190
191
			if($header === 'content-length'){
192
193
				// Small body content length can be calculated here.
194
				if(array_key_exists(CURLOPT_POSTFIELDS, $options)){
195
					$values = [strlen($options[CURLOPT_POSTFIELDS])];
196
				}
197
				// Else if there is no body, forcing "Content-length" to 0
198
				elseif(!array_key_exists(CURLOPT_READFUNCTION, $options)){
199
					$values = ['0'];
200
				}
201
202
			}
203
204 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...
205
				$value = (string)$value;
206
207
				// cURL requires a special format for empty headers.
208
				// See https://github.com/guzzle/guzzle/issues/1882 for more details.
209
				$headers[] = $value === '' ? $name.';' : $name.': '.$value;
210
			}
211
212
		}
213
214
		return $headers;
215
	}
216
217
	/**
218
	 * @return \chillerlan\HTTP\CurlUtils\CurlHandleInterface
219
	 */
220
	public function init():CurlHandleInterface{
221
		$options  = $this->initCurlOptions();
222
		$userinfo = $this->request->getUri()->getUserInfo();
223
		$method   = $this->request->getMethod();
224
225
		if(!empty($userinfo)){
226
			$options[CURLOPT_USERPWD] = $userinfo;
227
		}
228
229
		/*
230
		 * Some HTTP methods cannot have payload:
231
		 *
232
		 * - GET   — cURL will automatically change the method to PUT or POST
233
		 *           if we set CURLOPT_UPLOAD or CURLOPT_POSTFIELDS.
234
		 * - HEAD  — cURL treats HEAD as a GET request with same restrictions.
235
		 * - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
236
		 */
237
238
		if(in_array($method, ['DELETE', 'PATCH', 'POST', 'PUT'], true)){
239
			$this->setBodyOptions($options);
240
		}
241
242
		// This will set HTTP method to "HEAD".
243
		if($method === 'HEAD'){
244
			$options[CURLOPT_NOBODY] = true;
245
		}
246
247
		// GET is a default method. Other methods should be specified explicitly.
248
		if($method !== 'GET'){
249
			$options[CURLOPT_CUSTOMREQUEST] = $method;
250
		}
251
252
		// overwrite the default values with $curl_options
253
		foreach($this->options->curl_options as $k => $v){
254
			// skip some options that are only set automatically
255
			if(in_array($k, [CURLOPT_HTTPHEADER, CURLOPT_CUSTOMREQUEST, CURLOPT_NOBODY], true)){
256
				continue;
257
			}
258
259
			$options[$k] = $v;
260
		}
261
262
		$options[CURLOPT_HTTPHEADER] = $this->initCurlHeaders($options);
263
264
		// If the Expect header is not present, prevent curl from adding it
265
		if(!$this->request->hasHeader('Expect')){
266
			$options[CURLOPT_HTTPHEADER][] = 'Expect:';
267
		}
268
269
		// cURL sometimes adds a content-type by default. Prevent this.
270
		if(!$this->request->hasHeader('Content-Type')){
271
			$options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
272
		}
273
274
		curl_setopt_array($this->curl, $options);
275
276
		return $this;
277
	}
278
279
	/**
280
	 * @param resource $curl
281
	 * @param resource $stream
282
	 * @param int      $length
283
	 *
284
	 * @return string
285
	 */
286
	public function readfunction($curl, $stream, int $length):string{
287
		return $this->request->getBody()->read($length);
288
	}
289
290
	/**
291
	 * @param resource $curl
292
	 * @param string   $data
293
	 *
294
	 * @return int
295
	 */
296
	public function writefunction($curl, string $data):int{
297
		return $this->response->getBody()->write($data);
298
	}
299
300
	/**
301
	 * @param resource $curl
302
	 * @param string   $line
303
	 *
304
	 * @return int
305
	 */
306
307
	public function headerfunction($curl, string $line):int{
308
		$str    = trim($line);
309
		$header = explode(':', $str, 2);
310
311
		if(count($header) === 2){
312
			$this->response = $this->response
313
				->withAddedHeader(trim($header[0]), trim($header[1]));
314
		}
315
		elseif(substr(strtoupper($str), 0, 5) === 'HTTP/'){
316
			$status = explode(' ', $str, 3);
317
			$reason = count($status) > 2 ? trim($status[2]) : '';
318
319
			$this->response = $this->response
320
				->withStatus((int)$status[1], $reason)
321
				->withProtocolVersion(substr($status[0], 5))
322
			;
323
		}
324
325
		return strlen($line);
326
	}
327
328
}
329