Use unicode literal in StringUtil
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / HTTPRequest.class.php
CommitLineData
86fc0430 1<?php
308c880f 2declare(strict_types=1);
86fc0430 3namespace wcf\util;
3536d2fe
AE
4use wcf\system\exception\HTTPNotFoundException;
5use wcf\system\exception\HTTPServerErrorException;
6use wcf\system\exception\HTTPUnauthorizedException;
86fc0430
TD
7use wcf\system\exception\SystemException;
8use wcf\system\io\RemoteFile;
9use wcf\system\Regex;
10use wcf\system\WCF;
3502a7f5 11use wcf\util\exception\HTTPException;
86fc0430
TD
12
13/**
8fe14bd6 14 * Sends HTTP/1.1 requests.
86fc0430
TD
15 * It supports POST, SSL, Basic Auth etc.
16 *
3536d2fe 17 * @author Tim Duesterhus
cea1798f 18 * @copyright 2001-2017 WoltLab GmbH
86fc0430 19 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4 20 * @package WoltLabSuite\Core\Util
86fc0430 21 */
a195ffa6 22final class HTTPRequest {
86fc0430
TD
23 /**
24 * given options
a17de04e 25 * @var array
86fc0430 26 */
058cbd6a 27 private $options = [];
86fc0430
TD
28
29 /**
30 * given post parameters
a17de04e 31 * @var array
86fc0430 32 */
058cbd6a 33 private $postParameters = [];
86fc0430 34
5f70a0de
TD
35 /**
36 * given files
06355ec3 37 * @var array
5f70a0de 38 */
058cbd6a 39 private $files = [];
5f70a0de 40
86fc0430 41 /**
60f613e2 42 * indicates if request will be made via SSL
a17de04e 43 * @var boolean
86fc0430
TD
44 */
45 private $useSSL = false;
46
5819f701
TD
47 /**
48 * indicates if the connection to the proxy target will be made via SSL
49 * @var boolean
50 */
51 private $originUseSSL = false;
52
86fc0430
TD
53 /**
54 * target host
a17de04e 55 * @var string
86fc0430
TD
56 */
57 private $host;
58
5819f701
TD
59 /**
60 * target host if a proxy is used
61 * @var string
62 */
63 private $originHost;
64
86fc0430
TD
65 /**
66 * target port
a17de04e 67 * @var integer
86fc0430
TD
68 */
69 private $port;
a17de04e 70
5819f701
TD
71 /**
72 * target port if a proxy is used
73 * @var integer
74 */
75 private $originPort;
76
86fc0430
TD
77 /**
78 * target path
a17de04e 79 * @var string
86fc0430
TD
80 */
81 private $path;
82
83 /**
84 * target query string
a17de04e 85 * @var string
86fc0430
TD
86 */
87 private $query;
88
25cdc083
AE
89 /**
90 * request URL
91 * @var string
92 */
93 private $url = '';
94
86fc0430
TD
95 /**
96 * request headers
5fffbcb8 97 * @var string[][]
86fc0430 98 */
058cbd6a 99 private $headers = [];
86fc0430 100
8fe14bd6
TD
101 /**
102 * legacy headers
7a23a706 103 * @var string[]
8fe14bd6 104 */
058cbd6a 105 private $legacyHeaders = [];
8fe14bd6 106
5f70a0de
TD
107 /**
108 * request body
109 * @var string
110 */
111 private $body = '';
112
86fc0430
TD
113 /**
114 * reply headers
7a23a706 115 * @var string[]
86fc0430 116 */
058cbd6a 117 private $replyHeaders = [];
86fc0430
TD
118
119 /**
120 * reply body
a17de04e 121 * @var string
86fc0430
TD
122 */
123 private $replyBody = '';
124
125 /**
126 * reply status code
a17de04e 127 * @var integer
86fc0430
TD
128 */
129 private $statusCode = 0;
130
131 /**
a17de04e 132 * Constructs a new instance of HTTPRequest.
86fc0430
TD
133 *
134 * @param string $url URL to connect to
7a23a706 135 * @param string[] $options
1b20f990 136 * @param mixed $postParameters Parameters to send via POST
5f70a0de 137 * @param array $files Files to attach to the request
86fc0430 138 */
058cbd6a 139 public function __construct($url, array $options = [], $postParameters = [], array $files = []) {
86fc0430
TD
140 $this->setURL($url);
141
142 $this->postParameters = $postParameters;
5f70a0de 143 $this->files = $files;
86fc0430
TD
144
145 $this->setOptions($options);
146
a195ffa6 147 // set default headers
359841c3 148 $this->addHeader('user-agent', "HTTP.PHP (HTTPRequest.class.php; WoltLab Suite/".WCF_VERSION."; ".WCF::getLanguage()->languageCode.")");
8fe14bd6
TD
149 $this->addHeader('accept', '*/*');
150 $this->addHeader('accept-language', WCF::getLanguage()->getFixedLanguageCode());
151
4d28d5a2 152 if (isset($this->options['maxLength'])) {
5262964b 153 $this->addHeader('Range', 'bytes=0-'.($this->options['maxLength'] - 1));
4d28d5a2
SG
154 }
155
86fc0430 156 if ($this->options['method'] !== 'GET') {
5f70a0de 157 if (empty($this->files)) {
1b20f990
AE
158 if (is_array($postParameters)) {
159 $this->body = http_build_query($this->postParameters, '', '&');
160 }
161 else if (is_string($postParameters) && !empty($postParameters)) {
162 $this->body = $postParameters;
163 }
164
8fe14bd6 165 $this->addHeader('content-type', 'application/x-www-form-urlencoded');
5f70a0de
TD
166 }
167 else {
168 $boundary = StringUtil::getRandomID();
8fe14bd6 169 $this->addHeader('content-type', 'multipart/form-data; boundary='.$boundary);
5f70a0de
TD
170
171 // source of the iterators: http://stackoverflow.com/a/7623716/782822
172 if (!empty($this->postParameters)) {
173 $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($this->postParameters), \RecursiveIteratorIterator::SELF_FIRST);
174 foreach ($iterator as $k => $v) {
fc52a845 175 /** @noinspection PhpUndefinedMethodInspection */
5f70a0de
TD
176 if (!$iterator->hasChildren()) {
177 $key = '';
178 for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) {
179 if ($i === 0) $key .= $iterator->getSubIterator($i)->key();
180 else $key .= '['.$iterator->getSubIterator($i)->key().']';
181 }
182
183 $this->body .= "--".$boundary."\r\n";
184 $this->body .= 'Content-Disposition: form-data; name="'.$key.'"'."\r\n\r\n";
185 $this->body .= $v."\r\n";
186 }
187 }
188 }
189
190 $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($this->files), \RecursiveIteratorIterator::SELF_FIRST);
191 foreach ($iterator as $k => $v) {
fc52a845 192 /** @noinspection PhpUndefinedMethodInspection */
5f70a0de
TD
193 if (!$iterator->hasChildren()) {
194 $key = '';
195 for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) {
196 if ($i === 0) $key .= $iterator->getSubIterator($i)->key();
197 else $key .= '['.$iterator->getSubIterator($i)->key().']';
198 }
199
200 $this->body .= "--".$boundary."\r\n";
201 $this->body .= 'Content-Disposition: form-data; name="'.$k.'"; filename="'.basename($v).'"'."\r\n";
202 $this->body .= 'Content-Type: '.(FileUtil::getMimeType($v) ?: 'application/octet-stream.')."\r\n\r\n";
203 $this->body .= file_get_contents($v)."\r\n";
204 }
205 }
206
207 $this->body .= "--".$boundary."--";
208 }
8fe14bd6 209 $this->addHeader('content-length', strlen($this->body));
86fc0430
TD
210 }
211 if (isset($this->options['auth'])) {
8fe14bd6 212 $this->addHeader('authorization', "Basic ".base64_encode($options['auth']['username'].":".$options['auth']['password']));
86fc0430 213 }
8fe14bd6 214 $this->addHeader('connection', 'Close');
86fc0430
TD
215 }
216
217 /**
218 * Parses the given URL and applies PROXY_SERVER_HTTP.
219 *
a17de04e 220 * @param string $url
cacb447d 221 * @throws SystemException
86fc0430
TD
222 */
223 private function setURL($url) {
27930682 224 $parsedUrl = $originUrl = Url::parse($url);
f8ee98c6
MW
225 if (empty($originUrl['scheme']) || empty($originUrl['host'])) {
226 throw new SystemException("Invalid URL '{$url}' given");
227 }
63f9ff4e
TD
228
229 $this->originUseSSL = $originUrl['scheme'] === 'https';
230 $this->originHost = $originUrl['host'];
231 $this->originPort = isset($originUrl['port']) ? $originUrl['port'] : ($this->originUseSSL ? 443 : 80);
232
63f9ff4e 233 if (PROXY_SERVER_HTTP && !$this->originUseSSL) {
86fc0430 234 $this->path = $url;
1d50541d 235 $this->query = '';
86fc0430
TD
236 }
237 else {
86fc0430 238 $this->path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
1d50541d 239 $this->query = isset($parsedUrl['query']) ? $parsedUrl['query'] : '';
86fc0430 240 }
d5bd7602 241
27930682
AE
242 if (PROXY_SERVER_HTTP && Url::is(PROXY_SERVER_HTTP)) {
243 $parsedUrl = Url::parse(PROXY_SERVER_HTTP);
d3ee6dcb
TD
244 }
245
86fc0430
TD
246 $this->useSSL = $parsedUrl['scheme'] === 'https';
247 $this->host = $parsedUrl['host'];
248 $this->port = isset($parsedUrl['port']) ? $parsedUrl['port'] : ($this->useSSL ? 443 : 80);
d5bd7602
AE
249
250 // update the 'Host:' header if URL has changed
adc039a5 251 if ($this->url != $url) {
5819f701 252 $this->addHeader('host', $this->originHost.($this->originPort != ($this->originUseSSL ? 443 : 80) ? ':'.$this->originPort : ''));
d5bd7602
AE
253 }
254
255 $this->url = $url;
86fc0430
TD
256 }
257
258 /**
259 * Executes the HTTP request.
260 */
261 public function execute() {
262 // connect
058cbd6a
MS
263 $remoteFile = new RemoteFile(($this->useSSL ? 'ssl://' : '').$this->host, $this->port, $this->options['timeout'], [
264 'ssl' => [
7e6297c8 265 'peer_name' => $this->originHost
058cbd6a
MS
266 ]
267 ]);
86fc0430 268
5819f701
TD
269 if ($this->originUseSSL && PROXY_SERVER_HTTP) {
270 if ($this->useSSL) throw new SystemException("Unable to proxy HTTPS when using TLS for proxy connection");
271
272 $request = "CONNECT ".$this->originHost.":".$this->originPort." HTTP/1.0\r\n";
273 if (isset($this->headers['user-agent'])) {
274 $request .= 'user-agent: '.reset($this->headers['user-agent'])."\r\n";
275 }
276 $request .= "Host: ".$this->originHost.":".$this->originPort."\r\n";
277 $request .= "\r\n";
278 $remoteFile->puts($request);
058cbd6a 279 $this->replyHeaders = [];
5819f701
TD
280 while (!$remoteFile->eof()) {
281 $line = $remoteFile->gets();
282 if (rtrim($line) === '') {
283 $this->parseReplyHeaders();
284
285 break;
286 }
287 $this->replyHeaders[] = $line;
288 }
3502a7f5 289 if ($this->statusCode != 200) throw new HTTPException($this, "Expected 200 Ok as reply to my CONNECT", $this->statusCode);
5819f701
TD
290 $remoteFile->setTLS(true);
291 }
292
8fe14bd6 293 $request = $this->options['method']." ".$this->path.($this->query ? '?'.$this->query : '')." HTTP/1.1\r\n";
86fc0430 294
a195ffa6 295 // add headers
86fc0430
TD
296 foreach ($this->headers as $name => $values) {
297 foreach ($values as $value) {
298 $request .= $name.": ".$value."\r\n";
299 }
300 }
301 $request .= "\r\n";
8fe14bd6 302
a195ffa6 303 // add post parameters
5f70a0de 304 if ($this->options['method'] !== 'GET') $request .= $this->body."\r\n\r\n";
d5bd7602 305
86fc0430
TD
306 $remoteFile->puts($request);
307
308 $inHeader = true;
058cbd6a 309 $this->replyHeaders = [];
86fc0430 310 $this->replyBody = '';
8fe14bd6 311 $chunkLength = 0;
5262964b 312 $bodyLength = 0;
c7fe2510 313
8d2262a1 314 $chunkedTransferRegex = new Regex('(^|,)[ \t]*chunked[ \t]*$', Regex::CASE_INSENSITIVE);
7ced4c6a
TD
315 // read http response, until one of is true
316 // a) EOF is reached
317 // b) bodyLength is at least maxLength
318 // c) bodyLength is at least Content-Length
319 while (!(
320 $remoteFile->eof() ||
321 (isset($this->options['maxLength']) && $bodyLength >= $this->options['maxLength']) ||
322 (isset($this->replyHeaders['content-length']) && $bodyLength >= end($this->replyHeaders['content-length']))
323 )) {
324
8fe14bd6 325 if ($chunkLength) {
5262964b 326 if (isset($this->options['maxLength'])) $chunkLength = min($chunkLength, $this->options['maxLength'] - $bodyLength);
8fe14bd6
TD
327 $line = $remoteFile->read($chunkLength);
328 }
8d2262a1 329 else if (!$inHeader && (!isset($this->replyHeaders['transfer-encoding']) || !$chunkedTransferRegex->match(end($this->replyHeaders['transfer-encoding'])))) {
4b3a6b71
TD
330 $length = 1024;
331 if (isset($this->options['maxLength'])) $length = min($length, $this->options['maxLength'] - $bodyLength);
7ced4c6a
TD
332 if (isset($this->replyHeaders['content-length'])) $length = min($length, end($this->replyHeaders['content-length']) - $bodyLength);
333
334 $line = $remoteFile->read($length);
335 }
8fe14bd6
TD
336 else {
337 $line = $remoteFile->gets();
338 }
339
86fc0430
TD
340 if ($inHeader) {
341 if (rtrim($line) === '') {
342 $inHeader = false;
8fe14bd6
TD
343 $this->parseReplyHeaders();
344
86fc0430
TD
345 continue;
346 }
347 $this->replyHeaders[] = $line;
348 }
349 else {
8fe14bd6 350 if (isset($this->replyHeaders['transfer-encoding']) && $chunkedTransferRegex->match(end($this->replyHeaders['transfer-encoding']))) {
8fe14bd6
TD
351 // last chunk finished
352 if ($chunkLength === 0) {
353 // read hex data and trash chunk-extension
354 list($hex) = explode(';', $line, 2);
355 $chunkLength = hexdec($hex);
356
357 // $chunkLength === 0 -> no more data
358 if ($chunkLength === 0) {
359 // clear remaining response
7ced4c6a 360 while (!$remoteFile->gets(1024));
8fe14bd6 361
8f3e6138
TD
362 // remove chunked from transfer-encoding
363 $this->replyHeaders['transfer-encoding'] = array_filter(array_map(function ($element) use ($chunkedTransferRegex) {
364 return $chunkedTransferRegex->replace($element, '');
365 }, $this->replyHeaders['transfer-encoding']), 'trim');
366 if (empty($this->replyHeaders['transfer-encoding'])) unset($this->replyHeaders['transfer-encoding']);
367
8fe14bd6
TD
368 // break out of main reading loop
369 break;
370 }
371 }
372 else {
373 $this->replyBody .= $line;
374 $chunkLength -= strlen($line);
8f3e6138
TD
375 $bodyLength += strlen($line);
376 if ($chunkLength === 0) $remoteFile->read(2); // CRLF
8fe14bd6
TD
377 }
378 }
379 else {
380 $this->replyBody .= $line;
8f3e6138 381 $bodyLength += strlen($line);
5262964b 382 }
86fc0430
TD
383 }
384 }
385
5262964b
TD
386 if (isset($this->options['maxLength'])) $this->replyBody = substr($this->replyBody, 0, $this->options['maxLength']);
387
8fe14bd6
TD
388 $remoteFile->close();
389
86fc0430
TD
390 $this->parseReply();
391 }
392
393 /**
8fe14bd6 394 * Parses the reply headers.
86fc0430 395 */
8fe14bd6 396 private function parseReplyHeaders() {
058cbd6a 397 $headers = [];
8fe14bd6 398 $lastKey = '';
86fc0430
TD
399 foreach ($this->replyHeaders as $header) {
400 if (strpos($header, ':') === false) {
058cbd6a 401 $headers[trim($header)] = [trim($header)];
86fc0430
TD
402 continue;
403 }
8fe14bd6
TD
404
405 // 4.2 Header fields can be
406 // extended over multiple lines by preceding each extra line with at
407 // least one SP or HT.
408 if (ltrim($header, "\t ") !== $header) {
409 $headers[$lastKey][] = array_pop($headers[$lastKey]).' '.trim($header);
410 }
411 else {
412 list($key, $value) = explode(':', $header, 2);
413
414 $lastKey = $key;
058cbd6a 415 if (!isset($headers[$key])) $headers[$key] = [];
8fe14bd6
TD
416 $headers[$key][] = trim($value);
417 }
86fc0430 418 }
8fe14bd6
TD
419 // 4.2 Field names are case-insensitive.
420 $this->replyHeaders = array_change_key_case($headers);
058cbd6a 421 if (isset($this->replyHeaders['transfer-encoding'])) $this->replyHeaders['transfer-encoding'] = [implode(',', $this->replyHeaders['transfer-encoding'])];
8fe14bd6 422 $this->legacyHeaders = array_map('end', $headers);
86fc0430 423
a0eb8370 424 // get status code
86fc0430 425 $statusLine = reset($this->replyHeaders);
8fe14bd6 426 $regex = new Regex('^HTTP/1.\d+ +(\d{3})');
3502a7f5 427 if (!$regex->match($statusLine[0])) throw new HTTPException($this, "Unexpected status '".$statusLine."'");
86fc0430 428 $matches = $regex->getMatches();
aeaa135c 429 $this->statusCode = $matches[1];
8fe14bd6
TD
430 }
431
432 /**
433 * Parses the reply.
434 */
435 private function parseReply() {
436 // 4.4 Messages MUST NOT include both a Content-Length header field and a
437 // non-identity transfer-coding. If the message does include a non-
438 // identity transfer-coding, the Content-Length MUST be ignored.
5262964b 439 if (isset($this->replyHeaders['content-length']) && (!isset($this->replyHeaders['transfer-encoding']) || strtolower(end($this->replyHeaders['transfer-encoding'])) !== 'identity') && !isset($this->options['maxLength'])) {
8009cc11 440 if (strlen($this->replyBody) != end($this->replyHeaders['content-length'])) {
3502a7f5 441 throw new HTTPException($this, 'Body length does not match length given in header');
a0eb8370
DR
442 }
443 }
444
445 // validate status code
aeaa135c 446 switch ($this->statusCode) {
86fc0430
TD
447 case '301':
448 case '302':
449 case '303':
450 case '307':
451 // redirect
46853994
MS
452 if ($this->options['maxDepth'] <= 0) {
453 throw new HTTPException(
454 $this,
455 "Received status code '".$this->statusCode."' from server, but recursion level is exhausted",
456 $this->statusCode
457 );
458 }
86fc0430
TD
459
460 $newRequest = clone $this;
461 $newRequest->options['maxDepth']--;
c7fe2510 462
8fe14bd6 463 // 10.3.4 The response to the request can be found under a different URI and SHOULD
c7fe2510 464 // be retrieved using a GET method on that resource.
c7fe2510 465 if ($this->statusCode == '303') {
86fc0430 466 $newRequest->options['method'] = 'GET';
058cbd6a 467 $newRequest->postParameters = [];
8fe14bd6
TD
468 $newRequest->addHeader('content-length', '');
469 $newRequest->addHeader('content-type', '');
86fc0430 470 }
c7fe2510 471
86fc0430 472 try {
8fe14bd6 473 $newRequest->setURL(end($this->replyHeaders['location']));
86fc0430
TD
474 }
475 catch (SystemException $e) {
46853994
MS
476 throw new HTTPException(
477 $this,
478 "Received 'Location: ".end($this->replyHeaders['location'])."' from server, which is invalid.",
479 0,
480 $e
481 );
86fc0430 482 }
86fc0430 483
283df336
TD
484 try {
485 $newRequest->execute();
283df336 486 }
50e71b23 487 finally {
283df336
TD
488 // update data with data from the inner request
489 $this->url = $newRequest->url;
490 $this->statusCode = $newRequest->statusCode;
491 $this->replyHeaders = $newRequest->replyHeaders;
df37e22d 492 $this->legacyHeaders = $newRequest->legacyHeaders;
283df336 493 $this->replyBody = $newRequest->replyBody;
283df336
TD
494 }
495
86fc0430
TD
496 return;
497 break;
a17de04e 498
4d28d5a2
SG
499 case '206':
500 // check, if partial content was expected
5262964b 501 if (!isset($this->headers['range'])) {
46853994
MS
502 throw new HTTPServerErrorException(
503 "Received unexpected status code '206' from server",
504 0,
505 '',
506 new HTTPException(
507 $this,
508 'Received partial response, without sending a range header',
509 206
510 )
511 );
4d28d5a2 512 }
5262964b 513 else if (!isset($this->replyHeaders['content-range'])) {
46853994
MS
514 throw new HTTPServerErrorException(
515 "Content-Range is missing in reply header",
516 0,
517 '',
518 new HTTPException(
519 $this,
520 'Server replied with 206 Partial Content, without sending a Content-Range header',
521 206
522 )
523 );
4d28d5a2
SG
524 }
525 break;
526
3536d2fe 527 case '401':
5cd59413 528 case '402':
3536d2fe 529 case '403':
46853994
MS
530 throw new HTTPUnauthorizedException(
531 "Received status code '".$this->statusCode."' from server",
532 0,
533 '',
534 new HTTPException(
535 $this,
536 "Received status code '".$this->statusCode."' from server",
537 $this->statusCode
538 )
539 );
3536d2fe
AE
540 break;
541
542 case '404':
46853994
MS
543 throw new HTTPNotFoundException(
544 "Received status code '404' from server",
545 0,
546 '',
547 new HTTPException(
548 $this,
549 "Received status code '".$this->statusCode."' from server",
550 $this->statusCode
551 )
552 );
3536d2fe 553 break;
46853994 554
86fc0430 555 default:
8fe14bd6
TD
556 // 6.1.1 However, applications MUST
557 // understand the class of any status code, as indicated by the first
558 // digit, and treat any unrecognized response as being equivalent to the
559 // x00 status code of that class, with the exception that an
560 // unrecognized response MUST NOT be cached.
561 switch (substr($this->statusCode, 0, 1)) {
562 case '2': // 200 and unknown 2XX
563 case '3': // 300 and unknown 3XX
564 // we are fine
565 break;
566 case '5': // 500 and unknown 5XX
46853994
MS
567 throw new HTTPServerErrorException(
568 "Received status code '".$this->statusCode."' from server",
569 0,
570 '',
571 new HTTPException(
572 $this,
573 "Received status code '".$this->statusCode."' from server",
574 $this->statusCode
575 )
576 );
8fe14bd6
TD
577 break;
578 default:
46853994
MS
579 throw new HTTPException(
580 $this,
581 "Received unhandled status code '".$this->statusCode."' from server",
582 $this->statusCode
583 );
8fe14bd6
TD
584 break;
585 }
86fc0430
TD
586 break;
587 }
86fc0430
TD
588 }
589
590 /**
591 * Returns an array with the replied data.
8fe14bd6 592 * Note that the 'headers' element is deprecated and may be removed in the future.
86fc0430 593 *
a17de04e 594 * @return array
86fc0430
TD
595 */
596 public function getReply() {
058cbd6a 597 return [
a195ffa6 598 'statusCode' => $this->statusCode,
8fe14bd6
TD
599 'headers' => $this->legacyHeaders,
600 'httpHeaders' => $this->replyHeaders,
25cdc083
AE
601 'body' => $this->replyBody,
602 'url' => $this->url
058cbd6a 603 ];
86fc0430
TD
604 }
605
606 /**
607 * Sets options and applies default values when an option is omitted.
608 *
a17de04e 609 * @param array $options
3502a7f5 610 * @throws \InvalidArgumentException
86fc0430
TD
611 */
612 private function setOptions(array $options) {
613 if (!isset($options['timeout'])) {
c7fe2510 614 $options['timeout'] = 10;
86fc0430
TD
615 }
616
617 if (!isset($options['method'])) {
5f70a0de 618 $options['method'] = (!empty($this->postParameters) || !empty($this->files) ? 'POST' : 'GET');
86fc0430
TD
619 }
620
621 if (!isset($options['maxDepth'])) {
622 $options['maxDepth'] = 2;
623 }
624
625 if (isset($options['auth'])) {
626 if (!isset($options['auth']['username'])) {
3502a7f5 627 throw new \InvalidArgumentException('Username is missing in authentication data.');
86fc0430
TD
628 }
629 if (!isset($options['auth']['password'])) {
3502a7f5 630 throw new \InvalidArgumentException('Password is missing in authentication data.');
86fc0430
TD
631 }
632 }
633
634 $this->options = $options;
635 }
636
637 /**
638 * Adds a header to this request.
c7fe2510 639 * When an empty value is given existing headers of this name will be removed. When append
86fc0430
TD
640 * is set to false existing values will be overwritten.
641 *
a17de04e
MS
642 * @param string $name
643 * @param string $value
644 * @param boolean $append
86fc0430
TD
645 */
646 public function addHeader($name, $value, $append = false) {
8fe14bd6
TD
647 // 4.2 Field names are case-insensitive.
648 $name = strtolower($name);
649
86fc0430
TD
650 if ($value === '') {
651 unset($this->headers[$name]);
652 return;
653 }
654
655 if ($append && isset($this->headers[$name])) {
656 $this->headers[$name][] = $value;
657 }
a377993e 658 else {
058cbd6a 659 $this->headers[$name] = [$value];
a377993e 660 }
86fc0430
TD
661 }
662
663 /**
664 * Resets reply data when cloning.
665 */
666 private function __clone() {
058cbd6a 667 $this->replyHeaders = [];
86fc0430
TD
668 $this->replyBody = '';
669 $this->statusCode = 0;
670 }
58e1d71f 671}