a tiny mvc framework for php using php-activerecord
at master 354 lines 8.9 kB view raw
1<?php 2/* 3 error and exception handling, logging, and notification 4*/ 5 6namespace HalfMoon; 7 8class StringMaskedDuringRescue { 9 private $string; 10 public $masked; 11 12 public function __construct($s, $mask = "[hidden]") { 13 $this->string = $s; 14 $this->masked = $mask; 15 } 16 17 public function __toString() { 18 return $this->string; 19 } 20} 21 22class Rescuer { 23 static $already_rescued = false; 24 25 /* exceptions that won't trigger an email in notify_of_exception() */ 26 static $exceptions_to_suppress = array( 27 '\HalfMoon\RoutingException', 28 '\HalfMoon\InvalidAuthenticityToken', 29 '\HalfMoon\UndefinedFunction', 30 '\HalfMoon\BadRequest', 31 '\ActiveRecord\RecordNotFound', 32 ); 33 34 static function error_handler($errno, $errstr, $errfile, $errline) { 35 return Rescuer::shutdown_error_handler(array( 36 "type" => $errno, 37 "message" => $errstr, 38 "file" => $errfile, 39 "line" => $errline, 40 )); 41 } 42 43 /* handle after-shutdown errors (like parse errors) */ 44 static function shutdown_error_handler($error = null) { 45 if (is_null($error)) 46 if (is_null($error = error_get_last())) 47 return; 48 49 /* if this shouldn't be reported at all, just bail */ 50 if (!((bool)($error["type"] & ini_get("error_reporting")))) 51 return; 52 53 $exception = new \ErrorException($error["message"], 0, $error["type"], 54 $error["file"], $error["line"]); 55 56 $title = $error["message"] . " in " . $error["file"] . " on line " 57 . $error["line"]; 58 59 /* everything according to the error_reporting ini value should be 60 * logged */ 61 Rescuer::log_exception($exception, $title, $request = null); 62 63 Rescuer::notify_of_exception($exception, $title, $request); 64 65 /* if it's a major/fatal problem (according to 66 * http://php.net/manual/en/errorfunc.constants.php), then we should 67 * show the user an error page and exit */ 68 if (in_array($error["type"], array(E_ERROR, E_PARSE, E_CORE_ERROR, 69 E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR))) 70 return Rescuer::rescue_in_public($exception, $title, $request); 71 } 72 73 /* handle exceptions by logging them, notifying about them (if we're in 74 * production), and then showing the user an error page */ 75 static function rescue_exception($exception, $request = null) { 76 $title = get_class($exception); 77 78 /* activerecord includes the stack trace in the message, so strip it 79 * out */ 80 if ($exception instanceof \ActiveRecord\DatabaseException) 81 $title .= ": " . preg_replace("/\nStack trace:.*/s", "", 82 $exception->getMessage()); 83 elseif ($exception->getMessage()) 84 $title .= ": " . $exception->getMessage() . " in " 85 . $exception->getFile() . " on line " . $exception->getLine(); 86 87 Rescuer::log_exception($exception, $title, $request); 88 89 Rescuer::notify_of_exception($exception, $title, $request); 90 91 if (php_sapi_name() == "cli") 92 exit(1); 93 else 94 return Rescuer::rescue_in_public($exception, $title, $request); 95 } 96 97 /* log an exception, mail it, try to show the user something */ 98 static function log_exception($exception, $title, $request) { 99 Log::error($title . ":"); 100 101 if (!is_null($exception) && is_object($exception)) 102 foreach (static::masked_stack_trace($exception) as $line) 103 Log::error($line); 104 105 return; 106 } 107 108 static function masked_stack_trace($exception, $html = false) { 109 $trace = array(); 110 111 foreach ($exception->getTrace() as $call) { 112 $out = ""; 113 114 if (!$html) 115 $out .= " "; 116 117 if (isset($call["file"])) { 118 $call["file"] = preg_replace("/^" . preg_quote(HALFMOON_ROOT, 119 "/") . "\/?/", "", $call["file"]); 120 121 if ($html) { 122 $fileparts = explode("/", $call["file"]); 123 124 for ($x = 0; $x < count($fileparts); $x++) { 125 $fileparts[$x] = h($fileparts[$x]); 126 127 if ($x == count($fileparts) - 1) 128 $fileparts[$x] = "<strong>" . $fileparts[$x] 129 . "</strong>"; 130 } 131 132 $out .= join("/", $fileparts); 133 } else 134 $out .= $call["file"]; 135 } 136 137 elseif (isset($call["class"])) 138 $out .= $call["class"]; 139 140 else 141 $out .= "unknown"; 142 143 $out .= ":"; 144 145 if (isset($call["line"])) 146 $out .= $call["line"]; 147 148 $out .= " in "; 149 150 if ($html) 151 $out .= "<strong>" . h($call["function"]) . "(</strong>"; 152 else 153 $out .= $call["function"] . "("; 154 155 foreach ($call["args"] as $x => $arg) { 156 if ($x) 157 $out .= ", "; 158 159 $m = static::_mask_object($arg); 160 if ($html) 161 $out .= h($m); 162 else 163 $out .= $m; 164 } 165 166 $out .= ($html ? "<strong>)</strong>" : ")"); 167 168 array_push($trace, $out); 169 } 170 171 return $trace; 172 } 173 174 static function _mask_object($obj) { 175 $out = null; 176 177 if (is_object($obj) && 178 get_class($obj) == "HalfMoon\\StringMaskedDuringRescue") 179 $out = "'" . $obj->masked . "'"; 180 181 elseif (is_object($obj)) 182 $out = get_class($obj) . (method_exists($obj, "__toString") ? 183 "(" . (string)$obj . ")" : ""); 184 185 elseif (is_string($obj)) 186 $out = "'" . $obj . "'"; 187 188 elseif (is_array($obj)) { 189 $out = "["; 190 191 $assoc = \HalfMoon\Utils::is_assoc($obj); 192 193 $t = array(); 194 foreach ($obj as $k => $v) 195 array_push($t, ($assoc ? $k . ":" : "") 196 . static::_mask_object($v)); 197 198 $out .= join(", ", $t) . "]"; 199 } 200 201 else 202 $out = (string)$obj; 203 204 return $out; 205 } 206 207 /* mail off the details of the exception */ 208 static function notify_of_exception($exception, $title, $request) { 209 if (HALFMOON_ENV != "production") 210 return; 211 212 if (static::$already_rescued) 213 return; 214 215 foreach (static::$exceptions_to_suppress as $e) 216 if ($exception instanceof $e) 217 return; 218 219 $config = Config::instance(); 220 221 if (!isset($config->exception_notification_recipient)) 222 return; 223 224 /* render the text template and mail it off */ 225 @ob_end_clean(); 226 @ob_start(); 227 @require(HALFMOON_ROOT . "/halfmoon/lib/rescue.ptxt"); 228 $mail_body = trim(@ob_get_contents()); 229 @ob_end_clean(); 230 231 @mail($config->exception_notification_recipient, 232 $config->exception_notification_subject . " " . $title, 233 $mail_body); 234 235 static::$already_rescued = true; 236 237 return; 238 } 239 240 /* return a friendly error page to the user (or a full one with debugging 241 * if we're in development mode with display_errors turned on) */ 242 static function rescue_in_public($exception, $title, $request) { 243 /* kill all buffered output */ 244 while (count(@ob_list_handlers())) 245 @ob_end_clean(); 246 247 if (HALFMOON_ENV == "development" && ini_get("display_errors")) 248 require_once(__DIR__ . "/rescue.phtml"); 249 else { 250 /* production mode, try to handle gracefully */ 251 252 if ($exception instanceof \HalfMoon\RoutingException || 253 $exception instanceof \HalfMoon\UndefinedFunction || 254 $exception instanceof \ActiveRecord\RecordNotFound) { 255 Request::send_status_header(404); 256 257 if (file_exists($f = HALFMOON_ROOT . "/public/404.html")) { 258 Log::error("Rendering " . $f); 259 require_once($f); 260 } else { 261 /* failsafe */ 262 ?> 263 <html> 264 <head> 265 <title>File Not Found</title> 266 </head> 267 <body> 268 <h1>File Not Found</h1> 269 The file you requested could not be found. An additional 270 error occured while processing the error document. 271 </body> 272 </html> 273 <?php 274 } 275 } 276 277 elseif ($exception instanceof \HalfMoon\BadRequest) { 278 Request::send_status_header(400); 279 280 if (file_exists($f = HALFMOON_ROOT . "/public/400.html")) { 281 Log::error("Rendering " . $f); 282 require_once($f); 283 } 284 } 285 286 elseif ($exception instanceof \HalfMoon\InvalidAuthenticityToken) { 287 /* be like rails and give the odd 422 status */ 288 Request::send_status_header(422); 289 290 if (file_exists($f = HALFMOON_ROOT . "/public/422.html")) { 291 Log::error("Rendering " . $f); 292 require_once($f); 293 } else { 294 /* failsafe */ 295 ?> 296 <html> 297 <head> 298 <title>Change Rejected</title> 299 </head> 300 <body> 301 <h1>Change Rejected</h1> 302 The change you submitted was rejected due to a security 303 problem. An additional error occured while processing the 304 error document. 305 </body> 306 </html> 307 <?php 308 } 309 } 310 311 else { 312 Request::send_status_header(500); 313 314 if (file_exists($f = HALFMOON_ROOT . "/public/500.html")) { 315 Log::error("Rendering " . $f); 316 require_once($f); 317 } else { 318 /* failsafe */ 319 ?> 320 <html> 321 <head> 322 <title>Application Error</title> 323 </head> 324 <body> 325 <h1>Application Error</h1> 326 An internal application error occured while processing your 327 request. Additionally, an error occured while processing 328 the error document. 329 </body> 330 </html> 331 <?php 332 } 333 } 334 } 335 336 static::$already_rescued = true; 337 338 /* that's it, end of the line */ 339 exit; 340 } 341} 342 343/* make traditional errors throw exceptions so we can handle everything in one 344 * place */ 345set_error_handler(array("\\HalfMoon\\Rescuer", "error_handler")); 346 347/* catch errors on the cleanup that we couldn't handle at runtime */ 348register_shutdown_function(array("\\HalfMoon\\Rescuer", 349 "shutdown_error_handler")); 350 351/* and catch all exceptions */ 352set_exception_handler(array("\\HalfMoon\\Rescuer", "rescue_exception")); 353 354?>