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