1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21:
22:
23: App::uses('CakeLog', 'Log');
24: App::uses('String', 'Utility');
25:
26: 27: 28: 29: 30: 31: 32: 33:
34: class Debugger {
35:
36: 37: 38: 39: 40:
41: public $errors = array();
42:
43: 44: 45: 46: 47:
48: protected $_outputFormat = 'js';
49:
50: 51: 52: 53: 54: 55:
56: protected $_templates = array(
57: 'log' => array(
58: 'trace' => '{:reference} - {:path}, line {:line}',
59: 'error' => "{:error} ({:code}): {:description} in [{:file}, line {:line}]"
60: ),
61: 'js' => array(
62: 'error' => '',
63: 'info' => '',
64: 'trace' => '<pre class="stack-trace">{:trace}</pre>',
65: 'code' => '',
66: 'context' => '',
67: 'links' => array(),
68: 'escapeContext' => true,
69: ),
70: 'html' => array(
71: 'trace' => '<pre class="cake-error trace"><b>Trace</b> <p>{:trace}</p></pre>',
72: 'context' => '<pre class="cake-error context"><b>Context</b> <p>{:context}</p></pre>',
73: 'escapeContext' => true,
74: ),
75: 'txt' => array(
76: 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}",
77: 'code' => '',
78: 'info' => ''
79: ),
80: 'base' => array(
81: 'traceLine' => '{:reference} - {:path}, line {:line}',
82: 'trace' => "Trace:\n{:trace}\n",
83: 'context' => "Context:\n{:context}\n",
84: ),
85: 'log' => array(),
86: );
87:
88: 89: 90: 91: 92:
93: protected $_data = array();
94:
95: 96: 97: 98:
99: public function __construct() {
100: $docRef = ini_get('docref_root');
101:
102: if (empty($docRef) && function_exists('ini_set')) {
103: ini_set('docref_root', 'http://php.net/');
104: }
105: if (!defined('E_RECOVERABLE_ERROR')) {
106: define('E_RECOVERABLE_ERROR', 4096);
107: }
108:
109: $e = '<pre class="cake-error">';
110: $e .= '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-trace\')';
111: $e .= '.style.display = (document.getElementById(\'{:id}-trace\').style.display == ';
112: $e .= '\'none\' ? \'\' : \'none\');"><b>{:error}</b> ({:code})</a>: {:description} ';
113: $e .= '[<b>{:path}</b>, line <b>{:line}</b>]';
114:
115: $e .= '<div id="{:id}-trace" class="cake-stack-trace" style="display: none;">';
116: $e .= '{:links}{:info}</div>';
117: $e .= '</pre>';
118: $this->_templates['js']['error'] = $e;
119:
120: $t = '<div id="{:id}-trace" class="cake-stack-trace" style="display: none;">';
121: $t .= '{:context}{:code}{:trace}</div>';
122: $this->_templates['js']['info'] = $t;
123:
124: $links = array();
125: $link = '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-code\')';
126: $link .= '.style.display = (document.getElementById(\'{:id}-code\').style.display == ';
127: $link .= '\'none\' ? \'\' : \'none\')">Code</a>';
128: $links['code'] = $link;
129:
130: $link = '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-context\')';
131: $link .= '.style.display = (document.getElementById(\'{:id}-context\').style.display == ';
132: $link .= '\'none\' ? \'\' : \'none\')">Context</a>';
133: $links['context'] = $link;
134:
135: $this->_templates['js']['links'] = $links;
136:
137: $this->_templates['js']['context'] = '<pre id="{:id}-context" class="cake-context" ';
138: $this->_templates['js']['context'] .= 'style="display: none;">{:context}</pre>';
139:
140: $this->_templates['js']['code'] = '<pre id="{:id}-code" class="cake-code-dump" ';
141: $this->_templates['js']['code'] .= 'style="display: none;">{:code}</pre>';
142:
143: $e = '<pre class="cake-error"><b>{:error}</b> ({:code}) : {:description} ';
144: $e .= '[<b>{:path}</b>, line <b>{:line}]</b></pre>';
145: $this->_templates['html']['error'] = $e;
146:
147: $this->_templates['html']['context'] = '<pre class="cake-context"><b>Context</b> ';
148: $this->_templates['html']['context'] .= '<p>{:context}</p></pre>';
149: }
150:
151: 152: 153: 154: 155: 156:
157: public static function getInstance($class = null) {
158: static $instance = array();
159: if (!empty($class)) {
160: if (!$instance || strtolower($class) != strtolower(get_class($instance[0]))) {
161: $instance[0] = new $class();
162: }
163: }
164: if (!$instance) {
165: $instance[0] = new Debugger();
166: }
167: return $instance[0];
168: }
169:
170: 171: 172: 173: 174: 175: 176: 177: 178:
179: public static function dump($var) {
180: pr(self::exportVar($var));
181: }
182:
183: 184: 185: 186: 187: 188: 189: 190: 191:
192: public static function log($var, $level = LOG_DEBUG) {
193: $source = self::trace(array('start' => 1)) . "\n";
194: CakeLog::write($level, "\n" . $source . self::exportVar($var));
195: }
196:
197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207:
208: public static function showError($code, $description, $file = null, $line = null, $context = null) {
209: $self = Debugger::getInstance();
210:
211: if (empty($file)) {
212: $file = '[internal]';
213: }
214: if (empty($line)) {
215: $line = '??';
216: }
217:
218: $info = compact('code', 'description', 'file', 'line');
219: if (!in_array($info, $self->errors)) {
220: $self->errors[] = $info;
221: } else {
222: return;
223: }
224:
225: switch ($code) {
226: case E_PARSE:
227: case E_ERROR:
228: case E_CORE_ERROR:
229: case E_COMPILE_ERROR:
230: case E_USER_ERROR:
231: $error = 'Fatal Error';
232: $level = LOG_ERR;
233: break;
234: case E_WARNING:
235: case E_USER_WARNING:
236: case E_COMPILE_WARNING:
237: case E_RECOVERABLE_ERROR:
238: $error = 'Warning';
239: $level = LOG_WARNING;
240: break;
241: case E_NOTICE:
242: case E_USER_NOTICE:
243: $error = 'Notice';
244: $level = LOG_NOTICE;
245: break;
246: case E_DEPRECATED:
247: case E_USER_DEPRECATED:
248: $error = 'Deprecated';
249: $level = LOG_NOTICE;
250: break;
251: default:
252: return;
253: }
254:
255: $data = compact(
256: 'level', 'error', 'code', 'description', 'file', 'path', 'line', 'context'
257: );
258: echo $self->outputError($data);
259:
260: if ($error === 'Fatal Error') {
261: exit();
262: }
263: return true;
264: }
265:
266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281:
282: public static function trace($options = array()) {
283: $self = Debugger::getInstance();
284: $defaults = array(
285: 'depth' => 999,
286: 'format' => $self->_outputFormat,
287: 'args' => false,
288: 'start' => 0,
289: 'scope' => null,
290: 'exclude' => array('call_user_func_array', 'trigger_error')
291: );
292: $options = Hash::merge($defaults, $options);
293:
294: $backtrace = debug_backtrace();
295: $count = count($backtrace);
296: $back = array();
297:
298: $_trace = array(
299: 'line' => '??',
300: 'file' => '[internal]',
301: 'class' => null,
302: 'function' => '[main]'
303: );
304:
305: for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) {
306: $trace = array_merge(array('file' => '[internal]', 'line' => '??'), $backtrace[$i]);
307: $signature = $reference = '[main]';
308:
309: if (isset($backtrace[$i + 1])) {
310: $next = array_merge($_trace, $backtrace[$i + 1]);
311: $signature = $reference = $next['function'];
312:
313: if (!empty($next['class'])) {
314: $signature = $next['class'] . '::' . $next['function'];
315: $reference = $signature . '(';
316: if ($options['args'] && isset($next['args'])) {
317: $args = array();
318: foreach ($next['args'] as $arg) {
319: $args[] = Debugger::exportVar($arg);
320: }
321: $reference .= implode(', ', $args);
322: }
323: $reference .= ')';
324: }
325: }
326: if (in_array($signature, $options['exclude'])) {
327: continue;
328: }
329: if ($options['format'] === 'points' && $trace['file'] !== '[internal]') {
330: $back[] = array('file' => $trace['file'], 'line' => $trace['line']);
331: } elseif ($options['format'] === 'array') {
332: $back[] = $trace;
333: } else {
334: if (isset($self->_templates[$options['format']]['traceLine'])) {
335: $tpl = $self->_templates[$options['format']]['traceLine'];
336: } else {
337: $tpl = $self->_templates['base']['traceLine'];
338: }
339: $trace['path'] = self::trimPath($trace['file']);
340: $trace['reference'] = $reference;
341: unset($trace['object'], $trace['args']);
342: $back[] = String::insert($tpl, $trace, array('before' => '{:', 'after' => '}'));
343: }
344: }
345:
346: if ($options['format'] === 'array' || $options['format'] === 'points') {
347: return $back;
348: }
349: return implode("\n", $back);
350: }
351:
352: 353: 354: 355: 356: 357: 358:
359: public static function trimPath($path) {
360: if (!defined('CAKE_CORE_INCLUDE_PATH') || !defined('APP')) {
361: return $path;
362: }
363:
364: if (strpos($path, APP) === 0) {
365: return str_replace(APP, 'APP' . DS, $path);
366: } elseif (strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) {
367: return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path);
368: } elseif (strpos($path, ROOT) === 0) {
369: return str_replace(ROOT, 'ROOT', $path);
370: }
371:
372: return $path;
373: }
374:
375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393:
394: public static function excerpt($file, $line, $context = 2) {
395: $lines = array();
396: if (!file_exists($file)) {
397: return array();
398: }
399: $data = file_get_contents($file);
400: if (empty($data)) {
401: return $lines;
402: }
403: if (strpos($data, "\n") !== false) {
404: $data = explode("\n", $data);
405: }
406: if (!isset($data[$line])) {
407: return $lines;
408: }
409: for ($i = $line - ($context + 1); $i < $line + $context; $i++) {
410: if (!isset($data[$i])) {
411: continue;
412: }
413: $string = str_replace(array("\r\n", "\n"), "", self::_highlight($data[$i]));
414: if ($i == $line) {
415: $lines[] = '<span class="code-highlight">' . $string . '</span>';
416: } else {
417: $lines[] = $string;
418: }
419: }
420: return $lines;
421: }
422:
423: 424: 425: 426: 427: 428: 429:
430: protected static function _highlight($str) {
431: if (function_exists('hphp_log') || function_exists('hphp_gettid')) {
432: return htmlentities($str);
433: }
434: $added = false;
435: if (strpos($str, '<?php') === false) {
436: $added = true;
437: $str = "<?php \n" . $str;
438: }
439: $highlight = highlight_string($str, true);
440: if ($added) {
441: $highlight = str_replace(
442: '<?php <br />',
443: '',
444: $highlight
445: );
446: }
447: return $highlight;
448: }
449:
450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471:
472: public static function exportVar($var, $depth = 3) {
473: return self::_export($var, $depth, 0);
474: }
475:
476: 477: 478: 479: 480: 481: 482: 483:
484: protected static function _export($var, $depth, $indent) {
485: switch (self::getType($var)) {
486: case 'boolean':
487: return ($var) ? 'true' : 'false';
488: case 'integer':
489: return '(int) ' . $var;
490: case 'float':
491: return '(float) ' . $var;
492: case 'string':
493: if (trim($var) === '') {
494: return "''";
495: }
496: return "'" . $var . "'";
497: case 'array':
498: return self::_array($var, $depth - 1, $indent + 1);
499: case 'resource':
500: return strtolower(gettype($var));
501: case 'null':
502: return 'null';
503: case 'unknown':
504: return 'unknown';
505: default:
506: return self::_object($var, $depth - 1, $indent + 1);
507: }
508: }
509:
510: 511: 512: 513: 514: 515: 516: 517: 518: 519: 520: 521: 522: 523: 524: 525: 526: 527:
528: protected static function _array(array $var, $depth, $indent) {
529: $secrets = array(
530: 'password' => '*****',
531: 'login' => '*****',
532: 'host' => '*****',
533: 'database' => '*****',
534: 'port' => '*****',
535: 'prefix' => '*****',
536: 'schema' => '*****'
537: );
538: $replace = array_intersect_key($secrets, $var);
539: $var = $replace + $var;
540:
541: $out = "array(";
542: $n = $break = $end = null;
543: if (!empty($var)) {
544: $n = "\n";
545: $break = "\n" . str_repeat("\t", $indent);
546: $end = "\n" . str_repeat("\t", $indent - 1);
547: }
548: $vars = array();
549:
550: if ($depth >= 0) {
551: foreach ($var as $key => $val) {
552:
553: if ($key === 'GLOBALS' && is_array($val) && isset($val['GLOBALS'])) {
554: $val = '[recursion]';
555: } elseif ($val !== $var) {
556: $val = self::_export($val, $depth, $indent);
557: }
558: $vars[] = $break . self::exportVar($key) .
559: ' => ' .
560: $val;
561: }
562: } else {
563: $vars[] = $break . '[maximum depth reached]';
564: }
565: return $out . implode(',', $vars) . $end . ')';
566: }
567:
568: 569: 570: 571: 572: 573: 574: 575: 576:
577: protected static function _object($var, $depth, $indent) {
578: $out = '';
579: $props = array();
580:
581: $className = get_class($var);
582: $out .= 'object(' . $className . ') {';
583:
584: if ($depth > 0) {
585: $end = "\n" . str_repeat("\t", $indent - 1);
586: $break = "\n" . str_repeat("\t", $indent);
587: $objectVars = get_object_vars($var);
588: foreach ($objectVars as $key => $value) {
589: $value = self::_export($value, $depth - 1, $indent);
590: $props[] = "$key => " . $value;
591: }
592:
593: if (version_compare(PHP_VERSION, '5.3.0') >= 0) {
594: $ref = new ReflectionObject($var);
595:
596: $filters = array(
597: ReflectionProperty::IS_PROTECTED => 'protected',
598: ReflectionProperty::IS_PRIVATE => 'private',
599: );
600: foreach ($filters as $filter => $visibility) {
601: $reflectionProperties = $ref->getProperties($filter);
602: foreach ($reflectionProperties as $reflectionProperty) {
603: $reflectionProperty->setAccessible(true);
604: $property = $reflectionProperty->getValue($var);
605:
606: $value = self::_export($property, $depth - 1, $indent);
607: $key = $reflectionProperty->name;
608: $props[] = sprintf('[%s] %s => %s', $visibility, $key, $value);
609: }
610: }
611: }
612:
613: $out .= $break . implode($break, $props) . $end;
614: }
615: $out .= '}';
616: return $out;
617: }
618:
619: 620: 621: 622: 623: 624: 625: 626:
627: public static function outputAs($format = null) {
628: $self = Debugger::getInstance();
629: if ($format === null) {
630: return $self->_outputFormat;
631: }
632: if ($format !== false && !isset($self->_templates[$format])) {
633: throw new CakeException(__d('cake_dev', 'Invalid Debugger output format.'));
634: }
635: $self->_outputFormat = $format;
636: }
637:
638: 639: 640: 641: 642: 643: 644: 645: 646: 647: 648: 649: 650: 651: 652: 653: 654: 655: 656: 657: 658: 659: 660: 661: 662: 663: 664: 665: 666: 667: 668: 669: 670: 671: 672: 673: 674: 675: 676:
677: public static function addFormat($format, array $strings) {
678: $self = Debugger::getInstance();
679: if (isset($self->_templates[$format])) {
680: if (isset($strings['links'])) {
681: $self->_templates[$format]['links'] = array_merge(
682: $self->_templates[$format]['links'],
683: $strings['links']
684: );
685: unset($strings['links']);
686: }
687: $self->_templates[$format] = array_merge($self->_templates[$format], $strings);
688: } else {
689: $self->_templates[$format] = $strings;
690: }
691: return $self->_templates[$format];
692: }
693:
694: 695: 696: 697: 698: 699: 700: 701: 702: 703: 704:
705: public function output($format = null, $strings = array()) {
706: $self = Debugger::getInstance();
707: $data = null;
708:
709: if ($format === null) {
710: return Debugger::outputAs();
711: }
712:
713: if (!empty($strings)) {
714: return Debugger::addFormat($format, $strings);
715: }
716:
717: if ($format === true && !empty($self->_data)) {
718: $data = $self->_data;
719: $self->_data = array();
720: $format = false;
721: }
722: Debugger::outputAs($format);
723: return $data;
724: }
725:
726: 727: 728: 729: 730: 731:
732: public function outputError($data) {
733: $defaults = array(
734: 'level' => 0,
735: 'error' => 0,
736: 'code' => 0,
737: 'description' => '',
738: 'file' => '',
739: 'line' => 0,
740: 'context' => array(),
741: 'start' => 2,
742: );
743: $data += $defaults;
744:
745: $files = $this->trace(array('start' => $data['start'], 'format' => 'points'));
746: $code = '';
747: $file = null;
748: if (isset($files[0]['file'])) {
749: $file = $files[0];
750: } elseif (isset($files[1]['file'])) {
751: $file = $files[1];
752: }
753: if ($file) {
754: $code = $this->excerpt($file['file'], $file['line'] - 1, 1);
755: }
756: $trace = $this->trace(array('start' => $data['start'], 'depth' => '20'));
757: $insertOpts = array('before' => '{:', 'after' => '}');
758: $context = array();
759: $links = array();
760: $info = '';
761:
762: foreach ((array)$data['context'] as $var => $value) {
763: $context[] = "\${$var} = " . $this->exportVar($value, 3);
764: }
765:
766: switch ($this->_outputFormat) {
767: case false:
768: $this->_data[] = compact('context', 'trace') + $data;
769: return;
770: case 'log':
771: $this->log(compact('context', 'trace') + $data);
772: return;
773: }
774:
775: $data['trace'] = $trace;
776: $data['id'] = 'cakeErr' . uniqid();
777: $tpl = array_merge($this->_templates['base'], $this->_templates[$this->_outputFormat]);
778:
779: if (isset($tpl['links'])) {
780: foreach ($tpl['links'] as $key => $val) {
781: $links[$key] = String::insert($val, $data, $insertOpts);
782: }
783: }
784:
785: if (!empty($tpl['escapeContext'])) {
786: $context = h($context);
787: }
788:
789: $infoData = compact('code', 'context', 'trace');
790: foreach ($infoData as $key => $value) {
791: if (empty($value) || !isset($tpl[$key])) {
792: continue;
793: }
794: if (is_array($value)) {
795: $value = implode("\n", $value);
796: }
797: $info .= String::insert($tpl[$key], array($key => $value) + $data, $insertOpts);
798: }
799: $links = implode(' ', $links);
800:
801: if (isset($tpl['callback']) && is_callable($tpl['callback'])) {
802: return call_user_func($tpl['callback'], $data, compact('links', 'info'));
803: }
804: echo String::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts);
805: }
806:
807: 808: 809: 810: 811: 812: 813:
814: public static function getType($var) {
815: if (is_object($var)) {
816: return get_class($var);
817: }
818: if ($var === null) {
819: return 'null';
820: }
821: if (is_string($var)) {
822: return 'string';
823: }
824: if (is_array($var)) {
825: return 'array';
826: }
827: if (is_int($var)) {
828: return 'integer';
829: }
830: if (is_bool($var)) {
831: return 'boolean';
832: }
833: if (is_float($var)) {
834: return 'float';
835: }
836: if (is_resource($var)) {
837: return 'resource';
838: }
839: return 'unknown';
840: }
841:
842: 843: 844: 845: 846:
847: public static function checkSecurityKeys() {
848: if (Configure::read('Security.salt') === 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi') {
849: trigger_error(__d('cake_dev', 'Please change the value of \'Security.salt\' in app/Config/core.php to a salt value specific to your application'), E_USER_NOTICE);
850: }
851:
852: if (Configure::read('Security.cipherSeed') === '76859309657453542496749683645') {
853: trigger_error(__d('cake_dev', 'Please change the value of \'Security.cipherSeed\' in app/Config/core.php to a numeric (digits only) seed value specific to your application'), E_USER_NOTICE);
854: }
855: }
856:
857: }
858: