a tiny mvc framework for php using php-activerecord
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?>