comparison cake/libs/debugger.php @ 0:261e66bd5a0c

hg init
author Shoshi TAMAKI <shoshi@cr.ie.u-ryukyu.ac.jp>
date Sun, 24 Jul 2011 21:08:31 +0900
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:261e66bd5a0c
1 <?php
2 /**
3 * Framework debugging and PHP error-handling class
4 *
5 * Provides enhanced logging, stack traces, and rendering debug views
6 *
7 * PHP versions 4 and 5
8 *
9 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10 * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
11 *
12 * Licensed under The MIT License
13 * Redistributions of files must retain the above copyright notice.
14 *
15 * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
16 * @link http://cakephp.org CakePHP(tm) Project
17 * @package cake
18 * @subpackage cake.cake.libs
19 * @since CakePHP(tm) v 1.2.4560
20 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
21 */
22
23 /**
24 * Included libraries.
25 *
26 */
27 if (!class_exists('Object')) {
28 require_once LIBS . 'object.php';
29 }
30 if (!class_exists('CakeLog')) {
31 require_once LIBS . 'cake_log.php';
32 }
33 if (!class_exists('String')) {
34 require_once LIBS . 'string.php';
35 }
36
37 /**
38 * Provide custom logging and error handling.
39 *
40 * Debugger overrides PHP's default error handling to provide stack traces and enhanced logging
41 *
42 * @package cake
43 * @subpackage cake.cake.libs
44 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
45 */
46 class Debugger extends Object {
47
48 /**
49 * A list of errors generated by the application.
50 *
51 * @var array
52 * @access public
53 */
54 var $errors = array();
55
56 /**
57 * Contains the base URL for error code documentation.
58 *
59 * @var string
60 * @access public
61 */
62 var $helpPath = null;
63
64 /**
65 * The current output format.
66 *
67 * @var string
68 * @access protected
69 */
70 var $_outputFormat = 'js';
71
72 /**
73 * Templates used when generating trace or error strings. Can be global or indexed by the format
74 * value used in $_outputFormat.
75 *
76 * @var string
77 * @access protected
78 */
79 var $_templates = array(
80 'log' => array(
81 'trace' => '{:reference} - {:path}, line {:line}',
82 'error' => "{:error} ({:code}): {:description} in [{:file}, line {:line}]"
83 ),
84 'js' => array(
85 'error' => '',
86 'info' => '',
87 'trace' => '<pre class="stack-trace">{:trace}</pre>',
88 'code' => '',
89 'context' => '',
90 'links' => array()
91 ),
92 'html' => array(
93 'trace' => '<pre class="cake-debug trace"><b>Trace</b> <p>{:trace}</p></pre>',
94 'context' => '<pre class="cake-debug context"><b>Context</b> <p>{:context}</p></pre>'
95 ),
96 'txt' => array(
97 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}",
98 'context' => "Context:\n{:context}\n",
99 'trace' => "Trace:\n{:trace}\n",
100 'code' => '',
101 'info' => ''
102 ),
103 'base' => array(
104 'traceLine' => '{:reference} - {:path}, line {:line}'
105 )
106 );
107
108 /**
109 * Holds current output data when outputFormat is false.
110 *
111 * @var string
112 * @access private
113 */
114 var $_data = array();
115
116 /**
117 * Constructor.
118 *
119 */
120 function __construct() {
121 $docRef = ini_get('docref_root');
122
123 if (empty($docRef) && function_exists('ini_set')) {
124 ini_set('docref_root', 'http://php.net/');
125 }
126 if (!defined('E_RECOVERABLE_ERROR')) {
127 define('E_RECOVERABLE_ERROR', 4096);
128 }
129 if (!defined('E_DEPRECATED')) {
130 define('E_DEPRECATED', 8192);
131 }
132
133 $e = '<pre class="cake-debug">';
134 $e .= '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-trace\')';
135 $e .= '.style.display = (document.getElementById(\'{:id}-trace\').style.display == ';
136 $e .= '\'none\' ? \'\' : \'none\');"><b>{:error}</b> ({:code})</a>: {:description} ';
137 $e .= '[<b>{:path}</b>, line <b>{:line}</b>]';
138
139 $e .= '<div id="{:id}-trace" class="cake-stack-trace" style="display: none;">';
140 $e .= '{:links}{:info}</div>';
141 $e .= '</pre>';
142 $this->_templates['js']['error'] = $e;
143
144 $t = '<div id="{:id}-trace" class="cake-stack-trace" style="display: none;">';
145 $t .= '{:context}{:code}{:trace}</div>';
146 $this->_templates['js']['info'] = $t;
147
148 $links = array();
149 $link = '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-code\')';
150 $link .= '.style.display = (document.getElementById(\'{:id}-code\').style.display == ';
151 $link .= '\'none\' ? \'\' : \'none\')">Code</a>';
152 $links['code'] = $link;
153
154 $link = '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-context\')';
155 $link .= '.style.display = (document.getElementById(\'{:id}-context\').style.display == ';
156 $link .= '\'none\' ? \'\' : \'none\')">Context</a>';
157 $links['context'] = $link;
158
159 $links['help'] = '<a href="{:helpPath}{:code}" target="_blank">Help</a>';
160 $this->_templates['js']['links'] = $links;
161
162 $this->_templates['js']['context'] = '<pre id="{:id}-context" class="cake-context" ';
163 $this->_templates['js']['context'] .= 'style="display: none;">{:context}</pre>';
164
165 $this->_templates['js']['code'] = '<div id="{:id}-code" class="cake-code-dump" ';
166 $this->_templates['js']['code'] .= 'style="display: none;"><pre>{:code}</pre></div>';
167
168 $e = '<pre class="cake-debug"><b>{:error}</b> ({:code}) : {:description} ';
169 $e .= '[<b>{:path}</b>, line <b>{:line}]</b></pre>';
170 $this->_templates['html']['error'] = $e;
171
172 $this->_templates['html']['context'] = '<pre class="cake-debug context"><b>Context</b> ';
173 $this->_templates['html']['context'] .= '<p>{:context}</p></pre>';
174 }
175
176 /**
177 * Returns a reference to the Debugger singleton object instance.
178 *
179 * @return object
180 * @access public
181 * @static
182 */
183 function &getInstance($class = null) {
184 static $instance = array();
185 if (!empty($class)) {
186 if (!$instance || strtolower($class) != strtolower(get_class($instance[0]))) {
187 $instance[0] = & new $class();
188 if (Configure::read() > 0) {
189 Configure::version(); // Make sure the core config is loaded
190 $instance[0]->helpPath = Configure::read('Cake.Debugger.HelpPath');
191 }
192 }
193 }
194
195 if (!$instance) {
196 $instance[0] =& new Debugger();
197 if (Configure::read() > 0) {
198 Configure::version(); // Make sure the core config is loaded
199 $instance[0]->helpPath = Configure::read('Cake.Debugger.HelpPath');
200 }
201 }
202 return $instance[0];
203 }
204
205 /**
206 * Formats and outputs the contents of the supplied variable.
207 *
208 * @param $var mixed the variable to dump
209 * @return void
210 * @see Debugger::exportVar()
211 * @access public
212 * @static
213 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
214 */
215 function dump($var) {
216 $_this =& Debugger::getInstance();
217 pr($_this->exportVar($var));
218 }
219
220 /**
221 * Creates an entry in the log file. The log entry will contain a stack trace from where it was called.
222 * as well as export the variable using exportVar. By default the log is written to the debug log.
223 *
224 * @param $var mixed Variable or content to log
225 * @param $level int type of log to use. Defaults to LOG_DEBUG
226 * @return void
227 * @static
228 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
229 */
230 function log($var, $level = LOG_DEBUG) {
231 $_this =& Debugger::getInstance();
232 $source = $_this->trace(array('start' => 1)) . "\n";
233 CakeLog::write($level, "\n" . $source . $_this->exportVar($var));
234 }
235
236 /**
237 * Overrides PHP's default error handling.
238 *
239 * @param integer $code Code of error
240 * @param string $description Error description
241 * @param string $file File on which error occurred
242 * @param integer $line Line that triggered the error
243 * @param array $context Context
244 * @return boolean true if error was handled
245 * @access public
246 */
247 function handleError($code, $description, $file = null, $line = null, $context = null) {
248 if (error_reporting() == 0 || $code === 2048 || $code === 8192) {
249 return;
250 }
251
252 $_this =& Debugger::getInstance();
253
254 if (empty($file)) {
255 $file = '[internal]';
256 }
257 if (empty($line)) {
258 $line = '??';
259 }
260 $path = $_this->trimPath($file);
261
262 $info = compact('code', 'description', 'file', 'line');
263 if (!in_array($info, $_this->errors)) {
264 $_this->errors[] = $info;
265 } else {
266 return;
267 }
268
269 switch ($code) {
270 case E_PARSE:
271 case E_ERROR:
272 case E_CORE_ERROR:
273 case E_COMPILE_ERROR:
274 case E_USER_ERROR:
275 $error = 'Fatal Error';
276 $level = LOG_ERROR;
277 break;
278 case E_WARNING:
279 case E_USER_WARNING:
280 case E_COMPILE_WARNING:
281 case E_RECOVERABLE_ERROR:
282 $error = 'Warning';
283 $level = LOG_WARNING;
284 break;
285 case E_NOTICE:
286 case E_USER_NOTICE:
287 $error = 'Notice';
288 $level = LOG_NOTICE;
289 break;
290 default:
291 return;
292 break;
293 }
294
295 $helpCode = null;
296 if (!empty($_this->helpPath) && preg_match('/.*\[([0-9]+)\]$/', $description, $codes)) {
297 if (isset($codes[1])) {
298 $helpID = $codes[1];
299 $description = trim(preg_replace('/\[[0-9]+\]$/', '', $description));
300 }
301 }
302
303 $data = compact(
304 'level', 'error', 'code', 'helpID', 'description', 'file', 'path', 'line', 'context'
305 );
306 echo $_this->_output($data);
307
308 if (Configure::read('log')) {
309 $tpl = $_this->_templates['log']['error'];
310 $options = array('before' => '{:', 'after' => '}');
311 CakeLog::write($level, String::insert($tpl, $data, $options));
312 }
313
314 if ($error == 'Fatal Error') {
315 exit();
316 }
317 return true;
318 }
319
320 /**
321 * Outputs a stack trace based on the supplied options.
322 *
323 * ### Options
324 *
325 * - `depth` - The number of stack frames to return. Defaults to 999
326 * - `format` - The format you want the return. Defaults to the currently selected format. If
327 * format is 'array' or 'points' the return will be an array.
328 * - `args` - Should arguments for functions be shown? If true, the arguments for each method call
329 * will be displayed.
330 * - `start` - The stack frame to start generating a trace from. Defaults to 0
331 *
332 * @param array $options Format for outputting stack trace
333 * @return mixed Formatted stack trace
334 * @access public
335 * @static
336 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
337 */
338 function trace($options = array()) {
339 $_this =& Debugger::getInstance();
340 $defaults = array(
341 'depth' => 999,
342 'format' => $_this->_outputFormat,
343 'args' => false,
344 'start' => 0,
345 'scope' => null,
346 'exclude' => null
347 );
348 $options += $defaults;
349
350 $backtrace = debug_backtrace();
351 $count = count($backtrace);
352 $back = array();
353
354 $_trace = array(
355 'line' => '??',
356 'file' => '[internal]',
357 'class' => null,
358 'function' => '[main]'
359 );
360
361 for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) {
362 $trace = array_merge(array('file' => '[internal]', 'line' => '??'), $backtrace[$i]);
363
364 if (isset($backtrace[$i + 1])) {
365 $next = array_merge($_trace, $backtrace[$i + 1]);
366 $reference = $next['function'];
367
368 if (!empty($next['class'])) {
369 $reference = $next['class'] . '::' . $reference . '(';
370 if ($options['args'] && isset($next['args'])) {
371 $args = array();
372 foreach ($next['args'] as $arg) {
373 $args[] = Debugger::exportVar($arg);
374 }
375 $reference .= join(', ', $args);
376 }
377 $reference .= ')';
378 }
379 } else {
380 $reference = '[main]';
381 }
382 if (in_array($reference, array('call_user_func_array', 'trigger_error'))) {
383 continue;
384 }
385 if ($options['format'] == 'points' && $trace['file'] != '[internal]') {
386 $back[] = array('file' => $trace['file'], 'line' => $trace['line']);
387 } elseif ($options['format'] == 'array') {
388 $back[] = $trace;
389 } else {
390 if (isset($_this->_templates[$options['format']]['traceLine'])) {
391 $tpl = $_this->_templates[$options['format']]['traceLine'];
392 } else {
393 $tpl = $_this->_templates['base']['traceLine'];
394 }
395 $trace['path'] = Debugger::trimPath($trace['file']);
396 $trace['reference'] = $reference;
397 unset($trace['object'], $trace['args']);
398 $back[] = String::insert($tpl, $trace, array('before' => '{:', 'after' => '}'));
399 }
400 }
401
402 if ($options['format'] == 'array' || $options['format'] == 'points') {
403 return $back;
404 }
405 return implode("\n", $back);
406 }
407
408 /**
409 * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core
410 * path with 'CORE'.
411 *
412 * @param string $path Path to shorten
413 * @return string Normalized path
414 * @access public
415 * @static
416 */
417 function trimPath($path) {
418 if (!defined('CAKE_CORE_INCLUDE_PATH') || !defined('APP')) {
419 return $path;
420 }
421
422 if (strpos($path, APP) === 0) {
423 return str_replace(APP, 'APP' . DS, $path);
424 } elseif (strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) {
425 return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path);
426 } elseif (strpos($path, ROOT) === 0) {
427 return str_replace(ROOT, 'ROOT', $path);
428 }
429 $corePaths = App::core('cake');
430
431 foreach ($corePaths as $corePath) {
432 if (strpos($path, $corePath) === 0) {
433 return str_replace($corePath, 'CORE' .DS . 'cake' .DS, $path);
434 }
435 }
436 return $path;
437 }
438
439 /**
440 * Grabs an excerpt from a file and highlights a given line of code
441 *
442 * @param string $file Absolute path to a PHP file
443 * @param integer $line Line number to highlight
444 * @param integer $context Number of lines of context to extract above and below $line
445 * @return array Set of lines highlighted
446 * @access public
447 * @static
448 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
449 */
450 function excerpt($file, $line, $context = 2) {
451 $data = $lines = array();
452 if (!file_exists($file)) {
453 return array();
454 }
455 $data = @explode("\n", file_get_contents($file));
456
457 if (empty($data) || !isset($data[$line])) {
458 return;
459 }
460 for ($i = $line - ($context + 1); $i < $line + $context; $i++) {
461 if (!isset($data[$i])) {
462 continue;
463 }
464 $string = str_replace(array("\r\n", "\n"), "", highlight_string($data[$i], true));
465 if ($i == $line) {
466 $lines[] = '<span class="code-highlight">' . $string . '</span>';
467 } else {
468 $lines[] = $string;
469 }
470 }
471 return $lines;
472 }
473
474 /**
475 * Converts a variable to a string for debug output.
476 *
477 * @param string $var Variable to convert
478 * @return string Variable as a formatted string
479 * @access public
480 * @static
481 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
482 */
483 function exportVar($var, $recursion = 0) {
484 $_this =& Debugger::getInstance();
485 switch (strtolower(gettype($var))) {
486 case 'boolean':
487 return ($var) ? 'true' : 'false';
488 break;
489 case 'integer':
490 case 'double':
491 return $var;
492 break;
493 case 'string':
494 if (trim($var) == "") {
495 return '""';
496 }
497 return '"' . h($var) . '"';
498 break;
499 case 'object':
500 return get_class($var) . "\n" . $_this->__object($var);
501 case 'array':
502 $out = "array(";
503 $vars = array();
504 foreach ($var as $key => $val) {
505 if ($recursion >= 0) {
506 if (is_numeric($key)) {
507 $vars[] = "\n\t" . $_this->exportVar($val, $recursion - 1);
508 } else {
509 $vars[] = "\n\t" .$_this->exportVar($key, $recursion - 1)
510 . ' => ' . $_this->exportVar($val, $recursion - 1);
511 }
512 }
513 }
514 $n = null;
515 if (!empty($vars)) {
516 $n = "\n";
517 }
518 return $out . implode(",", $vars) . "{$n})";
519 break;
520 case 'resource':
521 return strtolower(gettype($var));
522 break;
523 case 'null':
524 return 'null';
525 break;
526 }
527 }
528
529 /**
530 * Handles object to string conversion.
531 *
532 * @param string $var Object to convert
533 * @return string
534 * @access private
535 * @see Debugger::exportVar()
536 */
537 function __object($var) {
538 $out = array();
539
540 if (is_object($var)) {
541 $className = get_class($var);
542 $objectVars = get_object_vars($var);
543
544 foreach ($objectVars as $key => $value) {
545 if (is_object($value)) {
546 $value = get_class($value) . ' object';
547 } elseif (is_array($value)) {
548 $value = 'array';
549 } elseif ($value === null) {
550 $value = 'NULL';
551 } elseif (in_array(gettype($value), array('boolean', 'integer', 'double', 'string', 'array', 'resource'))) {
552 $value = Debugger::exportVar($value);
553 }
554 $out[] = "$className::$$key = " . $value;
555 }
556 }
557 return implode("\n", $out);
558 }
559
560 /**
561 * Switches output format, updates format strings
562 *
563 * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for
564 * straight HTML output, or 'txt' for unformatted text.
565 * @param array $strings Template strings to be used for the output format.
566 * @access protected
567 */
568 function output($format = null, $strings = array()) {
569 $_this =& Debugger::getInstance();
570 $data = null;
571
572 if (is_null($format)) {
573 return $_this->_outputFormat;
574 }
575
576 if (!empty($strings)) {
577 if (isset($_this->_templates[$format])) {
578 if (isset($strings['links'])) {
579 $_this->_templates[$format]['links'] = array_merge(
580 $_this->_templates[$format]['links'],
581 $strings['links']
582 );
583 unset($strings['links']);
584 }
585 $_this->_templates[$format] = array_merge($_this->_templates[$format], $strings);
586 } else {
587 $_this->_templates[$format] = $strings;
588 }
589 return $_this->_templates[$format];
590 }
591
592 if ($format === true && !empty($_this->_data)) {
593 $data = $_this->_data;
594 $_this->_data = array();
595 $format = false;
596 }
597 $_this->_outputFormat = $format;
598
599 return $data;
600 }
601
602 /**
603 * Renders error messages
604 *
605 * @param array $data Data about the current error
606 * @access private
607 */
608 function _output($data = array()) {
609 $defaults = array(
610 'level' => 0,
611 'error' => 0,
612 'code' => 0,
613 'helpID' => null,
614 'description' => '',
615 'file' => '',
616 'line' => 0,
617 'context' => array()
618 );
619 $data += $defaults;
620
621 $files = $this->trace(array('start' => 2, 'format' => 'points'));
622 $code = $this->excerpt($files[0]['file'], $files[0]['line'] - 1, 1);
623 $trace = $this->trace(array('start' => 2, 'depth' => '20'));
624 $insertOpts = array('before' => '{:', 'after' => '}');
625 $context = array();
626 $links = array();
627 $info = '';
628
629 foreach ((array)$data['context'] as $var => $value) {
630 $context[] = "\${$var}\t=\t" . $this->exportVar($value, 1);
631 }
632
633 switch ($this->_outputFormat) {
634 case false:
635 $this->_data[] = compact('context', 'trace') + $data;
636 return;
637 case 'log':
638 $this->log(compact('context', 'trace') + $data);
639 return;
640 }
641
642 if (empty($this->_outputFormat) || !isset($this->_templates[$this->_outputFormat])) {
643 $this->_outputFormat = 'js';
644 }
645
646 $data['id'] = 'cakeErr' . count($this->errors);
647 $tpl = array_merge($this->_templates['base'], $this->_templates[$this->_outputFormat]);
648 $insert = array('context' => join("\n", $context), 'helpPath' => $this->helpPath) + $data;
649
650 $detect = array('help' => 'helpID', 'context' => 'context');
651
652 if (isset($tpl['links'])) {
653 foreach ($tpl['links'] as $key => $val) {
654 if (isset($detect[$key]) && empty($insert[$detect[$key]])) {
655 continue;
656 }
657 $links[$key] = String::insert($val, $insert, $insertOpts);
658 }
659 }
660
661 foreach (array('code', 'context', 'trace') as $key) {
662 if (empty($$key) || !isset($tpl[$key])) {
663 continue;
664 }
665 if (is_array($$key)) {
666 $$key = join("\n", $$key);
667 }
668 $info .= String::insert($tpl[$key], compact($key) + $insert, $insertOpts);
669 }
670 $links = join(' | ', $links);
671 unset($data['context']);
672
673 echo String::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts);
674 }
675
676 /**
677 * Verifies that the application's salt and cipher seed value has been changed from the default value.
678 *
679 * @access public
680 * @static
681 */
682 function checkSecurityKeys() {
683 if (Configure::read('Security.salt') == 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi') {
684 trigger_error(__('Please change the value of \'Security.salt\' in app/config/core.php to a salt value specific to your application', true), E_USER_NOTICE);
685 }
686
687 if (Configure::read('Security.cipherSeed') === '76859309657453542496749683645') {
688 trigger_error(__('Please change the value of \'Security.cipherSeed\' in app/config/core.php to a numeric (digits only) seed value specific to your application', true), E_USER_NOTICE);
689 }
690 }
691
692 /**
693 * Invokes the given debugger object as the current error handler, taking over control from the
694 * previous handler in a stack-like hierarchy.
695 *
696 * @param object $debugger A reference to the Debugger object
697 * @access public
698 * @static
699 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
700 */
701 function invoke(&$debugger) {
702 set_error_handler(array(&$debugger, 'handleError'));
703 }
704 }
705
706 if (!defined('DISABLE_DEFAULT_ERROR_HANDLING')) {
707 Debugger::invoke(Debugger::getInstance());
708 }