Adds missing trailing whitespaces
[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/**
a195ffa6 9 * HTTPRequest 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
22 * @var array
23 */
24 private $options = array();
25
26 /**
27 * given post parameters
28 * @var array
29 */
30 private $postParameters = array();
31
32 /**
33 * is the request made via SSL
34 * @var boolean
35 */
36 private $useSSL = false;
37
38 /**
39 * target host
40 * @var string
41 */
42 private $host;
43
44 /**
45 * target port
46 * @var integer
47 */
48 private $port;
49
50 /**
51 * target path
52 * @var string
53 */
54 private $path;
55
56 /**
57 * target query string
58 * @var string
59 */
60 private $query;
61
62 /**
63 * request headers
64 * @var array<string>
65 */
66 private $headers = array();
67
68 /**
69 * reply headers
70 * @var array<string>
71 */
72 private $replyHeaders = array();
73
74 /**
75 * reply body
76 * @var string
77 */
78 private $replyBody = '';
79
80 /**
81 * reply status code
82 * @var integer
83 */
84 private $statusCode = 0;
85
86 /**
87 * Constructs a new request.
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 *
118 * @param string $url
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;
228 case '200':
229 case '204':
230 // we are fine
231 break;
232 default:
233 throw new SystemException("Got status '".$statusCode."' and I don't know how to handle it");
234 break;
235 }
236
a195ffa6 237 // validate length
86fc0430
TD
238 if (isset($this->replyHeaders['Content-Length'])) {
239 if (strlen($this->replyBody) != $this->replyHeaders['Content-Length']) {
240 throw new SystemException('Body length does not match length given in header');
241 }
242 }
243 }
244
245 /**
246 * Returns an array with the replied data.
247 *
248 * @return array
249 */
250 public function getReply() {
a195ffa6
TD
251 return array(
252 'statusCode' => $this->statusCode,
253 'headers' => $this->replyHeaders,
254 'body' => $this->replyBody
255 );
86fc0430
TD
256 }
257
258 /**
259 * Sets options and applies default values when an option is omitted.
260 *
261 * @param array $options
262 */
263 private function setOptions(array $options) {
264 if (!isset($options['timeout'])) {
265 $options['timeout'] = 30;
266 }
267
268 if (!isset($options['method'])) {
269 $options['method'] = (!empty($this->postParameters) ? 'POST' : 'GET');
270 }
271
272 if (!isset($options['maxDepth'])) {
273 $options['maxDepth'] = 2;
274 }
275
276 if (isset($options['auth'])) {
277 if (!isset($options['auth']['username'])) {
278 throw new SystemException('username is missing in authentification data');
279 }
280 if (!isset($options['auth']['password'])) {
281 throw new SystemException('password is missing in authentification data');
282 }
283 }
284
285 $this->options = $options;
286 }
287
288 /**
289 * Adds a header to this request.
290 * When an empty value is given existing headers of this name will be remove. When append
291 * is set to false existing values will be overwritten.
292 *
293 * @param string $name
294 * @param string $value
295 * @param boolean $append
296 */
297 public function addHeader($name, $value, $append = false) {
298 if ($value === '') {
299 unset($this->headers[$name]);
300 return;
301 }
302
303 if ($append && isset($this->headers[$name])) {
304 $this->headers[$name][] = $value;
305 }
306
307 $this->headers[$name] = (array) $value;
308 }
309
310 /**
311 * Resets reply data when cloning.
312 */
313 private function __clone() {
314 $this->replyHeaders = array();
315 $this->replyBody = '';
316 $this->statusCode = 0;
317 }
318}