Merge branch 'master' of git://github.com/WoltLab/WCF into enhancement/cleanup
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / util / HTTPRequest.class.php
CommitLineData
86fc0430
TD
1<?php
2namespace wcf\util;
86fc0430
TD
3use wcf\system\exception\SystemException;
4use wcf\system\io\RemoteFile;
5use wcf\system\Regex;
6use wcf\system\WCF;
7
8/**
a17de04e 9 * Sends HTTP requests.
86fc0430
TD
10 * It supports POST, SSL, Basic Auth etc.
11 *
12 * @author Tim Düsterhus
13 * @copyright 2001-2012 WoltLab GmbH
14 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
15 * @package com.woltlab.wcf
16 * @subpackage util
17 * @category Community Framework
18 */
a195ffa6 19final class HTTPRequest {
86fc0430
TD
20 /**
21 * given options
a17de04e 22 * @var array
86fc0430
TD
23 */
24 private $options = array();
25
26 /**
27 * given post parameters
a17de04e 28 * @var array
86fc0430
TD
29 */
30 private $postParameters = array();
31
32 /**
a17de04e
MS
33 * indicates if request was made via SSL
34 * @var boolean
86fc0430
TD
35 */
36 private $useSSL = false;
37
38 /**
39 * target host
a17de04e 40 * @var string
86fc0430
TD
41 */
42 private $host;
43
44 /**
45 * target port
a17de04e 46 * @var integer
86fc0430
TD
47 */
48 private $port;
a17de04e 49
86fc0430
TD
50 /**
51 * target path
a17de04e 52 * @var string
86fc0430
TD
53 */
54 private $path;
55
56 /**
57 * target query string
a17de04e 58 * @var string
86fc0430
TD
59 */
60 private $query;
61
62 /**
63 * request headers
a17de04e 64 * @var array<string>
86fc0430
TD
65 */
66 private $headers = array();
67
68 /**
69 * reply headers
a17de04e 70 * @var array<string>
86fc0430
TD
71 */
72 private $replyHeaders = array();
73
74 /**
75 * reply body
a17de04e 76 * @var string
86fc0430
TD
77 */
78 private $replyBody = '';
79
80 /**
81 * reply status code
a17de04e 82 * @var integer
86fc0430
TD
83 */
84 private $statusCode = 0;
85
86 /**
a17de04e 87 * Constructs a new instance of HTTPRequest.
86fc0430
TD
88 *
89 * @param string $url URL to connect to
90 * @param array<string> $options
91 * @param array $postParameters Parameters to send via POST
92 */
93 public function __construct($url, array $options = array(), array $postParameters = array()) {
94 $this->setURL($url);
95
96 $this->postParameters = $postParameters;
97
98 $this->setOptions($options);
99
a195ffa6 100 // set default headers
3b00890e 101 $this->addHeader('User-Agent', "HTTP.PHP (HTTPRequest.class.php; WoltLab Community Framework/".WCF_VERSION."; ".WCF::getLanguage()->languageCode.")");
86fc0430
TD
102 $this->addHeader('Accept', '*/*');
103 $this->addHeader('Accept-Language', WCF::getLanguage()->languageCode);
104 if ($this->options['method'] !== 'GET') {
105 $this->addHeader('Content-length', strlen(http_build_query($this->postParameters)));
106 $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
107 }
108 if (isset($this->options['auth'])) {
109 $this->addHeader('Authorization', "Basic ".base64_encode($options['auth']['username'].":".$options['auth']['password']));
110 }
111 $this->addHeader('Host', $this->host);
112 $this->addHeader('Connection', 'Close');
113 }
114
115 /**
116 * Parses the given URL and applies PROXY_SERVER_HTTP.
117 *
a17de04e 118 * @param string $url
86fc0430
TD
119 */
120 private function setURL($url) {
121 if (PROXY_SERVER_HTTP) {
122 $parsedUrl = parse_url(PROXY_SERVER_HTTP);
123 $this->path = $url;
124 }
125 else {
126 $parsedUrl = parse_url($url);
127 $this->path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
128 }
129 $this->useSSL = $parsedUrl['scheme'] === 'https';
130 $this->host = $parsedUrl['host'];
131 $this->port = isset($parsedUrl['port']) ? $parsedUrl['port'] : ($this->useSSL ? 443 : 80);
132 $this->path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '/';
133 $this->query = isset($parsedUrl['query']) ? $parsedUrl['query'] : '';
134 }
135
136 /**
137 * Executes the HTTP request.
138 */
139 public function execute() {
140 // connect
141 $remoteFile = new RemoteFile(($this->useSSL ? 'ssl://' : '').$this->host, $this->port, $this->options['timeout']);
142
143 $request = $this->options['method']." ".$this->path.($this->query ? '?'.$this->query : '')." HTTP/1.0\r\n";
144
a195ffa6 145 // add headers
86fc0430
TD
146 foreach ($this->headers as $name => $values) {
147 foreach ($values as $value) {
148 $request .= $name.": ".$value."\r\n";
149 }
150 }
151 $request .= "\r\n";
a195ffa6 152 // add post parameters
86fc0430
TD
153 if ($this->options['method'] !== 'GET') $request .= http_build_query($this->postParameters)."\r\n\r\n";
154 $remoteFile->puts($request);
155
156 $inHeader = true;
157 $this->replyHeaders = array();
158 $this->replyBody = '';
159 // read http response.
160 while (!$remoteFile->eof()) {
161 $line = $remoteFile->gets();
162 if ($inHeader) {
163 if (rtrim($line) === '') {
164 $inHeader = false;
165 continue;
166 }
167 $this->replyHeaders[] = $line;
168 }
169 else {
170 $this->replyBody .= $line;
171 }
172 }
173
174 $this->parseReply();
175 }
176
177 /**
178 * Parses the reply.
179 */
180 private function parseReply() {
181 $headers = array();
182
183 foreach ($this->replyHeaders as $header) {
184 if (strpos($header, ':') === false) {
185 $headers[trim($header)] = trim($header);
186 continue;
187 }
188 list($key, $value) = explode(':', $header, 2);
189 $headers[$key] = trim($value);
190 }
191 $this->replyHeaders = $headers;
192
193 $statusLine = reset($this->replyHeaders);
194 $regex = new Regex('^HTTP/1.0 (\d{3})'); // we expect an HTTP 1.0 response, as we sent an HTTP 1.0 request
195 if (!$regex->match($statusLine)) throw new SystemException("Unexpected status '".$statusLine."'");
196 $matches = $regex->getMatches();
197 $statusCode = $matches[1];
198
199 switch ($statusCode) {
200 case '301':
201 case '302':
202 case '303':
203 case '307':
204 // redirect
205 if ($this->options['maxDepth'] <= 0) throw new SystemException("Got redirect status '".$statusCode."', but recursion level is exhausted");
206
207 $newRequest = clone $this;
208 $newRequest->options['maxDepth']--;
209 if ($statusCode != '307') {
210 $newRequest->options['method'] = 'GET';
211 $newRequest->postParameters = array();
212 $newRequest->addHeader('Content-length', '');
213 $newRequest->addHeader('Content-Type', '');
214 }
215 try {
216 $newRequest->setURL($this->replyHeaders['Location']);
217 }
218 catch (SystemException $e) {
219 throw new SystemException("Given redirect URL '".$this->replyHeaders['Location']."' is invalid. Probably the host is missing?", 0, $e);
220 }
221 $newRequest->execute();
222
223 $this->statusCode = $newRequest->statusCode;
224 $this->replyHeaders = $newRequest->replyHeaders;
225 $this->replyBody = $newRequest->replyBody;
226 return;
227 break;
a17de04e 228
86fc0430
TD
229 case '200':
230 case '204':
231 // we are fine
232 break;
a17de04e 233
86fc0430
TD
234 default:
235 throw new SystemException("Got status '".$statusCode."' and I don't know how to handle it");
236 break;
237 }
238
a195ffa6 239 // validate length
86fc0430
TD
240 if (isset($this->replyHeaders['Content-Length'])) {
241 if (strlen($this->replyBody) != $this->replyHeaders['Content-Length']) {
242 throw new SystemException('Body length does not match length given in header');
243 }
244 }
245 }
246
247 /**
248 * Returns an array with the replied data.
249 *
a17de04e 250 * @return array
86fc0430
TD
251 */
252 public function getReply() {
a195ffa6
TD
253 return array(
254 'statusCode' => $this->statusCode,
255 'headers' => $this->replyHeaders,
256 'body' => $this->replyBody
257 );
86fc0430
TD
258 }
259
260 /**
261 * Sets options and applies default values when an option is omitted.
262 *
a17de04e 263 * @param array $options
86fc0430
TD
264 */
265 private function setOptions(array $options) {
266 if (!isset($options['timeout'])) {
267 $options['timeout'] = 30;
268 }
269
270 if (!isset($options['method'])) {
271 $options['method'] = (!empty($this->postParameters) ? 'POST' : 'GET');
272 }
273
274 if (!isset($options['maxDepth'])) {
275 $options['maxDepth'] = 2;
276 }
277
278 if (isset($options['auth'])) {
279 if (!isset($options['auth']['username'])) {
280 throw new SystemException('username is missing in authentification data');
281 }
282 if (!isset($options['auth']['password'])) {
283 throw new SystemException('password is missing in authentification data');
284 }
285 }
286
287 $this->options = $options;
288 }
289
290 /**
291 * Adds a header to this request.
292 * When an empty value is given existing headers of this name will be remove. When append
293 * is set to false existing values will be overwritten.
294 *
a17de04e
MS
295 * @param string $name
296 * @param string $value
297 * @param boolean $append
86fc0430
TD
298 */
299 public function addHeader($name, $value, $append = false) {
300 if ($value === '') {
301 unset($this->headers[$name]);
302 return;
303 }
304
305 if ($append && isset($this->headers[$name])) {
306 $this->headers[$name][] = $value;
307 }
308
309 $this->headers[$name] = (array) $value;
310 }
311
312 /**
313 * Resets reply data when cloning.
314 */
315 private function __clone() {
316 $this->replyHeaders = array();
317 $this->replyBody = '';
318 $this->statusCode = 0;
319 }
320}