Remove unused local variables
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / HTTPRequest.class.php
CommitLineData
86fc0430 1<?php
a9229942 2
86fc0430 3namespace wcf\util;
a9229942 4
5ff87450
TD
5use GuzzleHttp\Exception\BadResponseException;
6use GuzzleHttp\Exception\TooManyRedirectsException;
7use GuzzleHttp\Exception\TransferException;
8use GuzzleHttp\Psr7\Request;
d4d98a6d 9use GuzzleHttp\RequestOptions;
c5fc8e59 10use ParagonIE\ConstantTime\Hex;
5ff87450
TD
11use Psr\Http\Message\RequestInterface;
12use Psr\Http\Message\ResponseInterface;
13use Psr\Http\Message\UriInterface;
3536d2fe
AE
14use wcf\system\exception\HTTPNotFoundException;
15use wcf\system\exception\HTTPServerErrorException;
16use wcf\system\exception\HTTPUnauthorizedException;
86fc0430 17use wcf\system\exception\SystemException;
5ff87450 18use wcf\system\io\HttpFactory;
86fc0430 19use wcf\system\WCF;
3502a7f5 20use wcf\util\exception\HTTPException;
86fc0430
TD
21
22/**
8fe14bd6 23 * Sends HTTP/1.1 requests.
86fc0430 24 * It supports POST, SSL, Basic Auth etc.
a9229942
TD
25 *
26 * @author Tim Duesterhus
27 * @copyright 2001-2019 WoltLab GmbH
28 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
29 * @package WoltLabSuite\Core\Util
30 * @deprecated 5.3 - Use Guzzle via \wcf\system\io\HttpFactory.
86fc0430 31 */
a9229942
TD
32final class HTTPRequest
33{
34 /**
35 * given options
36 * @var array
37 */
38 private $options = [];
39
40 /**
41 * given post parameters
42 * @var array
43 */
44 private $postParameters = [];
45
46 /**
47 * given files
48 * @var array
49 */
50 private $files = [];
51
52 /**
53 * request URL
54 * @var string
55 */
56 private $url = '';
57
58 /**
59 * request headers
60 * @var string[][]
61 */
62 private $headers = [];
63
64 /**
65 * request body
66 * @var string
67 */
68 private $body = '';
69
70 /**
71 * reply body
72 * @var string
73 */
74 private $replyBody;
75
76 /**
77 * @var ResponseInterface
78 */
79 private $response;
80
81 /**
82 * Constructs a new instance of HTTPRequest.
83 *
84 * @param string $url URL to connect to
85 * @param array $options
86 * @param mixed $postParameters Parameters to send via POST
87 * @param array $files Files to attach to the request
88 */
89 public function __construct($url, array $options = [], $postParameters = [], array $files = [])
90 {
91 $this->url = $url;
92
93 $this->postParameters = $postParameters;
94 $this->files = $files;
95
96 $this->setOptions($options);
97
98 // set default headers
99 $language = WCF::getLanguage();
100 $this->addHeader(
101 'user-agent',
102 "HTTP.PHP (HTTPRequest.class.php; WoltLab Suite/" . WCF_VERSION . "; " . ($language ? $language->languageCode : 'en') . ")"
103 );
104 $this->addHeader('accept', '*/*');
105 if ($language) {
106 $this->addHeader('accept-language', $language->getFixedLanguageCode());
107 }
108
109 if (isset($this->options['maxLength'])) {
110 $this->addHeader('Range', 'bytes=0-' . ($this->options['maxLength'] - 1));
111 }
112
113 if ($this->options['method'] !== 'GET') {
114 if (empty($this->files)) {
115 if (\is_array($postParameters)) {
116 $this->body = \http_build_query($this->postParameters, '', '&');
117 } elseif (\is_string($postParameters) && !empty($postParameters)) {
118 $this->body = $postParameters;
119 }
120
121 $this->addHeader('content-type', 'application/x-www-form-urlencoded');
122 } else {
123 $boundary = Hex::encode(\random_bytes(20));
124 $this->addHeader('content-type', 'multipart/form-data; boundary=' . $boundary);
125
126 // source of the iterators: http://stackoverflow.com/a/7623716/782822
127 if (!empty($this->postParameters)) {
128 $iterator = new \RecursiveIteratorIterator(
129 new \RecursiveArrayIterator($this->postParameters),
130 \RecursiveIteratorIterator::SELF_FIRST
131 );
13b11e4c 132 foreach ($iterator as $v) {
a9229942
TD
133 /** @noinspection PhpUndefinedMethodInspection */
134 if (!$iterator->hasChildren()) {
135 $key = '';
136 for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) {
137 if ($i === 0) {
138 $key .= $iterator->getSubIterator($i)->key();
139 } else {
140 $key .= '[' . $iterator->getSubIterator($i)->key() . ']';
141 }
142 }
143
144 $this->body .= "--" . $boundary . "\r\n";
145 $this->body .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n";
146 $this->body .= $v . "\r\n";
147 }
148 }
149 }
150
151 $iterator = new \RecursiveIteratorIterator(
152 new \RecursiveArrayIterator($this->files),
153 \RecursiveIteratorIterator::SELF_FIRST
154 );
155 foreach ($iterator as $k => $v) {
156 /** @noinspection PhpUndefinedMethodInspection */
157 if (!$iterator->hasChildren()) {
158 $key = '';
159 for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) {
160 if ($i === 0) {
161 $key .= $iterator->getSubIterator($i)->key();
162 } else {
163 $key .= '[' . $iterator->getSubIterator($i)->key() . ']';
164 }
165 }
166
167 $this->body .= "--" . $boundary . "\r\n";
168 $this->body .= 'Content-Disposition: form-data; name="' . $k . '"; filename="' . \basename($v) . '"' . "\r\n";
169 $this->body .= 'Content-Type: ' . (FileUtil::getMimeType($v) ?: 'application/octet-stream.') . "\r\n\r\n";
170 $this->body .= \file_get_contents($v) . "\r\n";
171 }
172 }
173
174 $this->body .= "--" . $boundary . "--";
175 }
176 }
177 $this->addHeader('connection', 'Close');
178 }
179
180 /**
181 * Executes the HTTP request.
182 */
183 public function execute()
184 {
185 $redirectHandler = function (RequestInterface $request, ResponseInterface $response, UriInterface $uri) {
186 $this->url = (string)$uri;
187 $this->response = $response;
188 };
189
190 $options = [
191 // No overall timeout
d4d98a6d
TD
192 RequestOptions::TIMEOUT => 0,
193 RequestOptions::CONNECT_TIMEOUT => $this->options['timeout'],
194 RequestOptions::READ_TIMEOUT => $this->options['timeout'],
195 RequestOptions::ALLOW_REDIRECTS => [
a9229942
TD
196 'max' => $this->options['maxDepth'],
197 'track_redirects' => true,
198 'on_redirect' => $redirectHandler,
199 ],
a9229942 200 ];
76ebd357 201 if (isset($this->options['maxLength'])) {
d4d98a6d 202 $options[RequestOptions::STREAM] = true;
76ebd357 203 }
a9229942 204 if (isset($this->options['auth'])) {
d4d98a6d 205 $options[RequestOptions::AUTH] = [
a9229942
TD
206 $this->options['auth']['username'],
207 $this->options['auth']['password'],
208 ];
209 }
210
211 $client = HttpFactory::makeClient($options);
212
213 $headers = [];
214 foreach ($this->headers as $name => $values) {
215 $headers[$name] = \implode(', ', $values);
216 }
217
218 $request = new Request($this->options['method'], $this->url, $headers, $this->body);
219
220 try {
221 $this->response = $client->send($request);
222 } catch (TooManyRedirectsException $e) {
223 throw new HTTPException(
224 $this,
225 "Received status code '" . $this->response->getStatusCode() . "' from server, but recursion level is exhausted",
226 $this->response->getStatusCode(),
227 $e
228 );
229 } catch (BadResponseException $e) {
230 $this->response = $e->getResponse();
231
232 switch ($this->response->getStatusCode()) {
233 case '401':
234 case '402':
235 case '403':
236 throw new HTTPUnauthorizedException(
237 "Received status code '" . $this->response->getStatusCode() . "' from server",
238 0,
239 '',
240 new HTTPException(
241 $this,
242 "Received status code '" . $this->response->getStatusCode() . "' from server",
243 (string)$this->response->getStatusCode(),
244 $e
245 )
246 );
247 case '404':
248 throw new HTTPNotFoundException(
249 "Received status code '404' from server",
250 0,
251 '',
252 new HTTPException(
253 $this,
254 "Received status code '" . $this->response->getStatusCode() . "' from server",
255 (string)$this->response->getStatusCode(),
256 $e
257 )
258 );
259 default:
260 if (\substr($this->response->getStatusCode(), 0, 1) == '5') {
261 throw new HTTPServerErrorException(
262 "Received status code '" . $this->response->getStatusCode() . "' from server",
263 0,
264 '',
265 new HTTPException(
266 $this,
267 "Received status code '" . $this->response->getStatusCode() . "' from server",
268 (string)$this->response->getStatusCode(),
269 $e
270 )
271 );
272 }
273 }
274 } catch (TransferException $e) {
275 throw new SystemException('Failed to HTTPRequest', 0, '', $e);
276 }
277 }
278
279 /**
280 * Returns an array with the replied data.
281 * Note that the 'headers' element is deprecated and may be removed in the future.
282 *
283 * @return array
284 */
285 public function getReply()
286 {
287 if (!$this->response) {
288 return [
289 'statusCode' => 0,
290 'headers' => [],
291 'httpHeaders' => [],
292 'body' => '',
293 'url' => $this->url,
294 ];
295 }
296
297 $headers = [];
298 $legacyHeaders = [];
299
300 foreach ($this->response->getHeaders() as $name => $values) {
301 $headers[\strtolower($name)] = $values;
302 $legacyHeaders[$name] = \end($values);
303 }
304
305 if ($this->replyBody === null) {
c588bcb3
MS
306 try {
307 $bodyLength = 0;
308 while (!$this->response->getBody()->eof()) {
309 $toRead = 8192;
310 if (isset($this->options['maxLength'])) {
311 $toRead = \min($toRead, $this->options['maxLength'] - $bodyLength);
312 }
a9229942 313
c588bcb3
MS
314 $data = $this->response->getBody()->read($toRead);
315 $this->replyBody .= $data;
316 $bodyLength += \strlen($data);
a9229942 317
c588bcb3
MS
318 if (isset($this->options['maxLength']) && $bodyLength >= $this->options['maxLength']) {
319 break;
320 }
a9229942 321 }
c588bcb3
MS
322 } finally {
323 $this->response->getBody()->close();
a9229942 324 }
c588bcb3 325
a9229942
TD
326 if (isset($this->options['maxLength'])) {
327 $this->replyBody = \substr($this->replyBody, 0, $this->options['maxLength']);
328 }
329 }
330
331 return [
332 'statusCode' => (string)$this->response->getStatusCode(),
333 'headers' => $legacyHeaders,
334 'httpHeaders' => $headers,
335 'body' => $this->replyBody,
336 'url' => $this->url,
337 ];
338 }
339
340 /**
341 * Sets options and applies default values when an option is omitted.
342 *
343 * @param array $options
344 * @throws \InvalidArgumentException
345 */
346 private function setOptions(array $options)
347 {
348 if (!isset($options['timeout'])) {
349 $options['timeout'] = 10;
350 }
351
352 if (!isset($options['method'])) {
353 $options['method'] = (!empty($this->postParameters) || !empty($this->files) ? 'POST' : 'GET');
354 }
355
356 if (!isset($options['maxDepth'])) {
357 $options['maxDepth'] = 2;
358 }
359
360 if (isset($options['auth'])) {
361 if (!isset($options['auth']['username'])) {
362 throw new \InvalidArgumentException('Username is missing in authentication data.');
363 }
364 if (!isset($options['auth']['password'])) {
365 throw new \InvalidArgumentException('Password is missing in authentication data.');
366 }
367 }
368
369 $this->options = $options;
370 }
371
372 /**
373 * Adds a header to this request.
374 * When an empty value is given existing headers of this name will be removed. When append
375 * is set to false existing values will be overwritten.
376 *
377 * @param string $name
378 * @param string $value
379 * @param bool $append
380 */
381 public function addHeader($name, $value, $append = false)
382 {
383 // 4.2 Field names are case-insensitive.
384 $name = \strtolower($name);
385
386 if ($value === '') {
387 unset($this->headers[$name]);
388
389 return;
390 }
391
392 if ($append && isset($this->headers[$name])) {
393 $this->headers[$name][] = $value;
394 } else {
395 $this->headers[$name] = [$value];
396 }
397 }
398
399 /**
400 * Resets reply data when cloning.
401 */
402 private function __clone()
403 {
404 $this->response = null;
405 }
58e1d71f 406}