Commit | Line | Data |
---|---|---|
86fc0430 TD |
1 | <?php |
2 | namespace wcf\util; | |
3536d2fe AE |
3 | use wcf\system\exception\HTTPNotFoundException; |
4 | use wcf\system\exception\HTTPServerErrorException; | |
5 | use wcf\system\exception\HTTPUnauthorizedException; | |
86fc0430 TD |
6 | use wcf\system\exception\SystemException; |
7 | use wcf\system\io\RemoteFile; | |
8 | use wcf\system\Regex; | |
9 | use wcf\system\WCF; | |
3502a7f5 | 10 | use wcf\util\exception\HTTPException; |
86fc0430 TD |
11 | |
12 | /** | |
8fe14bd6 | 13 | * Sends HTTP/1.1 requests. |
86fc0430 TD |
14 | * It supports POST, SSL, Basic Auth etc. |
15 | * | |
3536d2fe | 16 | * @author Tim Duesterhus |
7b7b9764 | 17 | * @copyright 2001-2019 WoltLab GmbH |
86fc0430 | 18 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
e71525e4 | 19 | * @package WoltLabSuite\Core\Util |
86fc0430 | 20 | */ |
a195ffa6 | 21 | final class HTTPRequest { |
86fc0430 TD |
22 | /** |
23 | * given options | |
a17de04e | 24 | * @var array |
86fc0430 | 25 | */ |
058cbd6a | 26 | private $options = []; |
86fc0430 TD |
27 | |
28 | /** | |
29 | * given post parameters | |
a17de04e | 30 | * @var array |
86fc0430 | 31 | */ |
058cbd6a | 32 | private $postParameters = []; |
86fc0430 | 33 | |
5f70a0de TD |
34 | /** |
35 | * given files | |
06355ec3 | 36 | * @var array |
5f70a0de | 37 | */ |
058cbd6a | 38 | private $files = []; |
5f70a0de | 39 | |
86fc0430 | 40 | /** |
60f613e2 | 41 | * indicates if request will be made via SSL |
a17de04e | 42 | * @var boolean |
86fc0430 TD |
43 | */ |
44 | private $useSSL = false; | |
45 | ||
5819f701 TD |
46 | /** |
47 | * indicates if the connection to the proxy target will be made via SSL | |
48 | * @var boolean | |
49 | */ | |
50 | private $originUseSSL = false; | |
51 | ||
86fc0430 TD |
52 | /** |
53 | * target host | |
a17de04e | 54 | * @var string |
86fc0430 TD |
55 | */ |
56 | private $host; | |
57 | ||
5819f701 TD |
58 | /** |
59 | * target host if a proxy is used | |
60 | * @var string | |
61 | */ | |
62 | private $originHost; | |
63 | ||
86fc0430 TD |
64 | /** |
65 | * target port | |
a17de04e | 66 | * @var integer |
86fc0430 TD |
67 | */ |
68 | private $port; | |
a17de04e | 69 | |
5819f701 TD |
70 | /** |
71 | * target port if a proxy is used | |
72 | * @var integer | |
73 | */ | |
74 | private $originPort; | |
75 | ||
86fc0430 TD |
76 | /** |
77 | * target path | |
a17de04e | 78 | * @var string |
86fc0430 TD |
79 | */ |
80 | private $path; | |
81 | ||
82 | /** | |
83 | * target query string | |
a17de04e | 84 | * @var string |
86fc0430 TD |
85 | */ |
86 | private $query; | |
87 | ||
25cdc083 AE |
88 | /** |
89 | * request URL | |
90 | * @var string | |
91 | */ | |
92 | private $url = ''; | |
93 | ||
86fc0430 TD |
94 | /** |
95 | * request headers | |
5fffbcb8 | 96 | * @var string[][] |
86fc0430 | 97 | */ |
058cbd6a | 98 | private $headers = []; |
86fc0430 | 99 | |
8fe14bd6 TD |
100 | /** |
101 | * legacy headers | |
7a23a706 | 102 | * @var string[] |
8fe14bd6 | 103 | */ |
058cbd6a | 104 | private $legacyHeaders = []; |
8fe14bd6 | 105 | |
5f70a0de TD |
106 | /** |
107 | * request body | |
108 | * @var string | |
109 | */ | |
110 | private $body = ''; | |
111 | ||
86fc0430 TD |
112 | /** |
113 | * reply headers | |
7a23a706 | 114 | * @var string[] |
86fc0430 | 115 | */ |
058cbd6a | 116 | private $replyHeaders = []; |
86fc0430 TD |
117 | |
118 | /** | |
119 | * reply body | |
a17de04e | 120 | * @var string |
86fc0430 TD |
121 | */ |
122 | private $replyBody = ''; | |
123 | ||
124 | /** | |
125 | * reply status code | |
a17de04e | 126 | * @var integer |
86fc0430 TD |
127 | */ |
128 | private $statusCode = 0; | |
129 | ||
130 | /** | |
a17de04e | 131 | * Constructs a new instance of HTTPRequest. |
86fc0430 TD |
132 | * |
133 | * @param string $url URL to connect to | |
7a23a706 | 134 | * @param string[] $options |
1b20f990 | 135 | * @param mixed $postParameters Parameters to send via POST |
5f70a0de | 136 | * @param array $files Files to attach to the request |
86fc0430 | 137 | */ |
058cbd6a | 138 | public function __construct($url, array $options = [], $postParameters = [], array $files = []) { |
86fc0430 TD |
139 | $this->setURL($url); |
140 | ||
141 | $this->postParameters = $postParameters; | |
5f70a0de | 142 | $this->files = $files; |
86fc0430 TD |
143 | |
144 | $this->setOptions($options); | |
145 | ||
a195ffa6 | 146 | // set default headers |
2bdfc612 TD |
147 | $language = WCF::getLanguage(); |
148 | $this->addHeader('user-agent', "HTTP.PHP (HTTPRequest.class.php; WoltLab Suite/".WCF_VERSION."; ".($language ? $language->languageCode : 'en').")"); | |
8fe14bd6 | 149 | $this->addHeader('accept', '*/*'); |
2bdfc612 | 150 | if ($language) $this->addHeader('accept-language', $language->getFixedLanguageCode()); |
8fe14bd6 | 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 { | |
2926ff1d | 168 | $boundary = bin2hex(\random_bytes(20)); |
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() { |
3306a75d TD |
397 | $statusLine = array_shift($this->replyHeaders); |
398 | ||
399 | // get status code | |
400 | $regex = new Regex('^HTTP/1.\d+\s+(\d{3})'); | |
401 | if (!$regex->match($statusLine)) throw new HTTPException($this, "Unexpected status '".$statusLine."'"); | |
402 | $matches = $regex->getMatches(); | |
403 | $this->statusCode = $matches[1]; | |
404 | ||
058cbd6a | 405 | $headers = []; |
8fe14bd6 | 406 | $lastKey = ''; |
86fc0430 TD |
407 | foreach ($this->replyHeaders as $header) { |
408 | if (strpos($header, ':') === false) { | |
058cbd6a | 409 | $headers[trim($header)] = [trim($header)]; |
86fc0430 TD |
410 | continue; |
411 | } | |
8fe14bd6 TD |
412 | |
413 | // 4.2 Header fields can be | |
414 | // extended over multiple lines by preceding each extra line with at | |
415 | // least one SP or HT. | |
416 | if (ltrim($header, "\t ") !== $header) { | |
417 | $headers[$lastKey][] = array_pop($headers[$lastKey]).' '.trim($header); | |
418 | } | |
419 | else { | |
420 | list($key, $value) = explode(':', $header, 2); | |
421 | ||
422 | $lastKey = $key; | |
058cbd6a | 423 | if (!isset($headers[$key])) $headers[$key] = []; |
8fe14bd6 TD |
424 | $headers[$key][] = trim($value); |
425 | } | |
86fc0430 | 426 | } |
8fe14bd6 TD |
427 | // 4.2 Field names are case-insensitive. |
428 | $this->replyHeaders = array_change_key_case($headers); | |
058cbd6a | 429 | if (isset($this->replyHeaders['transfer-encoding'])) $this->replyHeaders['transfer-encoding'] = [implode(',', $this->replyHeaders['transfer-encoding'])]; |
8fe14bd6 | 430 | $this->legacyHeaders = array_map('end', $headers); |
8fe14bd6 TD |
431 | } |
432 | ||
433 | /** | |
434 | * Parses the reply. | |
435 | */ | |
436 | private function parseReply() { | |
437 | // 4.4 Messages MUST NOT include both a Content-Length header field and a | |
438 | // non-identity transfer-coding. If the message does include a non- | |
439 | // identity transfer-coding, the Content-Length MUST be ignored. | |
5262964b | 440 | if (isset($this->replyHeaders['content-length']) && (!isset($this->replyHeaders['transfer-encoding']) || strtolower(end($this->replyHeaders['transfer-encoding'])) !== 'identity') && !isset($this->options['maxLength'])) { |
8009cc11 | 441 | if (strlen($this->replyBody) != end($this->replyHeaders['content-length'])) { |
3502a7f5 | 442 | throw new HTTPException($this, 'Body length does not match length given in header'); |
a0eb8370 DR |
443 | } |
444 | } | |
445 | ||
446 | // validate status code | |
aeaa135c | 447 | switch ($this->statusCode) { |
86fc0430 TD |
448 | case '301': |
449 | case '302': | |
450 | case '303': | |
451 | case '307': | |
452 | // redirect | |
46853994 MS |
453 | if ($this->options['maxDepth'] <= 0) { |
454 | throw new HTTPException( | |
455 | $this, | |
456 | "Received status code '".$this->statusCode."' from server, but recursion level is exhausted", | |
457 | $this->statusCode | |
458 | ); | |
459 | } | |
86fc0430 TD |
460 | |
461 | $newRequest = clone $this; | |
462 | $newRequest->options['maxDepth']--; | |
c7fe2510 | 463 | |
8fe14bd6 | 464 | // 10.3.4 The response to the request can be found under a different URI and SHOULD |
c7fe2510 | 465 | // be retrieved using a GET method on that resource. |
c7fe2510 | 466 | if ($this->statusCode == '303') { |
86fc0430 | 467 | $newRequest->options['method'] = 'GET'; |
058cbd6a | 468 | $newRequest->postParameters = []; |
8fe14bd6 TD |
469 | $newRequest->addHeader('content-length', ''); |
470 | $newRequest->addHeader('content-type', ''); | |
86fc0430 | 471 | } |
c7fe2510 | 472 | |
86fc0430 | 473 | try { |
8fe14bd6 | 474 | $newRequest->setURL(end($this->replyHeaders['location'])); |
86fc0430 TD |
475 | } |
476 | catch (SystemException $e) { | |
46853994 MS |
477 | throw new HTTPException( |
478 | $this, | |
479 | "Received 'Location: ".end($this->replyHeaders['location'])."' from server, which is invalid.", | |
480 | 0, | |
481 | $e | |
482 | ); | |
86fc0430 | 483 | } |
86fc0430 | 484 | |
283df336 TD |
485 | try { |
486 | $newRequest->execute(); | |
283df336 | 487 | } |
50e71b23 | 488 | finally { |
283df336 TD |
489 | // update data with data from the inner request |
490 | $this->url = $newRequest->url; | |
491 | $this->statusCode = $newRequest->statusCode; | |
492 | $this->replyHeaders = $newRequest->replyHeaders; | |
df37e22d | 493 | $this->legacyHeaders = $newRequest->legacyHeaders; |
283df336 | 494 | $this->replyBody = $newRequest->replyBody; |
283df336 TD |
495 | } |
496 | ||
86fc0430 TD |
497 | return; |
498 | break; | |
a17de04e | 499 | |
4d28d5a2 SG |
500 | case '206': |
501 | // check, if partial content was expected | |
5262964b | 502 | if (!isset($this->headers['range'])) { |
46853994 MS |
503 | throw new HTTPServerErrorException( |
504 | "Received unexpected status code '206' from server", | |
505 | 0, | |
506 | '', | |
507 | new HTTPException( | |
508 | $this, | |
509 | 'Received partial response, without sending a range header', | |
510 | 206 | |
511 | ) | |
512 | ); | |
4d28d5a2 | 513 | } |
5262964b | 514 | else if (!isset($this->replyHeaders['content-range'])) { |
46853994 MS |
515 | throw new HTTPServerErrorException( |
516 | "Content-Range is missing in reply header", | |
517 | 0, | |
518 | '', | |
519 | new HTTPException( | |
520 | $this, | |
521 | 'Server replied with 206 Partial Content, without sending a Content-Range header', | |
522 | 206 | |
523 | ) | |
524 | ); | |
4d28d5a2 SG |
525 | } |
526 | break; | |
527 | ||
3536d2fe | 528 | case '401': |
5cd59413 | 529 | case '402': |
3536d2fe | 530 | case '403': |
46853994 MS |
531 | throw new HTTPUnauthorizedException( |
532 | "Received status code '".$this->statusCode."' from server", | |
533 | 0, | |
534 | '', | |
535 | new HTTPException( | |
536 | $this, | |
537 | "Received status code '".$this->statusCode."' from server", | |
538 | $this->statusCode | |
539 | ) | |
540 | ); | |
3536d2fe AE |
541 | break; |
542 | ||
543 | case '404': | |
46853994 MS |
544 | throw new HTTPNotFoundException( |
545 | "Received status code '404' from server", | |
546 | 0, | |
547 | '', | |
548 | new HTTPException( | |
549 | $this, | |
550 | "Received status code '".$this->statusCode."' from server", | |
551 | $this->statusCode | |
552 | ) | |
553 | ); | |
3536d2fe | 554 | break; |
46853994 | 555 | |
86fc0430 | 556 | default: |
8fe14bd6 TD |
557 | // 6.1.1 However, applications MUST |
558 | // understand the class of any status code, as indicated by the first | |
559 | // digit, and treat any unrecognized response as being equivalent to the | |
560 | // x00 status code of that class, with the exception that an | |
561 | // unrecognized response MUST NOT be cached. | |
562 | switch (substr($this->statusCode, 0, 1)) { | |
563 | case '2': // 200 and unknown 2XX | |
564 | case '3': // 300 and unknown 3XX | |
565 | // we are fine | |
566 | break; | |
567 | case '5': // 500 and unknown 5XX | |
46853994 MS |
568 | throw new HTTPServerErrorException( |
569 | "Received status code '".$this->statusCode."' from server", | |
570 | 0, | |
571 | '', | |
572 | new HTTPException( | |
573 | $this, | |
574 | "Received status code '".$this->statusCode."' from server", | |
575 | $this->statusCode | |
576 | ) | |
577 | ); | |
8fe14bd6 TD |
578 | break; |
579 | default: | |
46853994 MS |
580 | throw new HTTPException( |
581 | $this, | |
582 | "Received unhandled status code '".$this->statusCode."' from server", | |
583 | $this->statusCode | |
584 | ); | |
8fe14bd6 TD |
585 | break; |
586 | } | |
86fc0430 TD |
587 | break; |
588 | } | |
86fc0430 TD |
589 | } |
590 | ||
591 | /** | |
592 | * Returns an array with the replied data. | |
8fe14bd6 | 593 | * Note that the 'headers' element is deprecated and may be removed in the future. |
86fc0430 | 594 | * |
a17de04e | 595 | * @return array |
86fc0430 TD |
596 | */ |
597 | public function getReply() { | |
058cbd6a | 598 | return [ |
a195ffa6 | 599 | 'statusCode' => $this->statusCode, |
8fe14bd6 TD |
600 | 'headers' => $this->legacyHeaders, |
601 | 'httpHeaders' => $this->replyHeaders, | |
25cdc083 AE |
602 | 'body' => $this->replyBody, |
603 | 'url' => $this->url | |
058cbd6a | 604 | ]; |
86fc0430 TD |
605 | } |
606 | ||
607 | /** | |
608 | * Sets options and applies default values when an option is omitted. | |
609 | * | |
a17de04e | 610 | * @param array $options |
3502a7f5 | 611 | * @throws \InvalidArgumentException |
86fc0430 TD |
612 | */ |
613 | private function setOptions(array $options) { | |
614 | if (!isset($options['timeout'])) { | |
c7fe2510 | 615 | $options['timeout'] = 10; |
86fc0430 TD |
616 | } |
617 | ||
618 | if (!isset($options['method'])) { | |
5f70a0de | 619 | $options['method'] = (!empty($this->postParameters) || !empty($this->files) ? 'POST' : 'GET'); |
86fc0430 TD |
620 | } |
621 | ||
622 | if (!isset($options['maxDepth'])) { | |
623 | $options['maxDepth'] = 2; | |
624 | } | |
625 | ||
626 | if (isset($options['auth'])) { | |
627 | if (!isset($options['auth']['username'])) { | |
3502a7f5 | 628 | throw new \InvalidArgumentException('Username is missing in authentication data.'); |
86fc0430 TD |
629 | } |
630 | if (!isset($options['auth']['password'])) { | |
3502a7f5 | 631 | throw new \InvalidArgumentException('Password is missing in authentication data.'); |
86fc0430 TD |
632 | } |
633 | } | |
634 | ||
635 | $this->options = $options; | |
636 | } | |
637 | ||
638 | /** | |
639 | * Adds a header to this request. | |
c7fe2510 | 640 | * When an empty value is given existing headers of this name will be removed. When append |
86fc0430 TD |
641 | * is set to false existing values will be overwritten. |
642 | * | |
a17de04e MS |
643 | * @param string $name |
644 | * @param string $value | |
645 | * @param boolean $append | |
86fc0430 TD |
646 | */ |
647 | public function addHeader($name, $value, $append = false) { | |
8fe14bd6 TD |
648 | // 4.2 Field names are case-insensitive. |
649 | $name = strtolower($name); | |
650 | ||
86fc0430 TD |
651 | if ($value === '') { |
652 | unset($this->headers[$name]); | |
653 | return; | |
654 | } | |
655 | ||
656 | if ($append && isset($this->headers[$name])) { | |
657 | $this->headers[$name][] = $value; | |
658 | } | |
a377993e | 659 | else { |
058cbd6a | 660 | $this->headers[$name] = [$value]; |
a377993e | 661 | } |
86fc0430 TD |
662 | } |
663 | ||
664 | /** | |
665 | * Resets reply data when cloning. | |
666 | */ | |
667 | private function __clone() { | |
058cbd6a | 668 | $this->replyHeaders = []; |
86fc0430 TD |
669 | $this->replyBody = ''; |
670 | $this->statusCode = 0; | |
671 | } | |
58e1d71f | 672 | } |