Commit | Line | Data |
---|---|---|
86fc0430 TD |
1 | <?php |
2 | namespace wcf\util; | |
86fc0430 TD |
3 | use wcf\system\exception\SystemException; |
4 | use wcf\system\io\RemoteFile; | |
5 | use wcf\system\Regex; | |
6 | use 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 | 19 | final 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 | } |