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