From 86fc04309d2eae6ddb2ca447a674adcafb2c4232 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 22 Nov 2012 21:40:02 +0100 Subject: [PATCH] Add HTTPUtil and deprecated FileUtil::downloadFileFromHTTP() HTTPUtil is a clean reimplementation of FileUtil::downloadFileFromHTTP() with some new features. --- .../install/files/lib/util/FileUtil.class.php | 108 +----- .../install/files/lib/util/HTTPUtil.class.php | 312 ++++++++++++++++++ 2 files changed, 321 insertions(+), 99 deletions(-) create mode 100644 wcfsetup/install/files/lib/util/HTTPUtil.class.php diff --git a/wcfsetup/install/files/lib/util/FileUtil.class.php b/wcfsetup/install/files/lib/util/FileUtil.class.php index 555bb5fcee..9ffeb1258a 100644 --- a/wcfsetup/install/files/lib/util/FileUtil.class.php +++ b/wcfsetup/install/files/lib/util/FileUtil.class.php @@ -397,110 +397,20 @@ final class FileUtil { * @param array $postParameters * @param array $headers empty array or a not initialized variable * @return string + * @deprecated This method currently only is a wrapper around \wcf\util\HTTPUtil. Please use HTTPUtil + * from now on, as this method may be removed in the future. */ public static function downloadFileFromHttp($httpUrl, $prefix = 'package', array $options = array(), array $postParameters = array(), &$headers = array()) { - $newFileName = self::getTemporaryFilename($prefix.'_'); - $localFile = new File($newFileName); // the file to write. - - if (!isset($options['timeout'])) { - $options['timeout'] = 30; - } - if (!isset($options['method'])) { - $options['method'] = (!empty($postParameters) ? 'POST' : 'GET'); - } - - // parse URL - $parsedUrl = parse_url($httpUrl); - $port = ($parsedUrl['scheme'] == 'https' ? 443 : 80); - $host = $parsedUrl['host']; - $path = (isset($parsedUrl['path']) ? $parsedUrl['path'] : '/'); - // proxy is set - if (PROXY_SERVER_HTTP) { - $parsedProxy = parse_url(PROXY_SERVER_HTTP); - $host = $parsedProxy['host']; - $port = ($parsedProxy['scheme'] == 'https' ? 443 : 80); - $path = $httpUrl; - $parsedUrl['query'] = ''; - } - - // build parameter-string - $parameterString = ''; - foreach ($postParameters as $key => $value) { - if (is_array($value)) { - foreach($value as $value2) { - $parameterString .= urlencode($key).'[]='.urlencode($value2).'&'; - } - } - else { - $parameterString .= urlencode($key).'='.urlencode($value).'&'; - } - } - $parameterString = rtrim($parameterString, '&'); + $request = new HTTPUtil($httpUrl, $options, $postParameters); + $request->execute(); + $reply = $request->getReply(); - // connect - try { - $remoteFile = new RemoteFile(($port === 443 ? 'ssl://' : '').$host, $port, $options['timeout']); - } - catch (SystemException $e) { - $localFile->close(); - unlink($newFileName); - throw $e; - } - - // build and send the http request. - $request = $options['method']." ".$path.(!empty($parsedUrl['query']) ? '?'.$parsedUrl['query'] : '')." HTTP/1.0\r\n"; - $request .= "User-Agent: HTTP.PHP (FileUtil.class.php; WoltLab Community Framework/".WCF_VERSION."; ".WCF::getLanguage()->languageCode.")\r\n"; - $request .= "Accept: */*\r\n"; - $request .= "Accept-Language: ".WCF::getLanguage()->languageCode."\r\n"; - if ($options['method'] !== 'GET') { - $request .= "Content-length: ".strlen($parameterString)."\r\n"; - $request .= "Content-Type: application/x-www-form-urlencoded\r\n"; - } - $request .= "Host: ".$host."\r\n"; - $request .= "Connection: Close\r\n\r\n"; - if ($options['method'] !== 'GET') $request .= $parameterString."\r\n\r\n"; - $remoteFile->puts($request); - - $inHeader = true; - $readResponse = array(); - // read http response. - while (!$remoteFile->eof()) { - $readResponse[] = $remoteFile->gets(); - if ($inHeader) { - if (rtrim(end($readResponse)) == '') { - $inHeader = false; - } - } - else { - // look if the webserver sent an error http statuscode - // This has still to be checked if really sufficient! - $arrayHeader = array('201', '301', '302', '303', '307', '404'); - foreach ($arrayHeader as $code) { - $error = strpos($readResponse[0], $code); - } - if ($error !== false) { - $localFile->close(); - unlink($newFileName); - throw new SystemException("file ".$path." not found at host '".$host."'"); - } - - // write to the target system. - $localFile->write(end($readResponse)); - } - } + $newFileName = self::getTemporaryFilename($prefix.'_'); + file_put_contents($newFileName, $reply['body']); // the file to write. - foreach ($readResponse as $line) { - if (rtrim($line) == '') break; - if (strpos($line, ':') === false) { - $headers[trim($line)] = trim($line); - continue; - } - list($key, $value) = explode(':', $line, 2); - $headers[$key] = trim($value); - } + $tmp = $reply['headers']; // copy variable, to avoid problems with the reference + $headers = $tmp; - $remoteFile->close(); - $localFile->close(); return $newFileName; } diff --git a/wcfsetup/install/files/lib/util/HTTPUtil.class.php b/wcfsetup/install/files/lib/util/HTTPUtil.class.php new file mode 100644 index 0000000000..40ef3f86c6 --- /dev/null +++ b/wcfsetup/install/files/lib/util/HTTPUtil.class.php @@ -0,0 +1,312 @@ + + * @package com.woltlab.wcf + * @subpackage util + * @category Community Framework + */ +final class HTTPUtil { + /** + * given options + * @var array + */ + private $options = array(); + + /** + * given post parameters + * @var array + */ + private $postParameters = array(); + + /** + * is the request made via SSL + * @var boolean + */ + private $useSSL = false; + + /** + * target host + * @var string + */ + private $host; + + /** + * target port + * @var integer + */ + private $port; + + /** + * target path + * @var string + */ + private $path; + + /** + * target query string + * @var string + */ + private $query; + + /** + * request headers + * @var array + */ + private $headers = array(); + + /** + * reply headers + * @var array + */ + private $replyHeaders = array(); + + /** + * reply body + * @var string + */ + private $replyBody = ''; + + /** + * reply status code + * @var integer + */ + private $statusCode = 0; + + /** + * Constructs a new request. + * + * @param string $url URL to connect to + * @param array $options + * @param array $postParameters Parameters to send via POST + */ + public function __construct($url, array $options = array(), array $postParameters = array()) { + $this->setURL($url); + + $this->postParameters = $postParameters; + + $this->setOptions($options); + + $this->addHeader('User-Agent', "HTTP.PHP (HTTPUtil.class.php; WoltLab Community Framework/".WCF_VERSION."; ".WCF::getLanguage()->languageCode.")"); + $this->addHeader('Accept', '*/*'); + $this->addHeader('Accept-Language', WCF::getLanguage()->languageCode); + if ($this->options['method'] !== 'GET') { + $this->addHeader('Content-length', strlen(http_build_query($this->postParameters))); + $this->addHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + if (isset($this->options['auth'])) { + $this->addHeader('Authorization', "Basic ".base64_encode($options['auth']['username'].":".$options['auth']['password'])); + } + $this->addHeader('Host', $this->host); + $this->addHeader('Connection', 'Close'); + } + + /** + * Parses the given URL and applies PROXY_SERVER_HTTP. + * + * @param string $url + */ + private function setURL($url) { + if (PROXY_SERVER_HTTP) { + $parsedUrl = parse_url(PROXY_SERVER_HTTP); + $this->path = $url; + } + else { + $parsedUrl = parse_url($url); + $this->path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/'; + } + $this->useSSL = $parsedUrl['scheme'] === 'https'; + $this->host = $parsedUrl['host']; + $this->port = isset($parsedUrl['port']) ? $parsedUrl['port'] : ($this->useSSL ? 443 : 80); + $this->path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/'; + $this->query = isset($parsedUrl['query']) ? $parsedUrl['query'] : ''; + } + + /** + * Executes the HTTP request. + */ + public function execute() { + // connect + $remoteFile = new RemoteFile(($this->useSSL ? 'ssl://' : '').$this->host, $this->port, $this->options['timeout']); + + $request = $this->options['method']." ".$this->path.($this->query ? '?'.$this->query : '')." HTTP/1.0\r\n"; + + foreach ($this->headers as $name => $values) { + foreach ($values as $value) { + $request .= $name.": ".$value."\r\n"; + } + } + $request .= "\r\n"; + if ($this->options['method'] !== 'GET') $request .= http_build_query($this->postParameters)."\r\n\r\n"; + $remoteFile->puts($request); + + $inHeader = true; + $this->replyHeaders = array(); + $this->replyBody = ''; + // read http response. + while (!$remoteFile->eof()) { + $line = $remoteFile->gets(); + if ($inHeader) { + if (rtrim($line) === '') { + $inHeader = false; + continue; + } + $this->replyHeaders[] = $line; + } + else { + $this->replyBody .= $line; + } + } + + $this->parseReply(); + } + + /** + * Parses the reply. + */ + private function parseReply() { + $headers = array(); + + foreach ($this->replyHeaders as $header) { + if (strpos($header, ':') === false) { + $headers[trim($header)] = trim($header); + continue; + } + list($key, $value) = explode(':', $header, 2); + $headers[$key] = trim($value); + } + $this->replyHeaders = $headers; + + $statusLine = reset($this->replyHeaders); + $regex = new Regex('^HTTP/1.0 (\d{3})'); // we expect an HTTP 1.0 response, as we sent an HTTP 1.0 request + if (!$regex->match($statusLine)) throw new SystemException("Unexpected status '".$statusLine."'"); + $matches = $regex->getMatches(); + $statusCode = $matches[1]; + + switch ($statusCode) { + case '301': + case '302': + case '303': + case '307': + // redirect + if ($this->options['maxDepth'] <= 0) throw new SystemException("Got redirect status '".$statusCode."', but recursion level is exhausted"); + + $newRequest = clone $this; + $newRequest->options['maxDepth']--; + if ($statusCode != '307') { + $newRequest->options['method'] = 'GET'; + $newRequest->postParameters = array(); + $newRequest->addHeader('Content-length', ''); + $newRequest->addHeader('Content-Type', ''); + } + try { + $newRequest->setURL($this->replyHeaders['Location']); + } + catch (SystemException $e) { + throw new SystemException("Given redirect URL '".$this->replyHeaders['Location']."' is invalid. Probably the host is missing?", 0, $e); + } + $newRequest->execute(); + + $this->statusCode = $newRequest->statusCode; + $this->replyHeaders = $newRequest->replyHeaders; + $this->replyBody = $newRequest->replyBody; + return; + break; + case '200': + case '204': + // we are fine + break; + default: + throw new SystemException("Got status '".$statusCode."' and I don't know how to handle it"); + break; + } + + if (isset($this->replyHeaders['Content-Length'])) { + if (strlen($this->replyBody) != $this->replyHeaders['Content-Length']) { + throw new SystemException('Body length does not match length given in header'); + } + } + } + + /** + * Returns an array with the replied data. + * + * @return array + */ + public function getReply() { + return array('statusCode' => $this->statusCode, 'headers' => $this->replyHeaders, 'body' => $this->replyBody); + } + + /** + * Sets options and applies default values when an option is omitted. + * + * @param array $options + */ + private function setOptions(array $options) { + if (!isset($options['timeout'])) { + $options['timeout'] = 30; + } + + if (!isset($options['method'])) { + $options['method'] = (!empty($this->postParameters) ? 'POST' : 'GET'); + } + + if (!isset($options['maxDepth'])) { + $options['maxDepth'] = 2; + } + + if (isset($options['auth'])) { + if (!isset($options['auth']['username'])) { + throw new SystemException('username is missing in authentification data'); + } + if (!isset($options['auth']['password'])) { + throw new SystemException('password is missing in authentification data'); + } + } + + $this->options = $options; + } + + /** + * Adds a header to this request. + * When an empty value is given existing headers of this name will be remove. 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) { + if ($value === '') { + unset($this->headers[$name]); + return; + } + + if ($append && isset($this->headers[$name])) { + $this->headers[$name][] = $value; + } + + $this->headers[$name] = (array) $value; + } + + /** + * Resets reply data when cloning. + */ + private function __clone() { + $this->replyHeaders = array(); + $this->replyBody = ''; + $this->statusCode = 0; + } +} \ No newline at end of file -- 2.20.1