1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie;
13
14 class Debug
15 {
16 const MODE_DEV = 'dev';
17 const MODE_TEST = 'test';
18 const MODE_PRODUCTION = 'production';
19
20 const MAX_MESSAGES = 100;
21
22 static public $mode = 'dev';
23
24 25 26 27 28
29 static public $last_error;
30
31 32 33 34 35
36 static public $last_error_message;
37
38 static public function synthesize_config(array $fragments)
39 {
40 $config = call_user_func_array('ICanBoogie\array_merge_recursive', $fragments);
41 $config = array_merge($config, $config['modes'][$config['mode']]);
42
43 return $config;
44 }
45
46 static private $config;
47
48 static private $config_code_sample = true;
49 static private $config_line_number = true;
50 static private $config_report = false;
51 static private $config_report_address = null;
52 static private $config_stack_trace = true;
53 static private $config_exception_chain = true;
54 static private $config_verbose = true;
55
56 static public function is_dev()
57 {
58 return self::$mode == self::MODE_DEV;
59 }
60
61 static public function is_test()
62 {
63 return self::$mode == self::MODE_TEST;
64 }
65
66 static public function is_production()
67 {
68 return self::$mode == self::MODE_PRODUCTION;
69 }
70
71 72 73 74 75
76 static public function configure(array $config)
77 {
78 $mode = self::$mode;
79 $modes = [];
80
81 foreach ($config as $directive => $value)
82 {
83 if ($directive == 'mode')
84 {
85 $mode = $value;
86
87 continue;
88 }
89 else if ($directive == 'modes')
90 {
91 $modes = $value;
92
93 continue;
94 }
95
96 $directive = 'config_' . $directive;
97
98 self::$$directive = $value;
99 }
100
101 self::$mode = $mode;
102
103 if (isset($modes[$mode]))
104 {
105 foreach ($modes[$mode] as $directive => $value)
106 {
107 $directive = 'config_' . $directive;
108
109 self::$$directive = $value;
110 }
111 }
112 }
113
114 115 116
117 static public function shutdown_handler()
118 {
119 global $core;
120
121 if (self::$logs)
122 {
123 if (!headers_sent() && isset($core))
124 {
125 $core->session;
126 }
127
128 $_SESSION['alerts'] = self::$logs;
129 }
130
131 $error = error_get_last();
132
133 if ($error && $error['type'] == E_ERROR)
134 {
135 $message = self::format_alert($error);
136
137 self::report($message);
138 }
139 }
140
141 static public function restore_logs(Event $event, Session $session)
142 {
143 if ($session->alerts)
144 {
145 self::$logs = array_merge($session->alerts, self::$logs);
146 }
147
148 $session->alerts = [];
149 }
150
151 152 153 154 155 156 157
158
159 160 161 162 163 164 165 166 167 168 169 170 171
172 static public function error_handler($no, $str, $file, $line, $context)
173 {
174 if (!headers_sent())
175 {
176 header('HTTP/1.0 500 ' . strip_tags($str));
177 }
178
179 $trace = debug_backtrace();
180
181 array_shift($trace);
182
183 $error = [
184
185 'type' => $no,
186 'message' => $str,
187 'file' => $file,
188 'line' => $line,
189 'context' => $context,
190 'trace' => $trace
191
192 ];
193
194 self::$last_error = $error;
195 self::$last_error_message = $str;
196
197 $message = self::format_alert($error);
198
199 self::report($message);
200
201 if (self::$config_verbose)
202 {
203 echo $message;
204
205 flush();
206 }
207 }
208
209 210 211 212 213
214 static public function exception_handler(\Exception $exception)
215 {
216 if (!headers_sent())
217 {
218 $code = $exception->getCode();
219
220 $message = $exception->getMessage();
221 $message = strip_tags($message);
222 $message = str_replace([ "\r\n", "\n" ], '', $message);
223
224 header("HTTP/1.0 $code $message");
225 }
226
227 $message = self::format_alert($exception);
228
229 self::report($message);
230
231 exit($message);
232 }
233
234 const MAX_STRING_LEN = 16;
235
236 static private $error_names = [
237
238 E_ERROR => 'Error',
239 E_WARNING => 'Warning',
240 E_PARSE => 'Parse error',
241 E_NOTICE => 'Notice'
242
243 ];
244
245 246 247 248 249 250 251 252 253 254
255 static public function format_alert($alert)
256 {
257 $type = 'Error';
258 $class = 'error';
259 $file = null;
260 $line = null;
261 $message = null;
262 $trace = null;
263 $more = null;
264
265 if (is_array($alert))
266 {
267 $file = $alert['file'];
268 $line = $alert['line'];
269 $message = $alert['message'];
270
271 if (isset(self::$error_names[$alert['type']]))
272 {
273 $type = self::$error_names[$alert['type']];
274 }
275
276 if (isset($alert['trace']))
277 {
278 $trace = $alert['trace'];
279 }
280 }
281 else if ($alert instanceof \Exception)
282 {
283 $type = get_class($alert);
284 $class = 'exception';
285 $file = $alert->getFile();
286 $line = $alert->getLine();
287 $message = $alert->getMessage();
288 $trace = $alert->getTrace();
289 }
290
291 $message = strip_tags($message, '<a><em><q><strong>');
292
293 if ($trace)
294 {
295 $more .= self::format_trace($trace);
296 }
297
298 if (is_array($alert) && $file)
299 {
300 $more .= self::format_code_sample($file, $line);
301 }
302
303 $file = strip_root($file);
304
305 $previous = null;
306
307 if ( $alert instanceof \Exception)
308 {
309 $previous = $alert->getPrevious();
310
311 if ($previous)
312 {
313 $previous = self::format_alert($previous);
314 }
315 }
316
317 return <<<EOT
318 <pre class="alert alert-error $class">
319 <strong>$type with the following message:</strong>
320
321 $message
322
323 in <em>$file</em> at line <em>$line</em>$more{$previous}
324 </pre>
325 EOT;
326 }
327
328 329 330 331 332 333 334
335 static public function format_trace(array $trace)
336 {
337 $root = str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']);
338 $count = count($trace);
339 $count_max = strlen((string) $count);
340
341 $rc = "\n\n<strong>Stack trace:</strong>\n";
342
343 foreach ($trace as $i => $node)
344 {
345 $trace_file = null;
346 $trace_line = 0;
347 $trace_class = null;
348 $trace_type = null;
349 $trace_args = null;
350
351 extract($node, EXTR_PREFIX_ALL, 'trace');
352
353 if ($trace_file)
354 {
355 $trace_file = str_replace('\\', '/', $trace_file);
356 $trace_file = str_replace($root, '', $trace_file);
357 }
358
359 $params = [];
360
361 if ($trace_args)
362 {
363 foreach ($trace_args as $arg)
364 {
365 switch (gettype($arg))
366 {
367 case 'array': $arg = 'Array'; break;
368 case 'object': $arg = 'Object of ' . get_class($arg); break;
369 case 'resource': $arg = 'Resource of type ' . get_resource_type($arg); break;
370 case 'null': $arg = 'null'; break;
371
372 default:
373 {
374 if (strlen($arg) > self::MAX_STRING_LEN)
375 {
376 $arg = substr($arg, 0, self::MAX_STRING_LEN) . '...';
377 }
378
379 $arg = '\'' . $arg .'\'';
380 }
381 break;
382 }
383
384 $params[] = $arg;
385 }
386 }
387
388 $rc .= sprintf
389 (
390 "\n%{$count_max}d. %s(%d): %s%s%s(%s)",
391
392 $count - $i, $trace_file, $trace_line, $trace_class, $trace_type,
393 $trace_function, escape(implode(', ', $params))
394 );
395 }
396
397 return $rc;
398 }
399
400 401 402 403 404 405 406 407
408 static public function format_code_sample($file, $line=0)
409 {
410 $sample = '';
411 $fh = new \SplFileObject($file);
412 $lines = new \LimitIterator($fh, $line < 5 ? 0 : $line - 5, 10);
413
414 foreach ($lines as $i => $str)
415 {
416 $i++;
417
418 $str = escape(rtrim($str));
419
420 if ($i == $line)
421 {
422 $str = '<ins>' . $str . '</ins>';
423 }
424
425 $str = str_replace("\t", "\xC2\xA0\xC2\xA0\xC2\xA0\xC2\xA0", $str);
426 $sample .= sprintf("\n%6d. %s", $i, $str);
427 }
428
429 return "\n\n<strong>Code sample:</strong>\n$sample";
430 }
431
432 433 434 435 436 437 438 439
440 static public function report($message)
441 {
442 $report_address = self::$config_report_address;
443
444 if (!$report_address)
445 {
446 return;
447 }
448
449 $more = "\n\n<strong>Request URI:</strong>\n\n" . escape($_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI']);
450
451 if (!empty($_SERVER['HTTP_REFERER']))
452 {
453 $more .= "\n\n<strong>Referer:</strong>\n\n" . escape($_SERVER['HTTP_REFERER']);
454 }
455
456 if (!empty($_SERVER['HTTP_USER_AGENT']))
457 {
458 $more .= "\n\n<strong>User Agent:</strong>\n\n" . escape($_SERVER['HTTP_USER_AGENT']);
459 }
460
461 $more .= "\n\n<strong>Remote address:</strong>\n\n" . escape($_SERVER['REMOTE_ADDR']);
462
463 if ($message instanceof \Exception)
464 {
465 $message = self::format_alert($message);
466 }
467
468 $message = str_replace('</pre>', '', $message);
469 $message = trim($message) . $more . '</pre>';
470
471
472
473
474
475 $hash = md5($message);
476
477 if (isset($_SESSION['wddebug']['reported'][$hash]))
478 {
479 return;
480 }
481
482 $_SESSION['wddebug']['reported'][$hash] = true;
483
484
485
486
487
488 $host = $_SERVER['SERVER_NAME'];
489 $host = str_replace('www.', '', $host);
490
491 $parts = [
492
493 'From' => 'icanboogie@' . $host,
494 'Content-Type' => 'text/html; charset=' . CHARSET
495
496 ];
497
498 $headers = '';
499
500 foreach ($parts as $key => $value)
501 {
502 $headers .= $key .= ': ' . $value . "\r\n";
503 }
504
505 mail($report_address, __CLASS__ . ': Report from ' . $host, $message, $headers);
506 }
507
508 static public $logs = [];
509
510 static public function log($type, $message, array $params=[], $message_id=null)
511 {
512 if (empty(self::$logs[$type]))
513 {
514 self::$logs[$type] = [];
515 }
516
517
518
519
520
521 $messages = &self::$logs[$type];
522
523 if ($messages)
524 {
525 $max = self::MAX_MESSAGES;
526 $count = count($messages);
527
528 if ($count > $max)
529 {
530 $messages = array_splice($messages, $count - $max);
531
532 array_unshift($messages, [ '*** SLICED', [] ]);
533 }
534 }
535
536 $message_id ? $messages[$message_id] = [ $message, $params ] : $messages[] = [ $message, $params ];
537 }
538
539 540 541 542 543 544 545
546 static public function get_messages($type)
547 {
548 if (empty(self::$logs[$type]))
549 {
550 return [];
551 }
552
553 $rc = [];
554
555 foreach (self::$logs[$type] as $message)
556 {
557 list($message, $args) = $message;
558
559 if ($args)
560 {
561 $message = format($message, $args);
562 }
563
564 $rc[] = (string) $message;
565 }
566
567 return $rc;
568 }
569
570 571 572 573 574 575 576 577
578 static public function fetch_messages($type)
579 {
580 $rc = self::get_messages($type);
581
582 self::$logs[$type] = [];
583
584 return $rc;
585 }
586
587 588 589 590 591 592 593
594 static private function strip_root($path)
595 {
596 $root = DOCUMENT_ROOT;
597
598 if (strpos($path, $root) === 0)
599 {
600 return substr($path, strlen($root) - 1);
601 }
602
603 return $path;
604 }
605 }