Fix `Ui/Message/Manager.getPermission()` for permissions with dashes
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / HTTPRequest.class.php
CommitLineData
86fc0430
TD
1<?php
2namespace wcf\util;
5ff87450
TD
3use GuzzleHttp\Exception\BadResponseException;
4use GuzzleHttp\Exception\TooManyRedirectsException;
5use GuzzleHttp\Exception\TransferException;
6use GuzzleHttp\Psr7\Request;
c5fc8e59 7use ParagonIE\ConstantTime\Hex;
5ff87450
TD
8use Psr\Http\Message\RequestInterface;
9use Psr\Http\Message\ResponseInterface;
10use Psr\Http\Message\UriInterface;
3536d2fe
AE
11use wcf\system\exception\HTTPNotFoundException;
12use wcf\system\exception\HTTPServerErrorException;
13use wcf\system\exception\HTTPUnauthorizedException;
86fc0430 14use wcf\system\exception\SystemException;
5ff87450 15use wcf\system\io\HttpFactory;
86fc0430 16use wcf\system\WCF;
3502a7f5 17use 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 29final 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}