From: Tim Düsterhus Date: Mon, 1 Feb 2021 10:54:13 +0000 (+0100) Subject: Merge branch '5.3' X-Git-Tag: 5.4.0_Alpha_1~329 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=76ebd3575f7c3d530866c7868c95d5385dfe5fa0;p=GitHub%2FWoltLab%2FWCF.git Merge branch '5.3' --- 76ebd3575f7c3d530866c7868c95d5385dfe5fa0 diff --cc wcfsetup/install/files/lib/util/HTTPRequest.class.php index 8d000b98ba,efcc294c20..3a5cf27467 --- a/wcfsetup/install/files/lib/util/HTTPRequest.class.php +++ b/wcfsetup/install/files/lib/util/HTTPRequest.class.php @@@ -21,379 -18,363 +21,381 @@@ use wcf\util\exception\HTTPException /** * Sends HTTP/1.1 requests. * It supports POST, SSL, Basic Auth etc. - * - * @author Tim Duesterhus - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @package WoltLabSuite\Core\Util - * @deprecated 5.3 - Use Guzzle via \wcf\system\io\HttpFactory. + * + * @author Tim Duesterhus + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @package WoltLabSuite\Core\Util + * @deprecated 5.3 - Use Guzzle via \wcf\system\io\HttpFactory. */ -final class HTTPRequest { - /** - * given options - * @var array - */ - private $options = []; - - /** - * given post parameters - * @var array - */ - private $postParameters = []; - - /** - * given files - * @var array - */ - private $files = []; - - /** - * request URL - * @var string - */ - private $url = ''; - - /** - * request headers - * @var string[][] - */ - private $headers = []; - - /** - * request body - * @var string - */ - private $body = ''; - - /** - * reply body - * @var string - */ - private $replyBody; - - /** - * @var ResponseInterface - */ - private $response; - - /** - * Constructs a new instance of HTTPRequest. - * - * @param string $url URL to connect to - * @param array $options - * @param mixed $postParameters Parameters to send via POST - * @param array $files Files to attach to the request - */ - public function __construct($url, array $options = [], $postParameters = [], array $files = []) { - $this->url = $url; - - $this->postParameters = $postParameters; - $this->files = $files; - - $this->setOptions($options); - - // set default headers - $language = WCF::getLanguage(); - $this->addHeader('user-agent', "HTTP.PHP (HTTPRequest.class.php; WoltLab Suite/".WCF_VERSION."; ".($language ? $language->languageCode : 'en').")"); - $this->addHeader('accept', '*/*'); - if ($language) $this->addHeader('accept-language', $language->getFixedLanguageCode()); - - if (isset($this->options['maxLength'])) { - $this->addHeader('Range', 'bytes=0-'.($this->options['maxLength'] - 1)); - } - - if ($this->options['method'] !== 'GET') { - if (empty($this->files)) { - if (is_array($postParameters)) { - $this->body = http_build_query($this->postParameters, '', '&'); - } - else if (is_string($postParameters) && !empty($postParameters)) { - $this->body = $postParameters; - } - - $this->addHeader('content-type', 'application/x-www-form-urlencoded'); - } - else { - $boundary = bin2hex(\random_bytes(20)); - $this->addHeader('content-type', 'multipart/form-data; boundary='.$boundary); - - // source of the iterators: http://stackoverflow.com/a/7623716/782822 - if (!empty($this->postParameters)) { - $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($this->postParameters), \RecursiveIteratorIterator::SELF_FIRST); - foreach ($iterator as $k => $v) { - /** @noinspection PhpUndefinedMethodInspection */ - if (!$iterator->hasChildren()) { - $key = ''; - for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) { - if ($i === 0) $key .= $iterator->getSubIterator($i)->key(); - else $key .= '['.$iterator->getSubIterator($i)->key().']'; - } - - $this->body .= "--".$boundary."\r\n"; - $this->body .= 'Content-Disposition: form-data; name="'.$key.'"'."\r\n\r\n"; - $this->body .= $v."\r\n"; - } - } - } - - $iterator = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($this->files), \RecursiveIteratorIterator::SELF_FIRST); - foreach ($iterator as $k => $v) { - /** @noinspection PhpUndefinedMethodInspection */ - if (!$iterator->hasChildren()) { - $key = ''; - for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) { - if ($i === 0) $key .= $iterator->getSubIterator($i)->key(); - else $key .= '['.$iterator->getSubIterator($i)->key().']'; - } - - $this->body .= "--".$boundary."\r\n"; - $this->body .= 'Content-Disposition: form-data; name="'.$k.'"; filename="'.basename($v).'"'."\r\n"; - $this->body .= 'Content-Type: '.(FileUtil::getMimeType($v) ?: 'application/octet-stream.')."\r\n\r\n"; - $this->body .= file_get_contents($v)."\r\n"; - } - } - - $this->body .= "--".$boundary."--"; - } - } - $this->addHeader('connection', 'Close'); - } - - /** - * Executes the HTTP request. - */ - public function execute() { - $redirectHandler = function(RequestInterface $request, ResponseInterface $response, UriInterface $uri) { - $this->url = (string) $uri; - $this->response = $response; - }; - - $options = [ - 'connect_timeout' => $this->options['timeout'], - 'read_timeout' => $this->options['timeout'], - 'allow_redirects' => [ - 'max' => $this->options['maxDepth'], - 'track_redirects' => true, - 'on_redirect' => $redirectHandler, - ], - ]; - if (isset($this->options['maxLength'])) { - $options['stream'] = true; - } - if (isset($this->options['auth'])) { - $options['auth'] = [ - $this->options['auth']['username'], - $this->options['auth']['password'], - ]; - } - - $client = HttpFactory::makeClient($options); - - $headers = []; - foreach ($this->headers as $name => $values) { - $headers[$name] = implode(', ', $values); - } - - $request = new Request($this->options['method'], $this->url, $headers, $this->body); - - try { - $this->response = $client->send($request, [ - // https://github.com/guzzle/guzzle/issues/2735 - 'sink' => \GuzzleHttp\Psr7\stream_for(fopen("php://temp", "w+")), - ]); - } - catch (TooManyRedirectsException $e) { - throw new HTTPException( - $this, - "Received status code '".$this->response->getStatusCode()."' from server, but recursion level is exhausted", - $this->response->getStatusCode(), - $e - ); - } - catch (BadResponseException $e) { - $this->response = $e->getResponse(); - - switch ($this->response->getStatusCode()) { - case '401': - case '402': - case '403': - throw new HTTPUnauthorizedException( - "Received status code '".$this->response->getStatusCode()."' from server", - 0, - '', - new HTTPException( - $this, - "Received status code '".$this->response->getStatusCode()."' from server", - (string) $this->response->getStatusCode(), - $e - ) - ); - case '404': - throw new HTTPNotFoundException( - "Received status code '404' from server", - 0, - '', - new HTTPException( - $this, - "Received status code '".$this->response->getStatusCode()."' from server", - (string) $this->response->getStatusCode(), - $e - ) - ); - default: - if (substr($this->response->getStatusCode(), 0, 1) == '5') { - throw new HTTPServerErrorException( - "Received status code '".$this->response->getStatusCode()."' from server", - 0, - '', - new HTTPException( - $this, - "Received status code '".$this->response->getStatusCode()."' from server", - (string) $this->response->getStatusCode(), - $e - ) - ); - } - } - } - catch (TransferException $e) { - throw new SystemException('Failed to HTTPRequest', 0, '', $e); - } - } - - /** - * Returns an array with the replied data. - * Note that the 'headers' element is deprecated and may be removed in the future. - * - * @return array - */ - public function getReply() { - if (!$this->response) { - return [ - 'statusCode' => 0, - 'headers' => [], - 'httpHeaders' => [], - 'body' => '', - 'url' => $this->url - ]; - } - - $headers = []; - $legacyHeaders = []; - - foreach ($this->response->getHeaders() as $name => $values) { - $headers[strtolower($name)] = $values; - $legacyHeaders[$name] = end($values); - } - - if ($this->replyBody === null) { - $bodyLength = 0; - while (!$this->response->getBody()->eof()) { - $toRead = 8192; - if (isset($this->options['maxLength'])) { - $toRead = min($toRead, $this->options['maxLength'] - $bodyLength); - } - - $data = $this->response->getBody()->read($toRead); - $this->replyBody .= $data; - $bodyLength += strlen($data); - - if (isset($this->options['maxLength']) && $bodyLength >= $this->options['maxLength']) { - $this->response->getBody()->close(); - break; - } - } - if (isset($this->options['maxLength'])) { - $this->replyBody = substr($this->replyBody, 0, $this->options['maxLength']); - } - } - - return [ - 'statusCode' => (string) $this->response->getStatusCode(), - 'headers' => $legacyHeaders, - 'httpHeaders' => $headers, - 'body' => $this->replyBody, - 'url' => $this->url, - ]; - } - - /** - * Sets options and applies default values when an option is omitted. - * - * @param array $options - * @throws \InvalidArgumentException - */ - private function setOptions(array $options) { - if (!isset($options['timeout'])) { - $options['timeout'] = 10; - } - - if (!isset($options['method'])) { - $options['method'] = (!empty($this->postParameters) || !empty($this->files) ? 'POST' : 'GET'); - } - - if (!isset($options['maxDepth'])) { - $options['maxDepth'] = 2; - } - - if (isset($options['auth'])) { - if (!isset($options['auth']['username'])) { - throw new \InvalidArgumentException('Username is missing in authentication data.'); - } - if (!isset($options['auth']['password'])) { - throw new \InvalidArgumentException('Password is missing in authentication data.'); - } - } - - $this->options = $options; - } - - /** - * Adds a header to this request. - * When an empty value is given existing headers of this name will be removed. When append - * is set to false existing values will be overwritten. - * - * @param string $name - * @param string $value - * @param boolean $append - */ - public function addHeader($name, $value, $append = false) { - // 4.2 Field names are case-insensitive. - $name = strtolower($name); - - if ($value === '') { - unset($this->headers[$name]); - return; - } - - if ($append && isset($this->headers[$name])) { - $this->headers[$name][] = $value; - } - else { - $this->headers[$name] = [$value]; - } - } - - /** - * Resets reply data when cloning. - */ - private function __clone() { - $this->response = null; - } +final class HTTPRequest +{ + /** + * given options + * @var array + */ + private $options = []; + + /** + * given post parameters + * @var array + */ + private $postParameters = []; + + /** + * given files + * @var array + */ + private $files = []; + + /** + * request URL + * @var string + */ + private $url = ''; + + /** + * request headers + * @var string[][] + */ + private $headers = []; + + /** + * request body + * @var string + */ + private $body = ''; + + /** + * reply body + * @var string + */ + private $replyBody; + + /** + * @var ResponseInterface + */ + private $response; + + /** + * Constructs a new instance of HTTPRequest. + * + * @param string $url URL to connect to + * @param array $options + * @param mixed $postParameters Parameters to send via POST + * @param array $files Files to attach to the request + */ + public function __construct($url, array $options = [], $postParameters = [], array $files = []) + { + $this->url = $url; + + $this->postParameters = $postParameters; + $this->files = $files; + + $this->setOptions($options); + + // set default headers + $language = WCF::getLanguage(); + $this->addHeader( + 'user-agent', + "HTTP.PHP (HTTPRequest.class.php; WoltLab Suite/" . WCF_VERSION . "; " . ($language ? $language->languageCode : 'en') . ")" + ); + $this->addHeader('accept', '*/*'); + if ($language) { + $this->addHeader('accept-language', $language->getFixedLanguageCode()); + } + + if (isset($this->options['maxLength'])) { + $this->addHeader('Range', 'bytes=0-' . ($this->options['maxLength'] - 1)); + } + + if ($this->options['method'] !== 'GET') { + if (empty($this->files)) { + if (\is_array($postParameters)) { + $this->body = \http_build_query($this->postParameters, '', '&'); + } elseif (\is_string($postParameters) && !empty($postParameters)) { + $this->body = $postParameters; + } + + $this->addHeader('content-type', 'application/x-www-form-urlencoded'); + } else { + $boundary = Hex::encode(\random_bytes(20)); + $this->addHeader('content-type', 'multipart/form-data; boundary=' . $boundary); + + // source of the iterators: http://stackoverflow.com/a/7623716/782822 + if (!empty($this->postParameters)) { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveArrayIterator($this->postParameters), + \RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $k => $v) { + /** @noinspection PhpUndefinedMethodInspection */ + if (!$iterator->hasChildren()) { + $key = ''; + for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) { + if ($i === 0) { + $key .= $iterator->getSubIterator($i)->key(); + } else { + $key .= '[' . $iterator->getSubIterator($i)->key() . ']'; + } + } + + $this->body .= "--" . $boundary . "\r\n"; + $this->body .= 'Content-Disposition: form-data; name="' . $key . '"' . "\r\n\r\n"; + $this->body .= $v . "\r\n"; + } + } + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveArrayIterator($this->files), + \RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $k => $v) { + /** @noinspection PhpUndefinedMethodInspection */ + if (!$iterator->hasChildren()) { + $key = ''; + for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++) { + if ($i === 0) { + $key .= $iterator->getSubIterator($i)->key(); + } else { + $key .= '[' . $iterator->getSubIterator($i)->key() . ']'; + } + } + + $this->body .= "--" . $boundary . "\r\n"; + $this->body .= 'Content-Disposition: form-data; name="' . $k . '"; filename="' . \basename($v) . '"' . "\r\n"; + $this->body .= 'Content-Type: ' . (FileUtil::getMimeType($v) ?: 'application/octet-stream.') . "\r\n\r\n"; + $this->body .= \file_get_contents($v) . "\r\n"; + } + } + + $this->body .= "--" . $boundary . "--"; + } + } + $this->addHeader('connection', 'Close'); + } + + /** + * Executes the HTTP request. + */ + public function execute() + { + $redirectHandler = function (RequestInterface $request, ResponseInterface $response, UriInterface $uri) { + $this->url = (string)$uri; + $this->response = $response; + }; + + $options = [ + // No overall timeout + 'timeout' => 0, + 'connect_timeout' => $this->options['timeout'], + 'read_timeout' => $this->options['timeout'], + 'allow_redirects' => [ + 'max' => $this->options['maxDepth'], + 'track_redirects' => true, + 'on_redirect' => $redirectHandler, + ], - 'stream' => true, + ]; ++ if (isset($this->options['maxLength'])) { ++ $options['stream'] = true; ++ } + if (isset($this->options['auth'])) { + $options['auth'] = [ + $this->options['auth']['username'], + $this->options['auth']['password'], + ]; + } + + $client = HttpFactory::makeClient($options); + + $headers = []; + foreach ($this->headers as $name => $values) { + $headers[$name] = \implode(', ', $values); + } + + $request = new Request($this->options['method'], $this->url, $headers, $this->body); + + try { + $this->response = $client->send($request); + } catch (TooManyRedirectsException $e) { + throw new HTTPException( + $this, + "Received status code '" . $this->response->getStatusCode() . "' from server, but recursion level is exhausted", + $this->response->getStatusCode(), + $e + ); + } catch (BadResponseException $e) { + $this->response = $e->getResponse(); + + switch ($this->response->getStatusCode()) { + case '401': + case '402': + case '403': + throw new HTTPUnauthorizedException( + "Received status code '" . $this->response->getStatusCode() . "' from server", + 0, + '', + new HTTPException( + $this, + "Received status code '" . $this->response->getStatusCode() . "' from server", + (string)$this->response->getStatusCode(), + $e + ) + ); + case '404': + throw new HTTPNotFoundException( + "Received status code '404' from server", + 0, + '', + new HTTPException( + $this, + "Received status code '" . $this->response->getStatusCode() . "' from server", + (string)$this->response->getStatusCode(), + $e + ) + ); + default: + if (\substr($this->response->getStatusCode(), 0, 1) == '5') { + throw new HTTPServerErrorException( + "Received status code '" . $this->response->getStatusCode() . "' from server", + 0, + '', + new HTTPException( + $this, + "Received status code '" . $this->response->getStatusCode() . "' from server", + (string)$this->response->getStatusCode(), + $e + ) + ); + } + } + } catch (TransferException $e) { + throw new SystemException('Failed to HTTPRequest', 0, '', $e); + } + } + + /** + * Returns an array with the replied data. + * Note that the 'headers' element is deprecated and may be removed in the future. + * + * @return array + */ + public function getReply() + { + if (!$this->response) { + return [ + 'statusCode' => 0, + 'headers' => [], + 'httpHeaders' => [], + 'body' => '', + 'url' => $this->url, + ]; + } + + $headers = []; + $legacyHeaders = []; + + foreach ($this->response->getHeaders() as $name => $values) { + $headers[\strtolower($name)] = $values; + $legacyHeaders[$name] = \end($values); + } + + if ($this->replyBody === null) { + $bodyLength = 0; + while (!$this->response->getBody()->eof()) { + $toRead = 8192; + if (isset($this->options['maxLength'])) { + $toRead = \min($toRead, $this->options['maxLength'] - $bodyLength); + } + + $data = $this->response->getBody()->read($toRead); + $this->replyBody .= $data; + $bodyLength += \strlen($data); + + if (isset($this->options['maxLength']) && $bodyLength >= $this->options['maxLength']) { + $this->response->getBody()->close(); + break; + } + } + if (isset($this->options['maxLength'])) { + $this->replyBody = \substr($this->replyBody, 0, $this->options['maxLength']); + } + } + + return [ + 'statusCode' => (string)$this->response->getStatusCode(), + 'headers' => $legacyHeaders, + 'httpHeaders' => $headers, + 'body' => $this->replyBody, + 'url' => $this->url, + ]; + } + + /** + * Sets options and applies default values when an option is omitted. + * + * @param array $options + * @throws \InvalidArgumentException + */ + private function setOptions(array $options) + { + if (!isset($options['timeout'])) { + $options['timeout'] = 10; + } + + if (!isset($options['method'])) { + $options['method'] = (!empty($this->postParameters) || !empty($this->files) ? 'POST' : 'GET'); + } + + if (!isset($options['maxDepth'])) { + $options['maxDepth'] = 2; + } + + if (isset($options['auth'])) { + if (!isset($options['auth']['username'])) { + throw new \InvalidArgumentException('Username is missing in authentication data.'); + } + if (!isset($options['auth']['password'])) { + throw new \InvalidArgumentException('Password is missing in authentication data.'); + } + } + + $this->options = $options; + } + + /** + * Adds a header to this request. + * When an empty value is given existing headers of this name will be removed. When append + * is set to false existing values will be overwritten. + * + * @param string $name + * @param string $value + * @param bool $append + */ + public function addHeader($name, $value, $append = false) + { + // 4.2 Field names are case-insensitive. + $name = \strtolower($name); + + if ($value === '') { + unset($this->headers[$name]); + + return; + } + + if ($append && isset($this->headers[$name])) { + $this->headers[$name][] = $value; + } else { + $this->headers[$name] = [$value]; + } + } + + /** + * Resets reply data when cloning. + */ + private function __clone() + { + $this->response = null; + } }