a tiny mvc framework for php using php-activerecord
1<?php
2/*
3 an individual HTTP request
4*/
5
6namespace HalfMoon;
7
8class Request {
9 const TRUSTED_PROXIES = '/^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i';
10
11 public $start_times = array();
12
13 public $url, $scheme, $host, $port, $path, $query;
14
15 public $referrer;
16
17 public $get = array();
18 public $post = array();
19 public $params = array();
20
21 public $headers = array();
22
23 public $etag = null;
24 public $redirected_to = null;
25
26 public function __get($name) {
27 if (method_exists($this, "get_" . $name)) {
28 $name = "get_" . $name;
29 return $this->$name();
30 }
31
32 throw new Exception("invalid property " . $name);
33 }
34
35 /* a register_shutdown function that will log some stats about the time it
36 * took to process and the url */
37 public static function log_runtime($req) {
38 $end_time = microtime(true);
39
40 $framework_time = (float)($req->start_times["request"] -
41 $req->start_times["init"]);
42 if (isset($req->start_times["app"]))
43 $app_time = (float)($end_time - $req->start_times["app"]);
44 $total_time = (float)($end_time - $req->start_times["init"]);
45
46 if (class_exists('\ActiveRecord\ConnectionManager') &&
47 \ActiveRecord\ConnectionManager::connection_count()) {
48 $db_time = (float)\ActiveRecord\ConnectionManager::
49 get_connection()->reset_database_time();
50
51 if (isset($app_time))
52 $app_time -= $db_time;
53 }
54
55 $status = "200";
56 foreach (headers_list() as $header)
57 if (preg_match("/^Status: (\d+)/", $header, $m))
58 $status = $m[1];
59
60 $log = "Completed in " . sprintf("%0.5f", $total_time);
61
62 if (isset($db_time))
63 $log .= " | DB: " . sprintf("%0.5f", $db_time) . " ("
64 . intval(($db_time / $total_time) * 100) . "%)";
65
66 if (isset($app_time))
67 $log .= " | App: " . sprintf("%0.5f", $app_time) . " ("
68 . intval(($app_time / $total_time) * 100) . "%)";
69
70 $log .= " | Framework: " . sprintf("%0.5f", $framework_time)
71 . " (" . intval(($framework_time / $total_time) * 100)
72 . "%)";
73
74 $log .= " | " . $status . " [" . $req->url . "]";
75
76 if (isset($req->redirected_to))
77 $log .= " -> [" . $req->redirected_to . "]";
78
79 Log::info($log);
80 }
81
82 /* send both style status headers; the first for mod_php to actually see
83 * it, and the second so it shows up in headers_list() and for fastcgi */
84 public static function send_status_header($status) {
85 header($_SERVER["SERVER_PROTOCOL"] . " " . $status, true, $status);
86 header("Status: " . $status, true, $status);
87 }
88
89 /* build a request from the web server interface */
90 public function __construct($url, $get_vars, $post_vars, $headers,
91 $start_time = null) {
92 $this->start_times["init"] = ($start_time ? $start_time
93 : microtime(true));
94 $this->start_times["request"] = microtime(true);
95
96 $url_parts = parse_url($url);
97
98 $this->scheme = @$url_parts["scheme"];
99 if (empty($this->scheme))
100 $this->scheme = "http";
101
102 $this->host = @$url_parts["host"];
103
104 $this->port = @$url_parts["port"];
105 if (empty($this->port)) {
106 if (strtolower($this->scheme) == "https")
107 $this->port = 443;
108 else
109 $this->port = 80;
110 }
111
112 /* normalize path, strip leading and trailing slashes */
113 $this->path = @$url_parts["path"];
114 if ($this->path == "")
115 $this->path = "/";
116 $path_dirs = explode("/", $this->path);
117 $tpath = array();
118 foreach ($path_dirs as $tdir) {
119 if ($tdir == "" || $tdir == ".")
120 continue;
121 elseif ($tdir == "..") {
122 array_pop($tpath);
123 continue;
124 }
125
126 array_push($tpath, $tdir);
127 }
128 $this->path = join("/", $tpath);
129
130 $this->query = @$url_parts["query"];
131
132 $this->url = $this->scheme . "://" . $this->host;
133 if ($this->port != 80 && $this->port != 443)
134 $this->url .= ":" . $this->port;
135
136 $this->url .= "/" . $this->path;
137
138 if ($this->query != "")
139 $this->url .= "?" . $this->query;
140
141 /* if this looks like a request from ie's castrated XDomainRequest()
142 * then it didn't send a proper content-type, so try to read and parse
143 * it to put it into $_POST ourselves */
144 if (empty($post_vars) && !empty($headers["REQUEST_METHOD"]) &&
145 $headers["REQUEST_METHOD"] == "POST" &&
146 !empty($headers["HTTP_ORIGIN"])) {
147 if (empty($HTTP_RAW_POST_DATA))
148 $HTTP_RAW_POST_DATA = file_get_contents("php://input");
149
150 if (!empty($HTTP_RAW_POST_DATA)) {
151 $pairs = explode("&", $HTTP_RAW_POST_DATA);
152 foreach ($pairs as $pair) {
153 if (!empty($pair)) {
154 list($k, $v) = explode('=', $pair);
155 $k = urldecode($k);
156 $v = urldecode($v);
157
158 if (preg_match("/^([^\[]+)\[([^\]]+)\]?$/", $k, $m)) {
159 if (!is_array($post_vars[$m[1]]))
160 $post_vars[$m[1]] = array();
161
162 $post_vars[$m[1]][$m[2]] = $v;
163 } else
164 $post_vars[$k] = $v;
165 }
166 }
167 }
168 }
169
170 /* store get and post vars in $params first according to php's
171 variables_order setting (EGPCS by default) */
172 foreach (str_split(ini_get("variables_order")) as $vtype) {
173 $varray = null;
174
175 switch (strtoupper($vtype)) {
176 case "P":
177 $varray = $post_vars;
178 break;
179 case "G":
180 $varray = $get_vars;
181 break;
182 }
183
184 if ($varray)
185 foreach ($varray as $k => $v) {
186 /* look for arrays that might be inside this array */
187 if (is_array($v)) {
188 $newv = array();
189
190 /* TODO: recurse */
191 foreach ($v as $vk => $vv) {
192 if (preg_match("/^([^\[]+)\[([^\]]+)\]?$/", $vk, $m)) {
193 if (!is_array($newv[$m[1]]))
194 $newv[$m[1]] = array();
195
196 $newv[$m[1]][$m[2]] = $vv;
197 } else
198 $newv[$vk] = $vv;
199 }
200
201 $this->params[$k] = $newv;
202 } else
203 $this->params[$k] = $v;
204 }
205 }
206
207 $this->get = $get_vars;
208 $this->post = $post_vars;
209
210 $this->headers = $headers;
211
212 $this->referrer = @$headers["HTTP_REFERER"];
213 }
214
215 public function __toString() {
216 return $this->request_method() . " " . $this->url . " ("
217 . $this->remote_ip() . ")";
218 }
219
220 /* pass ourself to the router and handle the url. if it fails, try to
221 * handle it gracefully. */
222 public function process() {
223 if (Config::log_level_at_least("short"))
224 register_shutdown_function(array("\\HalfMoon\\Request",
225 "log_runtime"), $this);
226
227 try {
228 ob_start();
229
230 Router::instance()->takeRouteForRequest($this);
231
232 /* if we received an If-None-Match header from the client and our
233 * generated etag matches it, send a not-modified header and no
234 * data */
235 if (empty($this->redirected_to) && $this->etag_matches_inm()) {
236 $headers = (array)headers_sent();
237 ob_end_clean();
238 foreach ($headers as $header)
239 header($header);
240
241 Request::send_status_header(304);
242 }
243
244 else {
245 if (empty($this->redirected_to))
246 $this->send_etag_header();
247
248 if (ob_get_level())
249 ob_end_flush();
250 }
251 }
252
253 catch (\Exception $e) {
254 /* rescue, log, notify (if necessary), exit */
255 if (class_exists("\\HalfMoon\\Rescuer"))
256 Rescuer::rescue_exception($e, $this);
257 else
258 throw $e;
259 }
260 }
261
262 /* determine originating IP address. REMOTE_ADDR is the standard but will
263 * fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
264 * HTTP_X_FORWARDED_FOR are set by proxies so check for these if
265 * REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma- delimited
266 * list in the case of multiple chained proxies; the last address which is
267 * not trusted is the originating IP. */
268 private $_remote_ip;
269 public function get_remote_ip() {
270 if ($this->_remote_ip)
271 return $this->_remote_ip;
272
273 $remote_addr_list = array();
274 if (!empty($this->headers["REMOTE_ADDR"]))
275 $remote_addr_list = explode(",", $this->headers["REMOTE_ADDR"]);
276
277 foreach ($remote_addr_list as $addr)
278 if (!preg_match(Request::TRUSTED_PROXIES, $addr))
279 return ($this->_remote_ip = $addr);
280
281 $forwarded_for = array();
282
283 if (!empty($this->headers["HTTP_X_FORWARDED_FOR"]))
284 $forwarded_for = explode(",",
285 $this->headers["HTTP_X_FORWARDED_FOR"]);
286
287 if (!empty($this->headers["HTTP_CLIENT_IP"])) {
288 if (!in_array($this->headers["HTTP_CLIENT_IP"], $forwarded_for))
289 throw new HalfMoonException("IP spoofing attack? "
290 . "HTTP_CLIENT_IP="
291 . var_export($this->headers["HTTP_CLIENT_IP"], true)
292 . ", HTTP_X_FORWARDED_FOR="
293 . var_export($this->headers["HTTP_X_FORWARDED_FOR"], true));
294
295 return ($this->_remote_ip = $this->headers["HTTP_CLIENT_IP"]);
296 }
297
298 if (!empty($forwarded_for)) {
299 while (count($forwarded_for) > 1 &&
300 preg_match(Request::TRUSTED_PROXIES, trim(end($forwarded_for))))
301 array_pop($forwarded_for);
302
303 return ($this->_remote_ip = trim(end($forwarded_for)));
304 }
305
306 return ($this->_remote_ip = $this->headers["REMOTE_ADDR"]);
307 }
308
309 /* whether this was a request received over ssl */
310 private $_ssl;
311 public function get_ssl() {
312 if (isset($this->_ssl))
313 return $this->_ssl;
314
315 return ($this->_ssl =
316 (!empty($this->headers["HTTPS"]) &&
317 $this->headers["HTTPS"] == "on") ||
318 (!empty($this->headers["HTTP_X_FORWARDED_PROTO"]) &&
319 $this->headers["HTTP_X_FORWARDED_PROTO"] == "https") ||
320 (!empty($this->headers["SCRIPT_URI"]) && preg_match("/^https:/",
321 $this->headers["SCRIPT_URI"]))
322 );
323 }
324
325 /* "GET", "PUT", etc. */
326 public function get_request_method() {
327 return strtoupper($this->headers["REQUEST_METHOD"]);
328 }
329
330 /* the user's browser as reported by the server */
331 public function get_user_agent() {
332 return @$this->headers["HTTP_USER_AGENT"];
333 }
334
335 private function etag_matches_inm() {
336 if (!empty($this->headers["HTTP_IF_NONE_MATCH"])) {
337 $this->calculate_etag();
338 if ($this->etag === $this->headers["HTTP_IF_NONE_MATCH"])
339 return true;
340 }
341
342 return false;
343 }
344
345 private function send_etag_header() {
346 $this->calculate_etag();
347 header("ETag: " . $this->etag);
348 }
349
350 private function calculate_etag() {
351 if (empty($this->etag))
352 $this->etag = md5(ob_get_contents());
353 }
354
355 /* legacy functions that have been changed to pseudo-properties */
356 public function remote_ip() {
357 return $this->get_remote_ip();
358 }
359 public function ssl() {
360 return $this->get_ssl();
361 }
362 public function request_method() {
363 return $this->get_request_method();
364 }
365 public function user_agent() {
366 return $this->get_user_agent();
367 }
368}
369
370?>