a tiny mvc framework for php using php-activerecord
at master 370 lines 10 kB view raw
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?>