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