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:
253: $source = static::trace(['start' => 1]);
254: $source .= "\n";
255:
256: Log::write($level, "\n" . $source . static::exportVar($var, $depth));
257: }
258:
259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274:
275: public static function trace(array $options = [])
276: {
277: return Debugger::formatTrace(debug_backtrace(), $options);
278: }
279:
280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296:
297: public static function formatTrace($backtrace, $options = [])
298: {
299: if ($backtrace instanceof Exception) {
300: $backtrace = $backtrace->getTrace();
301: }
302: $self = Debugger::getInstance();
303: $defaults = [
304: 'depth' => 999,
305: 'format' => $self->_outputFormat,
306: 'args' => false,
307: 'start' => 0,
308: 'scope' => null,
309: 'exclude' => ['call_user_func_array', 'trigger_error']
310: ];
311: $options = Hash::merge($defaults, $options);
312:
313: $count = count($backtrace);
314: $back = [];
315:
316: $_trace = [
317: 'line' => '??',
318: 'file' => '[internal]',
319: 'class' => null,
320: 'function' => '[main]'
321: ];
322:
323: for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) {
324: $trace = $backtrace[$i] + ['file' => '[internal]', 'line' => '??'];
325: $signature = $reference = '[main]';
326:
327: if (isset($backtrace[$i + 1])) {
328: $next = $backtrace[$i + 1] + $_trace;
329: $signature = $reference = $next['function'];
330:
331: if (!empty($next['class'])) {
332: $signature = $next['class'] . '::' . $next['function'];
333: $reference = $signature . '(';
334: if ($options['args'] && isset($next['args'])) {
335: $args = [];
336: foreach ($next['args'] as $arg) {
337: $args[] = Debugger::exportVar($arg);
338: }
339: $reference .= implode(', ', $args);
340: }
341: $reference .= ')';
342: }
343: }
344: if (in_array($signature, $options['exclude'])) {
345: continue;
346: }
347: if ($options['format'] === 'points' && $trace['file'] !== '[internal]') {
348: $back[] = ['file' => $trace['file'], 'line' => $trace['line']];
349: } elseif ($options['format'] === 'array') {
350: $back[] = $trace;
351: } else {
352: if (isset($self->_templates[$options['format']]['traceLine'])) {
353: $tpl = $self->_templates[$options['format']]['traceLine'];
354: } else {
355: $tpl = $self->_templates['base']['traceLine'];
356: }
357: $trace['path'] = static::trimPath($trace['file']);
358: $trace['reference'] = $reference;
359: unset($trace['object'], $trace['args']);
360: $back[] = Text::insert($tpl, $trace, ['before' => '{:', 'after' => '}']);
361: }
362: }
363:
364: if ($options['format'] === 'array' || $options['format'] === 'points') {
365: return $back;
366: }
367:
368: return implode("\n", $back);
369: }
370:
371: 372: 373: 374: 375: 376: 377:
378: public static function trimPath($path)
379: {
380: if (defined('APP') && strpos($path, APP) === 0) {
381: return str_replace(APP, 'APP/', $path);
382: }
383: if (defined('CAKE_CORE_INCLUDE_PATH') && strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) {
384: return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path);
385: }
386: if (defined('ROOT') && strpos($path, ROOT) === 0) {
387: return str_replace(ROOT, 'ROOT', $path);
388: }
389:
390: return $path;
391: }
392:
393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413:
414: public static function excerpt($file, $line, $context = 2)
415: {
416: $lines = [];
417: if (!file_exists($file)) {
418: return [];
419: }
420: $data = file_get_contents($file);
421: if (empty($data)) {
422: return $lines;
423: }
424: if (strpos($data, "\n") !== false) {
425: $data = explode("\n", $data);
426: }
427: $line--;
428: if (!isset($data[$line])) {
429: return $lines;
430: }
431: for ($i = $line - $context; $i < $line + $context + 1; $i++) {
432: if (!isset($data[$i])) {
433: continue;
434: }
435: $string = str_replace(["\r\n", "\n"], '', static::_highlight($data[$i]));
436: if ($i == $line) {
437: $lines[] = '<span class="code-highlight">' . $string . '</span>';
438: } else {
439: $lines[] = $string;
440: }
441: }
442:
443: return $lines;
444: }
445:
446: 447: 448: 449: 450: 451: 452:
453: protected static function _highlight($str)
454: {
455: if (function_exists('hphp_log') || function_exists('hphp_gettid')) {
456: return htmlentities($str);
457: }
458: $added = false;
459: if (strpos($str, '<?php') === false) {
460: $added = true;
461: $str = "<?php \n" . $str;
462: }
463: $highlight = highlight_string($str, true);
464: if ($added) {
465: $highlight = str_replace(
466: ['<?php <br/>', '<?php <br />'],
467: '',
468: $highlight
469: );
470: }
471:
472: return $highlight;
473: }
474:
475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495:
496: public static function exportVar($var, $depth = 3)
497: {
498: return static::_export($var, $depth, 0);
499: }
500:
501: 502: 503: 504: 505: 506: 507: 508:
509: protected static function _export($var, $depth, $indent)
510: {
511: switch (static::getType($var)) {
512: case 'boolean':
513: return $var ? 'true' : 'false';
514: case 'integer':
515: return '(int) ' . $var;
516: case 'float':
517: return '(float) ' . $var;
518: case 'string':
519: if (trim($var) === '' && ctype_space($var) === false) {
520: return "''";
521: }
522:
523: return "'" . $var . "'";
524: case 'array':
525: return static::_array($var, $depth - 1, $indent + 1);
526: case 'resource':
527: return strtolower(gettype($var));
528: case 'null':
529: return 'null';
530: case 'unknown':
531: return 'unknown';
532: default:
533: return static::_object($var, $depth - 1, $indent + 1);
534: }
535: }
536:
537: 538: 539: 540: 541: 542: 543: 544: 545: 546: 547: 548: 549: 550: 551: 552: 553: 554:
555: protected static function _array(array $var, $depth, $indent)
556: {
557: $out = '[';
558: $break = $end = null;
559: if (!empty($var)) {
560: $break = "\n" . str_repeat("\t", $indent);
561: $end = "\n" . str_repeat("\t", $indent - 1);
562: }
563: $vars = [];
564:
565: if ($depth >= 0) {
566: $outputMask = (array)static::outputMask();
567: foreach ($var as $key => $val) {
568:
569: if ($key === 'GLOBALS' && is_array($val) && isset($val['GLOBALS'])) {
570: $val = '[recursion]';
571: } elseif (array_key_exists($key, $outputMask)) {
572: $val = (string)$outputMask[$key];
573: } elseif ($val !== $var) {
574: $val = static::_export($val, $depth, $indent);
575: }
576: $vars[] = $break . static::exportVar($key) .
577: ' => ' .
578: $val;
579: }
580: } else {
581: $vars[] = $break . '[maximum depth reached]';
582: }
583:
584: return $out . implode(',', $vars) . $end . ']';
585: }
586:
587: 588: 589: 590: 591: 592: 593: 594: 595:
596: protected static function _object($var, $depth, $indent)
597: {
598: $out = '';
599: $props = [];
600:
601: $className = get_class($var);
602: $out .= 'object(' . $className . ') {';
603: $break = "\n" . str_repeat("\t", $indent);
604: $end = "\n" . str_repeat("\t", $indent - 1);
605:
606: if ($depth > 0 && method_exists($var, '__debugInfo')) {
607: try {
608: return $out . "\n" .
609: substr(static::_array($var->__debugInfo(), $depth - 1, $indent), 1, -1) .
610: $end . '}';
611: } catch (Exception $e) {
612: $message = $e->getMessage();
613:
614: return $out . "\n(unable to export object: $message)\n }";
615: }
616: }
617:
618: if ($depth > 0) {
619: $outputMask = (array)static::outputMask();
620: $objectVars = get_object_vars($var);
621: foreach ($objectVars as $key => $value) {
622: $value = array_key_exists($key, $outputMask) ? $outputMask[$key] : static::_export($value, $depth - 1, $indent);
623: $props[] = "$key => " . $value;
624: }
625:
626: $ref = new ReflectionObject($var);
627:
628: $filters = [
629: ReflectionProperty::IS_PROTECTED => 'protected',
630: ReflectionProperty::IS_PRIVATE => 'private',
631: ];
632: foreach ($filters as $filter => $visibility) {
633: $reflectionProperties = $ref->getProperties($filter);
634: foreach ($reflectionProperties as $reflectionProperty) {
635: $reflectionProperty->setAccessible(true);
636: $property = $reflectionProperty->getValue($var);
637:
638: $value = static::_export($property, $depth - 1, $indent);
639: $key = $reflectionProperty->name;
640: $props[] = sprintf(
641: '[%s] %s => %s',
642: $visibility,
643: $key,
644: array_key_exists($key, $outputMask) ? $outputMask[$key] : $value
645: );
646: }
647: }
648:
649: $out .= $break . implode($break, $props) . $end;
650: }
651: $out .= '}';
652:
653: return $out;
654: }
655:
656: 657: 658: 659: 660:
661: public static function getOutputFormat()
662: {
663: return Debugger::getInstance()->_outputFormat;
664: }
665:
666: 667: 668: 669: 670: 671: 672:
673: public static function setOutputFormat($format)
674: {
675: $self = Debugger::getInstance();
676:
677: if (!isset($self->_templates[$format])) {
678: throw new InvalidArgumentException('Invalid Debugger output format.');
679: }
680: $self->_outputFormat = $format;
681: }
682:
683: 684: 685: 686: 687: 688: 689: 690: 691:
692: public static function outputAs($format = null)
693: {
694: deprecationWarning(
695: 'Debugger::outputAs() is deprecated. Use Debugger::getOutputFormat()/setOutputFormat() instead.'
696: );
697: $self = Debugger::getInstance();
698: if ($format === null) {
699: return $self->_outputFormat;
700: }
701:
702: if (!isset($self->_templates[$format])) {
703: throw new InvalidArgumentException('Invalid Debugger output format.');
704: }
705: $self->_outputFormat = $format;
706:
707: return null;
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: 751: 752:
753: public static function addFormat($format, array $strings)
754: {
755: $self = Debugger::getInstance();
756: if (isset($self->_templates[$format])) {
757: if (isset($strings['links'])) {
758: $self->_templates[$format]['links'] = array_merge(
759: $self->_templates[$format]['links'],
760: $strings['links']
761: );
762: unset($strings['links']);
763: }
764: $self->_templates[$format] = $strings + $self->_templates[$format];
765: } else {
766: $self->_templates[$format] = $strings;
767: }
768:
769: return $self->_templates[$format];
770: }
771:
772: 773: 774: 775: 776: 777:
778: public function outputError($data)
779: {
780: $defaults = [
781: 'level' => 0,
782: 'error' => 0,
783: 'code' => 0,
784: 'description' => '',
785: 'file' => '',
786: 'line' => 0,
787: 'context' => [],
788: 'start' => 2,
789: ];
790: $data += $defaults;
791:
792: $files = static::trace(['start' => $data['start'], 'format' => 'points']);
793: $code = '';
794: $file = null;
795: if (isset($files[0]['file'])) {
796: $file = $files[0];
797: } elseif (isset($files[1]['file'])) {
798: $file = $files[1];
799: }
800: if ($file) {
801: $code = static::excerpt($file['file'], $file['line'], 1);
802: }
803: $trace = static::trace(['start' => $data['start'], 'depth' => '20']);
804: $insertOpts = ['before' => '{:', 'after' => '}'];
805: $context = [];
806: $links = [];
807: $info = '';
808:
809: foreach ((array)$data['context'] as $var => $value) {
810: $context[] = "\${$var} = " . static::exportVar($value, 3);
811: }
812:
813: switch ($this->_outputFormat) {
814: case false:
815: $this->_data[] = compact('context', 'trace') + $data;
816:
817: return;
818: case 'log':
819: static::log(compact('context', 'trace') + $data);
820:
821: return;
822: }
823:
824: $data['trace'] = $trace;
825: $data['id'] = 'cakeErr' . uniqid();
826: $tpl = $this->_templates[$this->_outputFormat] + $this->_templates['base'];
827:
828: if (isset($tpl['links'])) {
829: foreach ($tpl['links'] as $key => $val) {
830: $links[$key] = Text::insert($val, $data, $insertOpts);
831: }
832: }
833:
834: if (!empty($tpl['escapeContext'])) {
835: $context = h($context);
836: $data['description'] = h($data['description']);
837: }
838:
839: $infoData = compact('code', 'context', 'trace');
840: foreach ($infoData as $key => $value) {
841: if (empty($value) || !isset($tpl[$key])) {
842: continue;
843: }
844: if (is_array($value)) {
845: $value = implode("\n", $value);
846: }
847: $info .= Text::insert($tpl[$key], [$key => $value] + $data, $insertOpts);
848: }
849: $links = implode(' ', $links);
850:
851: if (isset($tpl['callback']) && is_callable($tpl['callback'])) {
852: call_user_func($tpl['callback'], $data, compact('links', 'info'));
853:
854: return;
855: }
856: echo Text::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts);
857: }
858:
859: 860: 861: 862: 863: 864: 865:
866: public static function getType($var)
867: {
868: if (is_object($var)) {
869: return get_class($var);
870: }
871: if ($var === null) {
872: return 'null';
873: }
874: if (is_string($var)) {
875: return 'string';
876: }
877: if (is_array($var)) {
878: return 'array';
879: }
880: if (is_int($var)) {
881: return 'integer';
882: }
883: if (is_bool($var)) {
884: return 'boolean';
885: }
886: if (is_float($var)) {
887: return 'float';
888: }
889: if (is_resource($var)) {
890: return 'resource';
891: }
892:
893: return 'unknown';
894: }
895:
896: 897: 898: 899: 900: 901: 902: 903: 904: 905:
906: public static function printVar($var, $location = [], $showHtml = null)
907: {
908: $location += ['file' => null, 'line' => null];
909: $file = $location['file'];
910: $line = $location['line'];
911: $lineInfo = '';
912: if ($file) {
913: $search = [];
914: if (defined('ROOT')) {
915: $search = [ROOT];
916: }
917: if (defined('CAKE_CORE_INCLUDE_PATH')) {
918: array_unshift($search, CAKE_CORE_INCLUDE_PATH);
919: }
920: $file = str_replace($search, '', $file);
921: }
922: $html = <<<HTML
923: <div class="cake-debug-output" style="direction:ltr">
924: %s
925: <pre class="cake-debug">
926: %s
927: </pre>
928: </div>
929: HTML;
930: $text = <<<TEXT
931: %s
932: ########## DEBUG ##########
933: %s
934: ###########################
935:
936: TEXT;
937: $template = $html;
938: if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') || $showHtml === false) {
939: $template = $text;
940: if ($file && $line) {
941: $lineInfo = sprintf('%s (line %s)', $file, $line);
942: }
943: }
944: if ($showHtml === null && $template !== $text) {
945: $showHtml = true;
946: }
947: $var = Debugger::exportVar($var, 25);
948: if ($showHtml) {
949: $template = $html;
950: $var = h($var);
951: if ($file && $line) {
952: $lineInfo = sprintf('<span><strong>%s</strong> (line <strong>%s</strong>)</span>', $file, $line);
953: }
954: }
955: printf($template, $lineInfo, $var);
956: }
957:
958: 959: 960: 961: 962:
963: public static function checkSecurityKeys()
964: {
965: if (Security::getSalt() === '__SALT__') {
966: 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);
967: }
968: }
969: }
970: