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