3 * Framework debugging and PHP error-handling class
5 * Provides enhanced logging, stack traces, and rendering debug views
9 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
10 * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
12 * Licensed under The MIT License
13 * Redistributions of files must retain the above copyright notice.
15 * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org)
16 * @link http://cakephp.org CakePHP(tm) Project
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)
27 if (!class_exists('Object')) {
28 require_once LIBS
. 'object.php';
30 if (!class_exists('CakeLog')) {
31 require_once LIBS
. 'cake_log.php';
33 if (!class_exists('String')) {
34 require_once LIBS
. 'string.php';
38 * Provide custom logging and error handling.
40 * Debugger overrides PHP's default error handling to provide stack traces and enhanced logging
43 * @subpackage cake.cake.libs
44 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
46 class Debugger
extends Object {
49 * A list of errors generated by the application.
54 var $errors = array();
57 * Contains the base URL for error code documentation.
65 * The current output format.
70 var $_outputFormat = 'js';
73 * Templates used when generating trace or error strings. Can be global or indexed by the format
74 * value used in $_outputFormat.
79 var $_templates = array(
81 'trace' => '{:reference} - {:path}, line {:line}',
82 'error' => "{:error} ({:code}): {:description} in [{:file}, line {:line}]"
87 'trace' => '<pre class="stack-trace">{:trace}</pre>',
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>'
97 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}",
98 'context' => "Context:\n{:context}\n",
99 'trace' => "Trace:\n{:trace}\n",
104 'traceLine' => '{:reference} - {:path}, line {:line}'
109 * Holds current output data when outputFormat is false.
114 var $_data = array();
120 function __construct() {
121 $docRef = ini_get('docref_root');
123 if (empty($docRef) && function_exists('ini_set')) {
124 ini_set('docref_root', 'http://php.net/');
126 if (!defined('E_RECOVERABLE_ERROR')) {
127 define('E_RECOVERABLE_ERROR', 4096);
129 if (!defined('E_DEPRECATED')) {
130 define('E_DEPRECATED', 8192);
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>]';
139 $e .= '<div id="{:id}-trace" class="cake-stack-trace" style="display: none;">';
140 $e .= '{:links}{:info}</div>';
142 $this->_templates
['js']['error'] = $e;
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;
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;
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;
159 $links['help'] = '<a href="{:helpPath}{:code}" target="_blank">Help</a>';
160 $this->_templates
['js']['links'] = $links;
162 $this->_templates
['js']['context'] = '<pre id="{:id}-context" class="cake-context" ';
163 $this->_templates
['js']['context'] .= 'style="display: none;">{:context}</pre>';
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>';
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;
172 $this->_templates
['html']['context'] = '<pre class="cake-debug context"><b>Context</b> ';
173 $this->_templates
['html']['context'] .= '<p>{:context}</p></pre>';
177 * Returns a reference to the Debugger singleton object instance.
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');
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');
206 * Formats and outputs the contents of the supplied variable.
208 * @param $var mixed the variable to dump
210 * @see Debugger::exportVar()
213 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
215 function dump($var) {
216 $_this =& Debugger
::getInstance();
217 pr($_this->exportVar($var));
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.
224 * @param $var mixed Variable or content to log
225 * @param $level int type of log to use. Defaults to LOG_DEBUG
228 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
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));
237 * Overrides PHP's default error handling.
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
247 function handleError($code, $description, $file = null, $line = null, $context = null) {
248 if (error_reporting() == 0 ||
$code === 2048 ||
$code === 8192) {
252 $_this =& Debugger
::getInstance();
255 $file = '[internal]';
260 $path = $_this->trimPath($file);
262 $info = compact('code', 'description', 'file', 'line');
263 if (!in_array($info, $_this->errors
)) {
264 $_this->errors
[] = $info;
273 case E_COMPILE_ERROR
:
275 $error = 'Fatal Error';
280 case E_COMPILE_WARNING
:
281 case E_RECOVERABLE_ERROR
:
283 $level = LOG_WARNING
;
296 if (!empty($_this->helpPath
) && preg_match('/.*\[([0-9]+)\]$/', $description, $codes)) {
297 if (isset($codes[1])) {
299 $description = trim(preg_replace('/\[[0-9]+\]$/', '', $description));
304 'level', 'error', 'code', 'helpID', 'description', 'file', 'path', 'line', 'context'
306 echo $_this->_output($data);
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));
314 if ($error == 'Fatal Error') {
321 * Outputs a stack trace based on the supplied options.
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
330 * - `start` - The stack frame to start generating a trace from. Defaults to 0
332 * @param array $options Format for outputting stack trace
333 * @return mixed Formatted stack trace
336 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
338 function trace($options = array()) {
339 $_this =& Debugger
::getInstance();
342 'format' => $_this->_outputFormat
,
348 $options +
= $defaults;
350 $backtrace = debug_backtrace();
351 $count = count($backtrace);
356 'file' => '[internal]',
358 'function' => '[main]'
361 for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++
) {
362 $trace = array_merge(array('file' => '[internal]', 'line' => '??'), $backtrace[$i]);
364 if (isset($backtrace[$i +
1])) {
365 $next = array_merge($_trace, $backtrace[$i +
1]);
366 $reference = $next['function'];
368 if (!empty($next['class'])) {
369 $reference = $next['class'] . '::' . $reference . '(';
370 if ($options['args'] && isset($next['args'])) {
372 foreach ($next['args'] as $arg) {
373 $args[] = Debugger
::exportVar($arg);
375 $reference .= join(', ', $args);
380 $reference = '[main]';
382 if (in_array($reference, array('call_user_func_array', 'trigger_error'))) {
385 if ($options['format'] == 'points' && $trace['file'] != '[internal]') {
386 $back[] = array('file' => $trace['file'], 'line' => $trace['line']);
387 } elseif ($options['format'] == 'array') {
390 if (isset($_this->_templates
[$options['format']]['traceLine'])) {
391 $tpl = $_this->_templates
[$options['format']]['traceLine'];
393 $tpl = $_this->_templates
['base']['traceLine'];
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' => '}'));
402 if ($options['format'] == 'array' ||
$options['format'] == 'points') {
405 return implode("\n", $back);
409 * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core
412 * @param string $path Path to shorten
413 * @return string Normalized path
417 function trimPath($path) {
418 if (!defined('CAKE_CORE_INCLUDE_PATH') ||
!defined('APP')) {
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);
429 $corePaths = App
::core('cake');
431 foreach ($corePaths as $corePath) {
432 if (strpos($path, $corePath) === 0) {
433 return str_replace($corePath, 'CORE' .DS
. 'cake' .DS
, $path);
440 * Grabs an excerpt from a file and highlights a given line of code
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
448 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
450 function excerpt($file, $line, $context = 2) {
451 $data = $lines = array();
452 if (!file_exists($file)) {
455 $data = @explode
("\n", file_get_contents($file));
457 if (empty($data) ||
!isset($data[$line])) {
460 for ($i = $line - ($context +
1); $i < $line +
$context; $i++
) {
461 if (!isset($data[$i])) {
464 $string = str_replace(array("\r\n", "\n"), "", highlight_string($data[$i], true));
466 $lines[] = '<span class="code-highlight">' . $string . '</span>';
475 * Converts a variable to a string for debug output.
477 * @param string $var Variable to convert
478 * @return string Variable as a formatted string
481 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
483 function exportVar($var, $recursion = 0) {
484 $_this =& Debugger
::getInstance();
485 switch (strtolower(gettype($var))) {
487 return ($var) ?
'true' : 'false';
494 if (trim($var) == "") {
497 return '"' . h($var) . '"';
500 return get_class($var) . "\n" . $_this->__object($var);
504 foreach ($var as $key => $val) {
505 if ($recursion >= 0) {
506 if (is_numeric($key)) {
507 $vars[] = "\n\t" . $_this->exportVar($val, $recursion - 1);
509 $vars[] = "\n\t" .$_this->exportVar($key, $recursion - 1)
510 . ' => ' . $_this->exportVar($val, $recursion - 1);
518 return $out . implode(",", $vars) . "{$n})";
521 return strtolower(gettype($var));
530 * Handles object to string conversion.
532 * @param string $var Object to convert
535 * @see Debugger::exportVar()
537 function __object($var) {
540 if (is_object($var)) {
541 $className = get_class($var);
542 $objectVars = get_object_vars($var);
544 foreach ($objectVars as $key => $value) {
545 if (is_object($value)) {
546 $value = get_class($value) . ' object';
547 } elseif (is_array($value)) {
549 } elseif ($value === null) {
551 } elseif (in_array(gettype($value), array('boolean', 'integer', 'double', 'string', 'array', 'resource'))) {
552 $value = Debugger
::exportVar($value);
554 $out[] = "$className::$$key = " . $value;
557 return implode("\n", $out);
561 * Switches output format, updates format strings
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.
568 function output($format = null, $strings = array()) {
569 $_this =& Debugger
::getInstance();
572 if (is_null($format)) {
573 return $_this->_outputFormat
;
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'],
583 unset($strings['links']);
585 $_this->_templates
[$format] = array_merge($_this->_templates
[$format], $strings);
587 $_this->_templates
[$format] = $strings;
589 return $_this->_templates
[$format];
592 if ($format === true && !empty($_this->_data
)) {
593 $data = $_this->_data
;
594 $_this->_data
= array();
597 $_this->_outputFormat
= $format;
603 * Renders error messages
605 * @param array $data Data about the current error
608 function _output($data = array()) {
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' => '}');
629 foreach ((array)$data['context'] as $var => $value) {
630 $context[] = "\${$var}\t=\t" . $this->exportVar($value, 1);
633 switch ($this->_outputFormat
) {
635 $this->_data
[] = compact('context', 'trace') +
$data;
638 $this->log(compact('context', 'trace') +
$data);
642 if (empty($this->_outputFormat
) ||
!isset($this->_templates
[$this->_outputFormat
])) {
643 $this->_outputFormat
= 'js';
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;
650 $detect = array('help' => 'helpID', 'context' => 'context');
652 if (isset($tpl['links'])) {
653 foreach ($tpl['links'] as $key => $val) {
654 if (isset($detect[$key]) && empty($insert[$detect[$key]])) {
657 $links[$key] = String::insert($val, $insert, $insertOpts);
661 foreach (array('code', 'context', 'trace') as $key) {
662 if (empty($
$key) ||
!isset($tpl[$key])) {
665 if (is_array($
$key)) {
666 $
$key = join("\n", $
$key);
668 $info .= String::insert($tpl[$key], compact($key) +
$insert, $insertOpts);
670 $links = join(' | ', $links);
671 unset($data['context']);
673 echo String::insert($tpl['error'], compact('links', 'info') +
$data, $insertOpts);
677 * Verifies that the application's salt and cipher seed value has been changed from the default value.
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
);
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
);
693 * Invokes the given debugger object as the current error handler, taking over control from the
694 * previous handler in a stack-like hierarchy.
696 * @param object $debugger A reference to the Debugger object
699 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
701 function invoke(&$debugger) {
702 set_error_handler(array(&$debugger, 'handleError'));
706 if (!defined('DISABLE_DEFAULT_ERROR_HANDLING')) {
707 Debugger
::invoke(Debugger
::getInstance());