3 use wcf\system\exception\HTTPNotFoundException
;
4 use wcf\system\exception\HTTPServerErrorException
;
5 use wcf\system\exception\HTTPUnauthorizedException
;
6 use wcf\system\exception\SystemException
;
7 use wcf\system\io\RemoteFile
;
12 * Sends HTTP requests.
13 * It supports POST, SSL, Basic Auth etc.
15 * @author Tim Duesterhus
16 * @copyright 2001-2014 WoltLab GmbH
17 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
18 * @package com.woltlab.wcf
20 * @category Community Framework
22 final class HTTPRequest
{
27 private $options = array();
30 * given post parameters
33 private $postParameters = array();
39 private $files = array();
42 * indicates if request will be made via SSL
45 private $useSSL = false;
81 private $headers = array();
93 private $replyHeaders = array();
99 private $replyBody = '';
105 private $statusCode = 0;
108 * Constructs a new instance of HTTPRequest.
110 * @param string $url URL to connect to
111 * @param array<string> $options
112 * @param mixed $postParameters Parameters to send via POST
113 * @param array $files Files to attach to the request
115 public function __construct($url, array $options = array(), $postParameters = array(), array $files = array()) {
118 $this->postParameters
= $postParameters;
119 $this->files
= $files;
121 $this->setOptions($options);
123 // set default headers
124 $this->addHeader('User-Agent', "HTTP.PHP (HTTPRequest.class.php; WoltLab Community Framework/".WCF_VERSION
."; ".WCF
::getLanguage()->languageCode
.")");
125 $this->addHeader('Accept', '*/*');
126 $this->addHeader('Accept-Language', WCF
::getLanguage()->getFixedLanguageCode());
127 if ($this->options
['method'] !== 'GET') {
128 if (empty($this->files
)) {
129 if (is_array($postParameters)) {
130 $this->body
= http_build_query($this->postParameters
, '', '&');
132 else if (is_string($postParameters) && !empty($postParameters)) {
133 $this->body
= $postParameters;
136 $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
139 $boundary = StringUtil
::getRandomID();
140 $this->addHeader('Content-Type', 'multipart/form-data; boundary='.$boundary);
142 // source of the iterators: http://stackoverflow.com/a/7623716/782822
143 if (!empty($this->postParameters
)) {
144 $iterator = new \
RecursiveIteratorIterator(new \
RecursiveArrayIterator($this->postParameters
), \RecursiveIteratorIterator
::SELF_FIRST
);
145 foreach ($iterator as $k => $v) {
146 if (!$iterator->hasChildren()) {
148 for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++
) {
149 if ($i === 0) $key .= $iterator->getSubIterator($i)->key();
150 else $key .= '['.$iterator->getSubIterator($i)->key().']';
153 $this->body
.= "--".$boundary."\r\n";
154 $this->body
.= 'Content-Disposition: form-data; name="'.$key.'"'."\r\n\r\n";
155 $this->body
.= $v."\r\n";
160 $iterator = new \
RecursiveIteratorIterator(new \
RecursiveArrayIterator($this->files
), \RecursiveIteratorIterator
::SELF_FIRST
);
161 foreach ($iterator as $k => $v) {
162 if (!$iterator->hasChildren()) {
164 for ($i = 0, $max = $iterator->getDepth(); $i <= $max; $i++
) {
165 if ($i === 0) $key .= $iterator->getSubIterator($i)->key();
166 else $key .= '['.$iterator->getSubIterator($i)->key().']';
169 $this->body
.= "--".$boundary."\r\n";
170 $this->body
.= 'Content-Disposition: form-data; name="'.$k.'"; filename="'.basename($v).'"'."\r\n";
171 $this->body
.= 'Content-Type: '.(FileUtil
::getMimeType($v) ?
: 'application/octet-stream.')."\r\n\r\n";
172 $this->body
.= file_get_contents($v)."\r\n";
176 $this->body
.= "--".$boundary."--";
178 $this->addHeader('Content-length', strlen($this->body
));
180 if (isset($this->options
['auth'])) {
181 $this->addHeader('Authorization', "Basic ".base64_encode($options['auth']['username'].":".$options['auth']['password']));
183 $this->addHeader('Host', $this->host
.($this->port
!= ($this->useSSL ?
443 : 80) ?
':'.$this->port
: ''));
184 $this->addHeader('Connection', 'Close');
188 * Parses the given URL and applies PROXY_SERVER_HTTP.
192 private function setURL($url) {
193 if (PROXY_SERVER_HTTP
) {
194 $parsedUrl = parse_url(PROXY_SERVER_HTTP
);
198 $parsedUrl = parse_url($url);
199 $this->path
= isset($parsedUrl['path']) ?
$parsedUrl['path'] : '/';
202 $this->useSSL
= $parsedUrl['scheme'] === 'https';
203 $this->host
= $parsedUrl['host'];
204 $this->port
= isset($parsedUrl['port']) ?
$parsedUrl['port'] : ($this->useSSL ?
443 : 80);
205 $this->query
= isset($parsedUrl['query']) ?
$parsedUrl['query'] : '';
207 // update the 'Host:' header if URL has changed
208 if (!empty($this->url
) && $this->url
!= $url) {
209 $this->addHeader('Host', $this->host
.($this->port
!= ($this->useSSL ?
443 : 80) ?
':'.$this->port
: ''));
216 * Executes the HTTP request.
218 public function execute() {
220 $remoteFile = new RemoteFile(($this->useSSL ?
'ssl://' : '').$this->host
, $this->port
, $this->options
['timeout']);
222 $request = $this->options
['method']." ".$this->path
.($this->query ?
'?'.$this->query
: '')." HTTP/1.0\r\n";
225 foreach ($this->headers
as $name => $values) {
226 foreach ($values as $value) {
227 $request .= $name.": ".$value."\r\n";
231 // add post parameters
232 if ($this->options
['method'] !== 'GET') $request .= $this->body
."\r\n\r\n";
234 $remoteFile->puts($request);
237 $this->replyHeaders
= array();
238 $this->replyBody
= '';
240 // read http response.
241 while (!$remoteFile->eof()) {
242 $line = $remoteFile->gets();
244 if (rtrim($line) === '') {
248 $this->replyHeaders
[] = $line;
251 $this->replyBody
.= $line;
261 private function parseReply() {
264 foreach ($this->replyHeaders
as $header) {
265 if (strpos($header, ':') === false) {
266 $headers[trim($header)] = trim($header);
269 list($key, $value) = explode(':', $header, 2);
270 $headers[$key] = trim($value);
272 $this->replyHeaders
= $headers;
275 $statusLine = reset($this->replyHeaders
);
276 $regex = new Regex('^HTTP/1.[01] (\d{3})');
277 if (!$regex->match($statusLine)) throw new SystemException("Unexpected status '".$statusLine."'");
278 $matches = $regex->getMatches();
279 $this->statusCode
= $matches[1];
282 if (isset($this->replyHeaders
['Content-Length'])) {
283 if (strlen($this->replyBody
) != $this->replyHeaders
['Content-Length']) {
284 throw new SystemException('Body length does not match length given in header');
288 // validate status code
289 switch ($this->statusCode
) {
295 if ($this->options
['maxDepth'] <= 0) throw new SystemException("Received status code '".$this->statusCode
."' from server, but recursion level is exhausted");
297 $newRequest = clone $this;
298 $newRequest->options
['maxDepth']--;
300 // The response to the request can be found under a different URI and SHOULD
301 // be retrieved using a GET method on that resource.
302 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
303 if ($this->statusCode
== '303') {
304 $newRequest->options
['method'] = 'GET';
305 $newRequest->postParameters
= array();
306 $newRequest->addHeader('Content-length', '');
307 $newRequest->addHeader('Content-Type', '');
311 $newRequest->setURL($this->replyHeaders
['Location']);
313 catch (SystemException
$e) {
314 throw new SystemException("Received 'Location: ".$this->replyHeaders
['Location']."' from server, which is invalid.", 0, $e);
318 $newRequest->execute();
320 // update data with data from the inner request
321 $this->url
= $newRequest->url
;
322 $this->statusCode
= $newRequest->statusCode
;
323 $this->replyHeaders
= $newRequest->replyHeaders
;
324 $this->replyBody
= $newRequest->replyBody
;
326 catch (SystemException
$e) {
327 // update data with data from the inner request
328 $this->url
= $newRequest->url
;
329 $this->statusCode
= $newRequest->statusCode
;
330 $this->replyHeaders
= $newRequest->replyHeaders
;
331 $this->replyBody
= $newRequest->replyBody
;
346 throw new HTTPUnauthorizedException("Received status code '".$this->statusCode
."' from server");
350 throw new HTTPNotFoundException("Received status code '404' from server");
354 throw new HTTPServerErrorException("Received status code '500' from server");
358 throw new SystemException("Received unhandled status code '".$this->statusCode
."' from server");
364 * Returns an array with the replied data.
368 public function getReply() {
370 'statusCode' => $this->statusCode
,
371 'headers' => $this->replyHeaders
,
372 'body' => $this->replyBody
,
378 * Sets options and applies default values when an option is omitted.
380 * @param array $options
382 private function setOptions(array $options) {
383 if (!isset($options['timeout'])) {
384 $options['timeout'] = 10;
387 if (!isset($options['method'])) {
388 $options['method'] = (!empty($this->postParameters
) ||
!empty($this->files
) ?
'POST' : 'GET');
391 if (!isset($options['maxDepth'])) {
392 $options['maxDepth'] = 2;
395 if (isset($options['auth'])) {
396 if (!isset($options['auth']['username'])) {
397 throw new SystemException('Username is missing in authentification data.');
399 if (!isset($options['auth']['password'])) {
400 throw new SystemException('Password is missing in authentification data.');
404 $this->options
= $options;
408 * Adds a header to this request.
409 * When an empty value is given existing headers of this name will be removed. When append
410 * is set to false existing values will be overwritten.
412 * @param string $name
413 * @param string $value
414 * @param boolean $append
416 public function addHeader($name, $value, $append = false) {
418 unset($this->headers
[$name]);
422 if ($append && isset($this->headers
[$name])) {
423 $this->headers
[$name][] = $value;
426 $this->headers
[$name] = array($value);
431 * Resets reply data when cloning.
433 private function __clone() {
434 $this->replyHeaders
= array();
435 $this->replyBody
= '';
436 $this->statusCode
= 0;