Commit | Line | Data |
---|---|---|
86fc0430 | 1 | <?php |
a9229942 | 2 | |
86fc0430 | 3 | namespace wcf\util; |
a9229942 | 4 | |
5ff87450 TD |
5 | use GuzzleHttp\Exception\BadResponseException; |
6 | use GuzzleHttp\Exception\TooManyRedirectsException; | |
7 | use GuzzleHttp\Exception\TransferException; | |
8 | use GuzzleHttp\Psr7\Request; | |
d4d98a6d | 9 | use GuzzleHttp\RequestOptions; |
c5fc8e59 | 10 | use ParagonIE\ConstantTime\Hex; |
5ff87450 TD |
11 | use Psr\Http\Message\RequestInterface; |
12 | use Psr\Http\Message\ResponseInterface; | |
13 | use Psr\Http\Message\UriInterface; | |
3536d2fe AE |
14 | use wcf\system\exception\HTTPNotFoundException; |
15 | use wcf\system\exception\HTTPServerErrorException; | |
16 | use wcf\system\exception\HTTPUnauthorizedException; | |
86fc0430 | 17 | use wcf\system\exception\SystemException; |
5ff87450 | 18 | use wcf\system\io\HttpFactory; |
86fc0430 | 19 | use wcf\system\WCF; |
3502a7f5 | 20 | use 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 |
32 | final 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 | } |